705 lines
23 KiB
Rust
705 lines
23 KiB
Rust
//! Runs the full server simulation headless, the same way singleplayer does:
|
|
//! client events are triggered locally and handled by the server observers.
|
|
|
|
use std::time::Duration;
|
|
|
|
use bevy::{prelude::*, state::app::StatesPlugin, time::TimeUpdateStrategy};
|
|
use bevy_replicon::prelude::*;
|
|
use game_core::{CorePlugin, components::*, protocol::*};
|
|
use game_server::{GameServerPlugin, ServerSettings};
|
|
|
|
#[derive(Resource, Default)]
|
|
struct Log(Vec<String>);
|
|
|
|
fn app() -> App {
|
|
let mut app = App::new();
|
|
app.add_plugins((
|
|
MinimalPlugins,
|
|
StatesPlugin,
|
|
RepliconPlugins,
|
|
CorePlugin,
|
|
GameServerPlugin {
|
|
settings: ServerSettings::default(),
|
|
},
|
|
))
|
|
.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(
|
|
50,
|
|
)))
|
|
.init_resource::<Log>()
|
|
.add_observer(|ev: On<GameLog>, mut log: ResMut<Log>| log.0.push(ev.text.clone()));
|
|
// Normally done by `App::run`; replicon builds its message systems in `finish`.
|
|
app.finish();
|
|
app.cleanup();
|
|
app.update();
|
|
app
|
|
}
|
|
|
|
fn send(app: &mut App, ev: impl Event) {
|
|
app.world_mut().client_trigger(ev);
|
|
// Resent locally in PostUpdate, triggered on the server in the next PreUpdate.
|
|
app.update();
|
|
app.update();
|
|
}
|
|
|
|
const PLAYER_ID: &str = "4f1c0c56-1111-4a5e-9a1e-000000000001";
|
|
|
|
fn login(name: &str) -> Login {
|
|
Login {
|
|
player_id: PLAYER_ID.into(),
|
|
name: name.into(),
|
|
password: String::new(),
|
|
}
|
|
}
|
|
|
|
fn take_log(app: &mut App) -> Vec<String> {
|
|
// Server events also go through a local resend before the client observer sees them.
|
|
app.update();
|
|
std::mem::take(&mut app.world_mut().resource_mut::<Log>().0)
|
|
}
|
|
|
|
#[test]
|
|
fn admin_tiled_import_replaces_world_preserves_characters_and_rejects_invalid_maps() {
|
|
use game_server::{
|
|
commands::{self, CommandSource},
|
|
map_io, persistence,
|
|
world::WorldLayout,
|
|
};
|
|
|
|
let mut app = app();
|
|
send(&mut app, login("MapAdmin"));
|
|
let mut expected = persistence::snapshot(app.world_mut());
|
|
let character = app
|
|
.world_mut()
|
|
.query_filtered::<Entity, With<Character>>()
|
|
.single(app.world())
|
|
.unwrap();
|
|
let file = std::env::temp_dir().join(format!("station-import-{}.tmx", uuid::Uuid::new_v4()));
|
|
let filename = file.to_str().unwrap();
|
|
let ordinary_player = CommandSource::Player {
|
|
client: ClientId::Server,
|
|
admin: false,
|
|
owner: false,
|
|
};
|
|
let denied = commands::execute(
|
|
app.world_mut(),
|
|
ordinary_player,
|
|
&format!("export {filename}"),
|
|
);
|
|
assert!(denied[0].contains("not allowed"));
|
|
assert!(!file.exists());
|
|
let denied = commands::execute(
|
|
app.world_mut(),
|
|
ordinary_player,
|
|
&format!("import {filename}"),
|
|
);
|
|
assert!(denied[0].contains("not allowed"));
|
|
let reply = commands::execute(
|
|
app.world_mut(),
|
|
CommandSource::Console,
|
|
&format!("export {filename}"),
|
|
);
|
|
assert!(reply[0].contains("exported"), "{reply:?}");
|
|
let original = std::fs::read_to_string(&file).unwrap();
|
|
assert!(
|
|
!original.contains("MapAdmin"),
|
|
"characters must not be exported"
|
|
);
|
|
let mut map = game_core::map::MapLayout::load(&game_core::assets_dir(), filename).unwrap();
|
|
let door = map
|
|
.objects
|
|
.iter_mut()
|
|
.find(|o| o.class == "building" && o.properties["proto"] == "door")
|
|
.unwrap();
|
|
door.properties.insert("state.health".into(), "11".into());
|
|
map.layers[1].cells[0] = None;
|
|
let mut edited = map_io::decode(map, app.world().resource::<game_core::GameData>()).unwrap();
|
|
edited.items.truncate(1);
|
|
edited.buildings.retain(|b| b.proto != "tree");
|
|
map_io::write(&edited, &file).unwrap();
|
|
edited = map_io::decode(
|
|
game_core::map::MapLayout::load(&game_core::assets_dir(), filename).unwrap(),
|
|
app.world().resource::<game_core::GameData>(),
|
|
)
|
|
.unwrap();
|
|
let reply = commands::execute(
|
|
app.world_mut(),
|
|
CommandSource::Console,
|
|
&format!("import {filename}"),
|
|
);
|
|
assert!(reply[0].contains("imported"), "{reply:?}");
|
|
let imported = persistence::snapshot(app.world_mut());
|
|
assert!(
|
|
!app.world()
|
|
.resource::<WorldLayout>()
|
|
.0
|
|
.is_solid(GridPos::new(0, 0))
|
|
);
|
|
assert_eq!(imported.buildings, edited.buildings);
|
|
assert_eq!(imported.items, edited.items);
|
|
assert_eq!(imported.characters, expected.characters);
|
|
assert!(
|
|
app.world().get::<Character>(character).is_some(),
|
|
"online entity must survive"
|
|
);
|
|
assert_eq!(
|
|
app.world().resource::<WorldLayout>().0.as_ref(),
|
|
edited.layout.as_ref().unwrap()
|
|
);
|
|
let mut maps = app.world_mut().query::<&WorldMap>();
|
|
assert_eq!(
|
|
&maps.single(app.world()).unwrap().layout,
|
|
edited.layout.as_ref().unwrap()
|
|
);
|
|
std::fs::write(
|
|
&file,
|
|
original.replace(
|
|
"proto\" type=\"string\" value=\"door",
|
|
"proto\" type=\"string\" value=\"missing",
|
|
),
|
|
)
|
|
.unwrap();
|
|
let reply = commands::execute(
|
|
app.world_mut(),
|
|
CommandSource::Console,
|
|
&format!("import {filename}"),
|
|
);
|
|
assert!(reply[0].contains("failed"), "{reply:?}");
|
|
assert_eq!(persistence::snapshot(app.world_mut()), imported);
|
|
let settings = persistence::StorageSettings {
|
|
database_url: "sqlite::memory:".into(),
|
|
world: persistence::WorldSelector::Name("persistent".into()),
|
|
};
|
|
let (storage, _) =
|
|
persistence::Storage::open(&settings, game_core::DEFAULT_MAP, || Ok(imported.clone()))
|
|
.unwrap();
|
|
app.world_mut().insert_resource(storage);
|
|
map_io::write(&expected, &file).unwrap();
|
|
map_io::import(app.world_mut(), &file).unwrap();
|
|
let storage = app.world().resource::<persistence::Storage>();
|
|
let persisted = storage
|
|
.runtime
|
|
.block_on(storage.db.load_world(storage.world.id))
|
|
.unwrap();
|
|
expected.characters = imported.characters;
|
|
assert_eq!(persisted.buildings, expected.buildings);
|
|
assert_eq!(persisted.items, expected.items);
|
|
assert_eq!(persisted.characters, expected.characters);
|
|
assert_eq!(
|
|
persisted.layout.as_ref().unwrap().layers,
|
|
expected.layout.as_ref().unwrap().layers
|
|
);
|
|
let before_failure = persistence::snapshot(app.world_mut());
|
|
let storage = app.world().resource::<persistence::Storage>();
|
|
storage.runtime.block_on(storage.db.close());
|
|
map_io::write(&edited, &file).unwrap();
|
|
assert!(map_io::import(app.world_mut(), &file).is_err());
|
|
assert_eq!(persistence::snapshot(app.world_mut()), before_failure);
|
|
std::fs::remove_file(file).unwrap();
|
|
}
|
|
|
|
fn count(app: &App, me: Entity, item: &str) -> u32 {
|
|
app.world().get::<Inventory>(me).unwrap().count(item)
|
|
}
|
|
|
|
#[test]
|
|
fn login_move_and_interact() {
|
|
let mut app = app();
|
|
|
|
// The map defines buildings and items.
|
|
let mut buildings = app.world_mut().query::<&Building>();
|
|
assert_eq!(buildings.iter(app.world()).count(), 19);
|
|
|
|
send(&mut app, login("Tester"));
|
|
let me = app
|
|
.world_mut()
|
|
.query_filtered::<Entity, With<Character>>()
|
|
.single(app.world())
|
|
.expect("one character");
|
|
let start = *app.world().get::<GridPos>(me).unwrap();
|
|
let log = take_log(&mut app);
|
|
assert!(log.iter().any(|t| t.contains("Tester")), "{log:?}");
|
|
assert_eq!(count(&app, me, "apple"), 2);
|
|
|
|
// A second login of the same client is ignored.
|
|
send(&mut app, login("Tester"));
|
|
let mut characters = app.world_mut().query::<&Character>();
|
|
assert_eq!(characters.iter(app.world()).count(), 1);
|
|
|
|
// The local client is the owner and may use admin commands.
|
|
send(
|
|
&mut app,
|
|
Chat {
|
|
text: "/whitelist add Someone".into(),
|
|
},
|
|
);
|
|
let log = take_log(&mut app);
|
|
assert!(
|
|
log.iter().any(|t| t.contains("whitelisted Someone")),
|
|
"{log:?}"
|
|
);
|
|
let config = app.world().resource::<game_server::config::ServerConfig>();
|
|
assert_eq!(config.access.whitelist, ["Someone"]);
|
|
send(
|
|
&mut app,
|
|
Chat {
|
|
text: "hello there".into(),
|
|
},
|
|
);
|
|
let log = take_log(&mut app);
|
|
assert!(log.iter().any(|t| t == "<Tester> hello there"), "{log:?}");
|
|
|
|
// Walk east until the character crosses into the next tile.
|
|
let start_x = app.world().get::<Position>(me).unwrap().0.x;
|
|
send(&mut app, SetMovement { x: 1, y: 0 });
|
|
for _ in 0..20 {
|
|
if app.world().get::<GridPos>(me).unwrap().x != start.x {
|
|
break;
|
|
}
|
|
app.update();
|
|
}
|
|
send(&mut app, SetMovement::default());
|
|
let pos = *app.world().get::<GridPos>(me).unwrap();
|
|
let position = *app.world().get::<Position>(me).unwrap();
|
|
assert_eq!(pos, GridPos::new(start.x + 1, start.y), "start {start:?}");
|
|
assert_eq!(position.tile(), pos);
|
|
assert!(position.0.x > start_x + 0.5, "{position:?}");
|
|
for _ in 0..3 {
|
|
app.update();
|
|
}
|
|
assert_eq!(
|
|
app.world().get::<Position>(me).unwrap(),
|
|
&position,
|
|
"stopped"
|
|
);
|
|
|
|
// Eat an apple through the Rune script.
|
|
app.world_mut().get_mut::<Vitals>(me).unwrap().nutrition = 50.0;
|
|
send(&mut app, UseItem { slot: 0 });
|
|
let vitals = app.world().get::<Vitals>(me).unwrap();
|
|
assert!(vitals.nutrition > 50.0, "{vitals:?}");
|
|
assert_eq!(count(&app, me, "apple"), 1);
|
|
assert!(take_log(&mut app).iter().any(|t| t.contains("Delicious")));
|
|
|
|
// Build a table next to us.
|
|
let target = GridPos::new(pos.x, pos.y + 1);
|
|
let data = app.world().resource::<game_core::GameData>().clone();
|
|
app.world_mut()
|
|
.get_mut::<Inventory>(me)
|
|
.unwrap()
|
|
.add("plank", 2, &data);
|
|
send(
|
|
&mut app,
|
|
Construct {
|
|
building: "table".into(),
|
|
target,
|
|
},
|
|
);
|
|
let log = take_log(&mut app);
|
|
let mut q = app.world_mut().query::<(&Building, &GridPos)>();
|
|
assert!(
|
|
q.iter(app.world())
|
|
.any(|(b, p)| b.proto == "table" && *p == target),
|
|
"{log:?}"
|
|
);
|
|
assert_eq!(count(&app, me, "plank"), 0);
|
|
|
|
// Doors toggle via script.
|
|
let door_pos = GridPos::new(pos.x + 1, pos.y + 1);
|
|
app.world_mut().spawn((
|
|
Building {
|
|
proto: "door".into(),
|
|
},
|
|
door_pos,
|
|
BuildingState {
|
|
solid: true,
|
|
..default()
|
|
},
|
|
));
|
|
send(&mut app, Interact { target: door_pos });
|
|
let mut q = app
|
|
.world_mut()
|
|
.query::<(&Building, &GridPos, &BuildingState)>();
|
|
let (_, _, state) = q
|
|
.iter(app.world())
|
|
.find(|(b, p, _)| b.proto == "door" && **p == door_pos)
|
|
.unwrap();
|
|
assert!(!state.solid);
|
|
assert_eq!(state.variant.as_deref(), Some("open"));
|
|
|
|
// Deconstruction refunds the materials.
|
|
send(&mut app, Deconstruct { target });
|
|
let mut q = app.world_mut().query::<(&Building, &GridPos)>();
|
|
assert!(!q.iter(app.world()).any(|(_, p)| *p == target));
|
|
assert_eq!(count(&app, me, "plank"), 2);
|
|
|
|
// Drop and pick up again.
|
|
let slot = app
|
|
.world()
|
|
.get::<Inventory>(me)
|
|
.unwrap()
|
|
.slots
|
|
.iter()
|
|
.position(|s| s.item == "plank")
|
|
.unwrap();
|
|
send(&mut app, DropItem { slot });
|
|
assert_eq!(count(&app, me, "plank"), 0);
|
|
send(&mut app, PickUp { target: pos });
|
|
assert_eq!(count(&app, me, "plank"), 2);
|
|
|
|
// Crafting without a station fails with a reason.
|
|
take_log(&mut app);
|
|
send(
|
|
&mut app,
|
|
Craft {
|
|
recipe: "saw_planks".into(),
|
|
},
|
|
);
|
|
let log = take_log(&mut app);
|
|
assert!(log.iter().any(|t| t.contains("workbench")), "{log:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn machines_process_items() {
|
|
let mut app = app();
|
|
send(&mut app, login("Smith"));
|
|
let me = app
|
|
.world_mut()
|
|
.query_filtered::<Entity, With<Character>>()
|
|
.single(app.world())
|
|
.unwrap();
|
|
let pos = *app.world().get::<GridPos>(me).unwrap();
|
|
let target = GridPos::new(pos.x + 1, pos.y);
|
|
let data = app.world().resource::<game_core::GameData>().clone();
|
|
// Ignore anything the map placed there.
|
|
let mut q = app.world_mut().query::<(Entity, &GridPos)>();
|
|
let blocking: Vec<_> = q
|
|
.iter(app.world())
|
|
.filter(|(e, p)| **p == target && *e != me)
|
|
.map(|(e, _)| e)
|
|
.collect();
|
|
for e in blocking {
|
|
app.world_mut().despawn(e);
|
|
}
|
|
{
|
|
let mut inventory = app.world_mut().get_mut::<Inventory>(me).unwrap();
|
|
inventory.slots.clear();
|
|
inventory.add("iron_ore", 5, &data);
|
|
inventory.add("log", 1, &data);
|
|
inventory.add("plank", 1, &data);
|
|
inventory.add("screwdriver", 1, &data);
|
|
inventory.add("iron_plate", 4, &data);
|
|
}
|
|
send(
|
|
&mut app,
|
|
Construct {
|
|
building: "furnace".into(),
|
|
target,
|
|
},
|
|
);
|
|
let furnace = app
|
|
.world_mut()
|
|
.query::<(Entity, &Building, &GridPos)>()
|
|
.iter(app.world())
|
|
.find(|(_, b, p)| b.proto == "furnace" && **p == target)
|
|
.map(|(e, _, _)| e);
|
|
let log = take_log(&mut app);
|
|
let Some(furnace) = furnace else {
|
|
panic!("furnace not built: {log:?}");
|
|
};
|
|
assert!(app.world().get::<Machine>(furnace).is_some());
|
|
{
|
|
let mut inventory = app.world_mut().get_mut::<Inventory>(me).unwrap();
|
|
inventory.add("iron_ore", 4, &data);
|
|
inventory.add("log", 1, &data);
|
|
}
|
|
let slot_of = |app: &App, item: &str| {
|
|
app.world()
|
|
.get::<Inventory>(me)
|
|
.unwrap()
|
|
.slots
|
|
.iter()
|
|
.position(|s| s.item == item)
|
|
.unwrap()
|
|
};
|
|
for item in ["iron_ore", "log"] {
|
|
let slot = slot_of(&app, item);
|
|
send(
|
|
&mut app,
|
|
MachineInsert {
|
|
target,
|
|
inventory_slot: slot,
|
|
machine_slot: None,
|
|
count: u32::MAX,
|
|
},
|
|
);
|
|
}
|
|
// 1 ore left over from construction plus 4 added.
|
|
assert_eq!(count(&app, me, "iron_ore"), 0);
|
|
let machine = app.world().get::<Machine>(furnace).unwrap();
|
|
assert_eq!(machine.input_count("iron_ore"), 5);
|
|
assert_eq!(machine.recipe.as_deref(), Some("smelt_iron"));
|
|
let state = app.world().get::<BuildingState>(furnace).unwrap();
|
|
assert_eq!(state.variant.as_deref(), Some("lit"));
|
|
|
|
// 4 seconds of 50 ms steps.
|
|
for _ in 0..90 {
|
|
app.update();
|
|
}
|
|
let machine = app.world().get::<Machine>(furnace).unwrap().clone();
|
|
assert_eq!(machine.input_count("iron_ore"), 1, "{machine:?}");
|
|
assert_eq!(machine.outputs[0], Some(ItemStack::new("iron_plate", 2)));
|
|
assert_eq!(machine.recipe, None);
|
|
let state = app.world().get::<BuildingState>(furnace).unwrap();
|
|
assert_eq!(state.variant, None);
|
|
|
|
send(
|
|
&mut app,
|
|
MachineTake {
|
|
target,
|
|
slot: MachineSlot::Output(0),
|
|
count: u32::MAX,
|
|
},
|
|
);
|
|
assert_eq!(count(&app, me, "iron_plate"), 6);
|
|
|
|
// Taking a machine apart returns its contents.
|
|
send(&mut app, Deconstruct { target });
|
|
send(&mut app, PickUp { target });
|
|
assert_eq!(count(&app, me, "iron_ore"), 5);
|
|
}
|
|
|
|
mod network {
|
|
use std::{
|
|
net::{Ipv4Addr, SocketAddr},
|
|
time::Instant,
|
|
};
|
|
|
|
use bevy_replicon_renet::RepliconRenetPlugins;
|
|
use game_server::{
|
|
config::ServerConfig,
|
|
embedded::{EmbeddedServer, ServerStatus},
|
|
network::create_client,
|
|
};
|
|
|
|
use super::*;
|
|
|
|
const HOST: &str = "4f1c0c56-1111-4a5e-9a1e-0000000000aa";
|
|
const GUEST: &str = "4f1c0c56-1111-4a5e-9a1e-0000000000bb";
|
|
|
|
#[derive(Resource, Default)]
|
|
struct Inbox {
|
|
welcome: Option<Welcome>,
|
|
rejected: Option<String>,
|
|
log: Vec<String>,
|
|
}
|
|
|
|
fn client(addr: SocketAddr) -> App {
|
|
let mut app = App::new();
|
|
app.add_plugins((
|
|
MinimalPlugins,
|
|
StatesPlugin,
|
|
RepliconPlugins,
|
|
RepliconRenetPlugins,
|
|
CorePlugin,
|
|
))
|
|
.init_resource::<Inbox>()
|
|
.add_observer(|ev: On<Welcome>, mut inbox: ResMut<Inbox>| {
|
|
inbox.welcome = Some(ev.event().clone())
|
|
})
|
|
.add_observer(|ev: On<Rejected>, mut inbox: ResMut<Inbox>| {
|
|
inbox.rejected = Some(ev.reason.clone())
|
|
})
|
|
.add_observer(|ev: On<GameLog>, mut inbox: ResMut<Inbox>| inbox.log.push(ev.text.clone()));
|
|
app.finish();
|
|
app.cleanup();
|
|
let channels = app.world().resource::<RepliconChannels>();
|
|
let (client, transport) = create_client(channels, addr).unwrap();
|
|
app.insert_resource(client).insert_resource(transport);
|
|
app
|
|
}
|
|
|
|
/// Updates the clients until `done` returns true for the first one.
|
|
fn pump(apps: &mut [&mut App], done: impl Fn(&mut App) -> bool) {
|
|
let start = Instant::now();
|
|
loop {
|
|
for app in apps.iter_mut() {
|
|
app.update();
|
|
}
|
|
if done(apps[0]) {
|
|
return;
|
|
}
|
|
assert!(start.elapsed() < Duration::from_secs(20), "timed out");
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
fn connected(app: &mut App) -> bool {
|
|
*app.world().resource::<State<ClientState>>() == ClientState::Connected
|
|
}
|
|
|
|
fn join(app: &mut App, id: &str, name: &str, password: &str) {
|
|
pump(&mut [app], connected);
|
|
app.world_mut().client_trigger(Login {
|
|
player_id: id.into(),
|
|
name: name.into(),
|
|
password: password.into(),
|
|
});
|
|
pump(&mut [app], |app| {
|
|
let inbox = app.world().resource::<Inbox>();
|
|
inbox.welcome.is_some() || inbox.rejected.is_some()
|
|
});
|
|
}
|
|
|
|
fn inbox(app: &App) -> &Inbox {
|
|
app.world().resource::<Inbox>()
|
|
}
|
|
|
|
fn start_server(
|
|
settings: ServerSettings,
|
|
config: ServerConfig,
|
|
) -> (EmbeddedServer, SocketAddr) {
|
|
let server = EmbeddedServer::start(settings, config, None, Ipv4Addr::LOCALHOST.into());
|
|
let start = Instant::now();
|
|
let addr = loop {
|
|
match server.poll() {
|
|
Some(ServerStatus::Started(addr)) => break addr,
|
|
Some(other) => panic!("{other:?}"),
|
|
None if start.elapsed() > Duration::from_secs(30) => panic!("server did not start"),
|
|
None => std::thread::sleep(Duration::from_millis(10)),
|
|
}
|
|
};
|
|
(server, addr)
|
|
}
|
|
|
|
#[test]
|
|
fn tiled_import_replicates_terrain_to_existing_and_joining_clients() {
|
|
use game_server::map_io;
|
|
|
|
let mut config = ServerConfig {
|
|
port: 0,
|
|
..default()
|
|
};
|
|
config.database.save = false;
|
|
let settings = ServerSettings {
|
|
owner: Some(HOST.into()),
|
|
..config.settings()
|
|
};
|
|
let (server, addr) = start_server(settings, config);
|
|
let mut host = client(addr);
|
|
join(&mut host, HOST, "Host", "");
|
|
let mut guest = client(addr);
|
|
join(&mut guest, GUEST, "Guest", "");
|
|
let file =
|
|
std::env::temp_dir().join(format!("station-network-{}.tmx", uuid::Uuid::new_v4()));
|
|
host.world_mut().client_trigger(Chat {
|
|
text: format!("/export {}", file.display()),
|
|
});
|
|
pump(&mut [&mut host, &mut guest], |app| {
|
|
inbox(app).log.iter().any(|l| l.contains("World exported:"))
|
|
});
|
|
let assets = game_core::assets_dir();
|
|
let data = game_core::GameData::load_dir(&assets.join("data")).unwrap();
|
|
let mut map = game_core::map::MapLayout::load(&assets, file.to_str().unwrap()).unwrap();
|
|
assert!(map.is_solid(GridPos::new(0, 0)));
|
|
map.layers[1].cells[0] = None;
|
|
let edited = map_io::decode(map, &data).unwrap();
|
|
map_io::write(&edited, &file).unwrap();
|
|
host.world_mut().client_trigger(Chat {
|
|
text: format!("/import {}", file.display()),
|
|
});
|
|
let imported = |app: &mut App| {
|
|
let mut maps = app.world_mut().query::<&WorldMap>();
|
|
maps.iter(app.world())
|
|
.any(|map| !map.layout.is_solid(GridPos::new(0, 0)))
|
|
};
|
|
pump(&mut [&mut host, &mut guest], imported);
|
|
pump(&mut [&mut guest, &mut host], imported);
|
|
let mut newcomer = client(addr);
|
|
join(
|
|
&mut newcomer,
|
|
"4f1c0c56-1111-4a5e-9a1e-0000000000cc",
|
|
"Newcomer",
|
|
"",
|
|
);
|
|
pump(&mut [&mut newcomer], imported);
|
|
server.stop();
|
|
std::fs::remove_file(file).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn password_admins_and_bans() {
|
|
let mut config = ServerConfig {
|
|
port: 0,
|
|
password: "letmein".into(),
|
|
..Default::default()
|
|
};
|
|
config.database.save = false;
|
|
let settings = ServerSettings {
|
|
owner: Some(HOST.into()),
|
|
..config.settings()
|
|
};
|
|
let (server, addr) = start_server(settings, config);
|
|
|
|
let mut intruder = client(addr);
|
|
join(&mut intruder, GUEST, "Guest", "wrong");
|
|
assert_eq!(
|
|
inbox(&intruder).rejected.as_deref(),
|
|
Some("Wrong password.")
|
|
);
|
|
|
|
// The owner needs no password and is always admin.
|
|
let mut host = client(addr);
|
|
join(&mut host, HOST, "Host", "");
|
|
let welcome = inbox(&host).welcome.clone().expect("host welcomed");
|
|
assert!(welcome.admin);
|
|
assert_eq!(welcome.character, "Host");
|
|
|
|
let mut guest = client(addr);
|
|
join(&mut guest, GUEST, "Guest", "letmein");
|
|
assert!(
|
|
!inbox(&guest)
|
|
.welcome
|
|
.as_ref()
|
|
.expect("guest welcomed")
|
|
.admin
|
|
);
|
|
|
|
guest.world_mut().client_trigger(Chat {
|
|
text: "/kick Host".into(),
|
|
});
|
|
pump(&mut [&mut guest, &mut host], |app| {
|
|
inbox(app).log.iter().any(|t| t.contains("not allowed"))
|
|
});
|
|
|
|
host.world_mut().client_trigger(Chat {
|
|
text: "/ban Guest".into(),
|
|
});
|
|
pump(&mut [&mut guest, &mut host], |app| {
|
|
inbox(app).rejected.is_some()
|
|
});
|
|
assert_eq!(
|
|
inbox(&guest).rejected.as_deref(),
|
|
Some("You have been banned from this server.")
|
|
);
|
|
pump(&mut [&mut host], |app| {
|
|
inbox(app).log.iter().any(|t| t.contains("banned Guest"))
|
|
});
|
|
|
|
drop(guest);
|
|
let mut again = client(addr);
|
|
join(&mut again, GUEST, "Guest", "letmein");
|
|
assert_eq!(
|
|
inbox(&again).rejected.as_deref(),
|
|
Some("You are banned from this server.")
|
|
);
|
|
pump(&mut [&mut host], |app| {
|
|
inbox(app)
|
|
.log
|
|
.iter()
|
|
.any(|t| t.contains("[admin] Rejected Guest"))
|
|
});
|
|
|
|
server.stop();
|
|
}
|
|
}
|