From c2cae1f61851b7918d09ccb7505365c5a9d02a3b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 27 Feb 2026 17:46:15 +0100 Subject: [PATCH] fix(simulation): register interaction systems and persist door state (#246) Follow-up to b6a9b78: register TerminalInteractedQueue resource and door/terminal interaction systems in SimulationPlugin; add door state save/load in save_io (open doors round-trip through SaveStateV1); make WalkabilityMap param optional in process_door_interaction so plugin-only tests work without a loaded map; fix information_boundaries test missing open_doors field. Co-Authored-By: Claude Opus 4.6 --- server/src/simulation/interaction.rs | 6 +++-- server/src/simulation/mod.rs | 7 +++++ server/src/simulation/save_io.rs | 36 ++++++++++++++++++++++++++ server/tests/information_boundaries.rs | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/server/src/simulation/interaction.rs b/server/src/simulation/interaction.rs index c541c6a9c..6cff96115 100644 --- a/server/src/simulation/interaction.rs +++ b/server/src/simulation/interaction.rs @@ -407,7 +407,7 @@ pub struct TerminalInteractRequest { /// Ordering: after `process_player_input`, before movement validation. pub fn process_door_interaction( mut commands: Commands, - mut walkability: ResMut, + walkability: Option>, player_query: Query<(Entity, &DoorInteractRequest), With>, mut door_query: Query<&mut DoorState>, ) { @@ -428,7 +428,9 @@ pub fn process_door_interaction( let walkable = door.is_open; let tile = door.blocking_tile; - walkability.set_walkable(&tile, walkable); + if let Some(mut walkability) = walkability { + walkability.set_walkable(&tile, walkable); + } tracing::info!( ?tile, diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs index a0f37f2f7..f0d6958d5 100644 --- a/server/src/simulation/mod.rs +++ b/server/src/simulation/mod.rs @@ -62,6 +62,7 @@ impl Plugin for SimulationPlugin { // Init here so SimulationPlugin works standalone in tests without those plugins. .init_resource::() .init_resource::() + .init_resource::() .add_systems( Update, ( @@ -92,6 +93,12 @@ impl Plugin for SimulationPlugin { poi_discovery::discover_pois .after(crate::perception::observer::compute_visibility_geometry) .before(crate::perception::observer::compute_observer_snapshot), + interaction::process_door_interaction + .after(input::process_player_input) + .before(movement::validate_movement), + interaction::process_terminal_interaction + .after(input::process_player_input) + .before(crate::perception::observer::compute_observer_snapshot), examine::process_examine_interaction .after(input::process_player_input) .before(crate::perception::observer::compute_observer_snapshot), diff --git a/server/src/simulation/save_io.rs b/server/src/simulation/save_io.rs index 895f4ac75..16519a59f 100644 --- a/server/src/simulation/save_io.rs +++ b/server/src/simulation/save_io.rs @@ -26,6 +26,8 @@ use crate::simulation::rng::SimRng; use crate::simulation::save_state::{ deserialize_npc_from_frozen, serialize_npc_to_frozen, SaveStateV1, SAVE_FORMAT_VERSION, }; +use crate::knowledge::types::StableId; +use crate::simulation::interaction::DoorState; use crate::simulation::tier::BackgroundSim; use crate::simulation::time::SimulationTime; @@ -130,6 +132,17 @@ pub fn save_to_file(path: &Path, world: &mut World) -> Result<(), SaveLoadError> npc_states, template_references, triangle_states, + open_doors: { + use crate::knowledge::registry::StableEntityId; + let mut q = world.query::<(&DoorState, &StableEntityId)>(); + let mut ids: Vec<_> = q + .iter(world) + .filter(|(ds, _)| ds.is_open) + .map(|(_, sid)| sid.0) + .collect(); + ids.sort_by_key(|id| id.0); + ids + }, }; let bytes = state @@ -230,6 +243,28 @@ pub fn load_from_file(path: &Path, world: &mut World) -> Result<(), SaveLoadErro } world.insert_resource(SimRng::new(state.seed)); + // Restore door open states (#246) — find door entities by StableId and toggle. + if !state.open_doors.is_empty() { + let open_set: std::collections::HashSet<_> = state.open_doors.iter().copied().collect(); + let door_entities: Vec<(Entity, StableId)> = { + let mut q = world.query::<(Entity, &crate::knowledge::registry::StableEntityId, &DoorState)>(); + q.iter(world) + .filter(|(_, sid, _)| open_set.contains(&sid.0)) + .map(|(e, sid, _)| (e, sid.0)) + .collect() + }; + for (entity, sid) in door_entities { + if let Some(mut door) = world.get_mut::(entity) { + door.is_open = true; + let tile = door.blocking_tile; + if let Some(mut wmap) = world.get_resource_mut::() { + wmap.set_walkable(&tile, true); + } + tracing::debug!(stable_id = sid.0, "load: restored open door state"); + } + } + } + // Update the player entity's KnowledgeGraph if a player exists. let player_entity = { let mut q = world.query_filtered::>(); @@ -522,6 +557,7 @@ mod tests { npc_states: vec![], template_references: TemplateReferenceMap::default(), triangle_states: vec![], + open_doors: vec![], }; let bytes = bad_state.to_bytes().expect("serialize"); let path = temp_path(); diff --git a/server/tests/information_boundaries.rs b/server/tests/information_boundaries.rs index 67e8f273b..2770f5721 100644 --- a/server/tests/information_boundaries.rs +++ b/server/tests/information_boundaries.rs @@ -205,6 +205,7 @@ fn save_state_npc_kg_isolation() { npc_states: vec![npc_a_state, npc_b_state], template_references: Default::default(), triangle_states: vec![], + open_doors: vec![], }; // Roundtrip: serialize → deserialize.