station/crates/game_server/tests/simulation.rs

297 lines
8.9 KiB
Rust
Raw Normal View History

//! 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::*, grid::Dir, 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();
}
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)
}
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 {
name: "Tester".into(),
},
);
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 with the same name is rejected.
send(
&mut app,
Login {
name: "Tester".into(),
},
);
let mut characters = app.world_mut().query::<&Character>();
assert_eq!(characters.iter(app.world()).count(), 1);
// Walk east one tile.
send(&mut app, SetMoveDir(Some(Dir::East)));
send(&mut app, SetMoveDir(None));
for _ in 0..5 {
app.update();
}
let pos = *app.world().get::<GridPos>(me).unwrap();
assert_eq!(pos, GridPos::new(start.x + 1, start.y), "start {start:?}");
// 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 {
name: "Smith".into(),
},
);
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);
}