//! 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); 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::() .add_observer(|ev: On, mut log: ResMut| 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 { // 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::().0) } fn count(app: &App, me: Entity, item: &str) -> u32 { app.world().get::(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::>() .single(app.world()) .expect("one character"); let start = *app.world().get::(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::(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::(me).unwrap().nutrition = 50.0; send(&mut app, UseItem { slot: 0 }); let vitals = app.world().get::(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::().clone(); app.world_mut() .get_mut::(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::(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:?}"); }