182 lines
5.4 KiB
Rust
182 lines
5.4 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::*, 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(), 18);
|
|
|
|
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: "smelt_iron".into(),
|
|
},
|
|
);
|
|
let log = take_log(&mut app);
|
|
assert!(log.iter().any(|t| t.contains("furnace")), "{log:?}");
|
|
}
|