diff --git a/server/src/main.rs b/server/src/main.rs index 3a4574b45..7182c49e9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -147,7 +147,7 @@ fn main() { let mut app = App::new(); settled_reach_server::tick_phases::TickPhase::configure(&mut app); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed }); app.add_plugins(BridgePlugin); app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin); app.add_plugins(settled_reach_server::npc::NpcPlugin); @@ -176,7 +176,8 @@ fn main() { app.insert_resource(BridgeResource::new(bridge)); app.insert_resource(HandshakeState::Complete); - // Override SimRng with the chosen seed (SimulationPlugin defaults to seed 0) + // SimulationPlugin { seed } already inserts SimRng with the correct seed. + // Re-insert here as a defensive override in case plugin ordering shifts. app.insert_resource(settled_reach_server::simulation::rng::SimRng::new(seed)); // Initialize empty line pool index (populated by generator pipeline in v0.2). @@ -359,7 +360,7 @@ fn dump_schedule_graph() { let mut app = App::new(); settled_reach_server::tick_phases::TickPhase::configure(&mut app); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin); app.add_plugins(settled_reach_server::npc::NpcPlugin); diff --git a/server/src/simulation/economy.rs b/server/src/simulation/economy.rs index 5cdad056a..928bf0134 100644 --- a/server/src/simulation/economy.rs +++ b/server/src/simulation/economy.rs @@ -358,9 +358,10 @@ pub fn try_load_economy(run_seed: u64) -> Option<(EconSimResource, EconStateReso match Simulation::load_auto(run_seed) { Ok(sim) => { tracing::info!( + econ_seed = run_seed, commodities = sim.economy().commodities.len(), active_nodes = sim.nodes.len(), - "Economy simulation loaded" + "Economy simulation loaded with seed" ); Some((EconSimResource::new(sim), EconStateResource::default())) } diff --git a/server/src/simulation/economy_plugin.rs b/server/src/simulation/economy_plugin.rs index 9076e864f..23ab02f5f 100644 --- a/server/src/simulation/economy_plugin.rs +++ b/server/src/simulation/economy_plugin.rs @@ -8,13 +8,17 @@ use bevy_ecs::schedule::IntoScheduleConfigs; use crate::tick_phases::TickPhase; -pub struct EconomyPlugin; +pub struct EconomyPlugin { + /// World seed from `StartupMessage` (#826). Passed to `try_load_economy` so the + /// tâtonnement simulation is seeded deterministically from the client's world seed. + pub seed: u64, +} impl Plugin for EconomyPlugin { fn build(&self, app: &mut App) { // Economy simulation (#821, D-031) — loaded once at startup, no-op when DB absent. - // Uses seed 0 for now; will be threaded through StartupMessage world seed (#826). - if let Some((econ_sim, econ_state)) = super::economy::try_load_economy(0) { + // Seed comes from StartupMessage.world_seed, threaded via SimulationPlugin (#826). + if let Some((econ_sim, econ_state)) = super::economy::try_load_economy(self.seed) { app.insert_resource(econ_sim).insert_resource(econ_state); } // EconQueryBuffer: always registered so EconStateQuery PlayerActions are accepted diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs index 0956ccd74..944e65e78 100644 --- a/server/src/simulation/mod.rs +++ b/server/src/simulation/mod.rs @@ -50,7 +50,12 @@ pub mod zone; /// Each sub-plugin owns its domain's systems, resources, and phase assignment. /// No system registration happens here — only sub-plugin composition and /// shared resources needed by multiple sub-plugins. -pub struct SimulationPlugin; +/// +/// `seed` is threaded through to [`economy_plugin::EconomyPlugin`] so the +/// economy simulation uses the world seed from `StartupMessage` (#826). +pub struct SimulationPlugin { + pub seed: u64, +} impl Plugin for SimulationPlugin { fn build(&self, app: &mut App) { @@ -59,7 +64,7 @@ impl Plugin for SimulationPlugin { // Shared resources needed by multiple sub-plugins. // Each sub-plugin inits its own domain-specific resources. - app.insert_resource(rng::SimRng::new(0)) + app.insert_resource(rng::SimRng::new(self.seed)) .init_resource::() .init_resource::() // Triangle escalation event queue (#250) — consumed by storyteller + npc plugins @@ -81,7 +86,7 @@ impl Plugin for SimulationPlugin { app.add_plugins(input_plugin::InputPlugin); app.add_plugins(movement_plugin::MovementPlugin); app.add_plugins(social_plugin::SocialPlugin); - app.add_plugins(economy_plugin::EconomyPlugin); + app.add_plugins(economy_plugin::EconomyPlugin { seed: self.seed }); // Background worker tick integration (#843 Part C) app.add_systems( diff --git a/server/src/test_world/mod.rs b/server/src/test_world/mod.rs index 9916aae9a..b70705d0a 100644 --- a/server/src/test_world/mod.rs +++ b/server/src/test_world/mod.rs @@ -695,7 +695,7 @@ mod tests { #[test] fn gauntlet_setup_creates_expected_entities() { let mut app = App::new(); - app.add_plugins(crate::simulation::SimulationPlugin); + app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 }); app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); @@ -718,7 +718,7 @@ mod tests { #[test] fn hub_center_is_walkable() { let mut app = App::new(); - app.add_plugins(crate::simulation::SimulationPlugin); + app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 }); app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); @@ -735,7 +735,7 @@ mod tests { #[test] fn occlusion_north_wall_blocks() { let mut app = App::new(); - app.add_plugins(crate::simulation::SimulationPlugin); + app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 }); app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); @@ -754,7 +754,7 @@ mod tests { #[test] fn corridor_connects_hub_to_occlusion() { let mut app = App::new(); - app.add_plugins(crate::simulation::SimulationPlugin); + app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 }); app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); @@ -771,7 +771,7 @@ mod tests { #[test] fn stable_id_ranges_match_spec() { let mut app = App::new(); - app.add_plugins(crate::simulation::SimulationPlugin); + app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 }); app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); diff --git a/server/tests/archetype_monologue.rs b/server/tests/archetype_monologue.rs index b5b1e6fe2..b1fb8669a 100644 --- a/server/tests/archetype_monologue.rs +++ b/server/tests/archetype_monologue.rs @@ -78,7 +78,7 @@ mod gauntlet_integration { /// Build a minimal Gauntlet app with the given archetype and run one tick. fn boot_gauntlet(archetype: CharacterArchetype) -> App { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); test_world::setup_gauntlet(&mut app, archetype); app.update(); app diff --git a/server/tests/cross_room_transitions.rs b/server/tests/cross_room_transitions.rs index c4b086a40..92cdd1f95 100644 --- a/server/tests/cross_room_transitions.rs +++ b/server/tests/cross_room_transitions.rs @@ -221,7 +221,7 @@ fn t3_pause_mid_corridor_discards_movement_and_resumes() { use settled_reach_server::simulation::SimulationPlugin; let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); // 200×200 walkability map covers the full gauntlet coordinate space. app.insert_resource(WalkabilityMap::new(200, 200, 1)); diff --git a/server/tests/determinism.rs b/server/tests/determinism.rs index 78320afb3..b8ad0285e 100644 --- a/server/tests/determinism.rs +++ b/server/tests/determinism.rs @@ -38,7 +38,7 @@ use settled_reach_server::simulation::SimulationPlugin; /// Snapshots are written to SnapshotBuffer for direct inspection. fn build_deterministic_app(seed: u64) -> App { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed }); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.add_plugins(NpcPlugin); diff --git a/server/tests/error_handling.rs b/server/tests/error_handling.rs index 96ebbecc2..7db6a3a55 100644 --- a/server/tests/error_handling.rs +++ b/server/tests/error_handling.rs @@ -46,7 +46,7 @@ fn malformed_input_produces_sim_error_and_server_continues() { let bridge = TcpBridge::accept_on(listener).expect("accept connection"); let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.init_resource::(); @@ -177,7 +177,7 @@ fn state_hash_populated_in_snapshot() { let bridge = TcpBridge::accept_on(listener).expect("accept connection"); let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.init_resource::(); diff --git a/server/tests/game_loop.rs b/server/tests/game_loop.rs index 8e10fcf81..61e903de2 100644 --- a/server/tests/game_loop.rs +++ b/server/tests/game_loop.rs @@ -28,7 +28,7 @@ fn player_moves_north_through_full_pipeline() { // Build app let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.init_resource::(); diff --git a/server/tests/gen_gauntlet_fixtures.rs b/server/tests/gen_gauntlet_fixtures.rs index beb59243f..aa6cb05d1 100644 --- a/server/tests/gen_gauntlet_fixtures.rs +++ b/server/tests/gen_gauntlet_fixtures.rs @@ -39,7 +39,7 @@ const FIXTURE_DIR: &str = "../tests/fixtures/gauntlet"; /// Identical to what the server runs in --test-mode. fn build_gauntlet(seed: u64) -> App { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.add_plugins(NpcPlugin); diff --git a/server/tests/golden_suite.rs b/server/tests/golden_suite.rs index ea4afba00..9b025f575 100644 --- a/server/tests/golden_suite.rs +++ b/server/tests/golden_suite.rs @@ -46,7 +46,7 @@ const NUM_TICKS: usize = 10; /// Mirrors the setup in determinism.rs / main.rs. fn build_app(seed: u64) -> App { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.add_plugins(NpcPlugin); diff --git a/server/tests/movement.rs b/server/tests/movement.rs index 27c11fe57..f6b0766d1 100644 --- a/server/tests/movement.rs +++ b/server/tests/movement.rs @@ -6,7 +6,7 @@ use settled_reach_server::simulation::SimulationPlugin; #[test] fn movement_validated_within_app() { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); // Insert a walkability map with one blocked tile let mut map = WalkabilityMap::new(10, 10, 1); @@ -56,7 +56,7 @@ fn movement_validated_within_app() { #[test] fn entity_collision_blocks_movement() { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.insert_resource(WalkabilityMap::new(10, 10, 1)); // Stationary entity at (5,4) diff --git a/server/tests/perf_bench.rs b/server/tests/perf_bench.rs index b0e440035..6cdfd0247 100644 --- a/server/tests/perf_bench.rs +++ b/server/tests/perf_bench.rs @@ -66,7 +66,7 @@ fn perf_tick_timing() { let bridge = TcpBridge::accept_on(listener).expect("accept connection"); let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); app.add_plugins(BridgePlugin); app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin); app.add_plugins(settled_reach_server::npc::NpcPlugin); diff --git a/server/tests/smoke.rs b/server/tests/smoke.rs index 9420d4216..96d32cbd6 100644 --- a/server/tests/smoke.rs +++ b/server/tests/smoke.rs @@ -7,7 +7,7 @@ use settled_reach_server::simulation::SimulationPlugin; #[test] fn world_boots_and_ticks() { let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); // Verify initial state let time = app.world().resource::(); diff --git a/server/tests/tell_escalation.rs b/server/tests/tell_escalation.rs index 1f41b93f6..a30f75555 100644 --- a/server/tests/tell_escalation.rs +++ b/server/tests/tell_escalation.rs @@ -112,7 +112,7 @@ fn build_storyteller_app() -> App { bridge::types::CharacterArchetype, simulation::SimulationPlugin, test_world, }; let mut app = App::new(); - app.add_plugins(SimulationPlugin); + app.add_plugins(SimulationPlugin { seed: 0 }); test_world::setup_gauntlet(&mut app, CharacterArchetype::default()); app }