diff --git a/server/src/bridge/mod.rs b/server/src/bridge/mod.rs index c88912621..59373725f 100644 --- a/server/src/bridge/mod.rs +++ b/server/src/bridge/mod.rs @@ -213,31 +213,36 @@ pub struct BridgePlugin; impl Plugin for BridgePlugin { fn build(&self, app: &mut App) { + use crate::tick_phases::TickPhase; + app.init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .init_resource::() .init_resource::() + // Bridge I/O — PreInput (receive) and PostSnapshot (send) + .add_systems( + Update, + receive_bridge_inputs.in_set(TickPhase::PreInput), + ) + .add_systems( + Update, + send_bridge_snapshot.in_set(TickPhase::PostSnapshot), + ) + // Debug commands — Snapshot phase + .add_systems( + Update, + debug::handle_debug_commands + .in_set(TickPhase::Snapshot), + ) + // Monologue chain — Simulation phase, strict intra-phase sequence. + // trigger_event_monologue must run after conversations + sound (also Simulation). .add_systems( Update, ( - receive_bridge_inputs.before(crate::simulation::input::process_player_input), - debug::handle_debug_commands - .after(crate::simulation::input::process_player_input) - // Note: NOT ordered after tick_economy_simulation — that creates a - // schedule cycle (debug → observer → advance_tick → econ → debug). - // GetEconState reads from EconStateResource which may be one tick - // stale on economy-tick boundaries. Acceptable for debug tooling. - .before(crate::perception::observer::compute_observer_snapshot), - crate::perception::observer::compute_visibility_geometry - .after(crate::simulation::movement::validate_movement), - crate::simulation::interaction::compute_nearby_interactions - .after(crate::simulation::movement::validate_movement), - crate::simulation::monologue::trigger_monologue - .after(crate::simulation::movement::validate_movement), + crate::simulation::monologue::trigger_monologue, crate::simulation::monologue::trigger_recognition_monologue .after(crate::simulation::monologue::trigger_monologue) .after(crate::perception::anomaly::detect_anomalies), @@ -248,29 +253,33 @@ impl Plugin for BridgePlugin { .after(crate::simulation::sound::collect_sound_events) .after(crate::simulation::conversation::run_npc_conversations) .after(crate::simulation::dialogue::process_walk_away), - // Contradiction monologue fires from queue populated by prior tick's - // process_knowledge_events (which runs after the snapshot). crate::simulation::monologue::process_contradiction_monologue .after(crate::simulation::monologue::trigger_event_monologue), - crate::simulation::follow::update_follow_state - .after(crate::perception::observer::compute_visibility_geometry) - .after(crate::simulation::movement::validate_movement) - .before(crate::perception::observer::compute_observer_snapshot), - crate::perception::observer::compute_observer_snapshot - .after(crate::perception::observer::compute_visibility_geometry) - .after(crate::simulation::interaction::compute_nearby_interactions) - .after(crate::simulation::monologue::process_contradiction_monologue) - .after(crate::simulation::dialogue::process_talk_interaction) - .after(crate::simulation::dialogue::process_confrontation_response) - .after(crate::simulation::dialogue::process_dialogue_response) - .before(crate::simulation::time::advance_tick), - crate::perception::observation::emit_observation_events - .after(crate::perception::observer::compute_observer_snapshot) - .before(crate::simulation::time::advance_tick), - send_bridge_snapshot - .after(crate::perception::observer::compute_observer_snapshot), - ), + ) + .in_set(TickPhase::Simulation), + ) + // Observation systems — Simulation phase (reads positions, feeds snapshot) + .add_systems( + Update, + ( + crate::perception::observer::compute_visibility_geometry, + crate::simulation::interaction::compute_nearby_interactions, + ) + .in_set(TickPhase::Simulation), + ) + // Observer snapshot assembly — Snapshot phase + .add_systems( + Update, + crate::perception::observer::compute_observer_snapshot + .in_set(TickPhase::Snapshot), + ) + // Post-snapshot: emit observation events + .add_systems( + Update, + crate::perception::observation::emit_observation_events + .in_set(TickPhase::PostSnapshot), ); + tracing::debug!("BridgePlugin initialized"); } } diff --git a/server/src/knowledge/mod.rs b/server/src/knowledge/mod.rs index 711eb8f8c..6bd8a80fb 100644 --- a/server/src/knowledge/mod.rs +++ b/server/src/knowledge/mod.rs @@ -29,22 +29,23 @@ pub struct KnowledgePlugin; impl Plugin for KnowledgePlugin { fn build(&self, app: &mut App) { + use crate::tick_phases::TickPhase; + app.init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() + // Knowledge phase: process events and decay. + // One-tick lag from snapshot is intentional — contradiction monologue + // and relationship shifts read as natural reaction delay (~0.1s). .add_systems( Update, ( - // Runs after snapshot: contradiction monologue and relationship - // shifts from KnowledgeGranted events lag by one tick (~0.1s). - // Acceptable — the player perceives the contradiction on the - // next snapshot, which reads as a natural reaction delay. - events::process_knowledge_events - .after(crate::perception::observer::compute_observer_snapshot), + events::process_knowledge_events, events::decay_knowledge.after(events::process_knowledge_events), - ), + ) + .in_set(TickPhase::Knowledge), ); tracing::debug!("KnowledgePlugin initialized"); } diff --git a/server/src/lib.rs b/server/src/lib.rs index 88849a393..89fed76c5 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,6 +2,7 @@ // Rust/bevy_ecs simulation server for D-010 client-server architecture pub mod bridge; +pub mod tick_phases; pub mod cause_chain; pub mod knowledge; pub mod npc; diff --git a/server/src/main.rs b/server/src/main.rs index f60749df4..b13edfb53 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -146,6 +146,7 @@ fn main() { let seed = seed_flag.unwrap_or(if test_mode { 42 } else { startup.world_seed }); let mut app = App::new(); + settled_reach_server::tick_phases::TickPhase::configure(&mut app); app.add_plugins(SimulationPlugin); app.add_plugins(BridgePlugin); app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin); @@ -357,6 +358,7 @@ fn dump_schedule_graph() { use bevy_ecs::schedule::Schedules; let mut app = App::new(); + settled_reach_server::tick_phases::TickPhase::configure(&mut app); app.add_plugins(SimulationPlugin); app.add_plugins(BridgePlugin); app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin); diff --git a/server/src/npc/mod.rs b/server/src/npc/mod.rs index 212d5f206..3506430c6 100644 --- a/server/src/npc/mod.rs +++ b/server/src/npc/mod.rs @@ -31,6 +31,8 @@ pub struct NpcPlugin; impl Plugin for NpcPlugin { fn build(&self, app: &mut App) { + use crate::tick_phases::TickPhase; + app.init_resource::() .init_resource::() .init_resource::() @@ -41,73 +43,66 @@ impl Plugin for NpcPlugin { .init_resource::() .init_resource::() .init_resource::() + // PreInput: routine phase transition (before pathfinding in Movement) + .add_systems( + Update, + routine::check_phase_transition.in_set(TickPhase::PreInput), + ) + // Input: dialogue systems (consume player talk/walk-away/confrontation actions) .add_systems( Update, ( - routine::check_phase_transition - .before(crate::simulation::pathfinding::compute_paths), - mood::update_mood - .after(routine::check_phase_transition) - .before(crate::simulation::dialogue::process_talk_interaction), - relationships::update_trust - .after(crate::simulation::dialogue::process_talk_interaction) - .after(crate::simulation::dialogue::process_walk_away) - .after(crate::simulation::dialogue::process_confrontation_response) - .after(crate::simulation::dialogue::process_dialogue_response) - .before(crate::simulation::time::advance_tick), - relationships::propagate_social_actions - .after(relationships::update_trust) - .before(crate::simulation::time::advance_tick), - relationships::update_relationship_dynamics - .after(relationships::update_trust) - .before(crate::simulation::time::advance_tick), - routine::enter_activity - .after(crate::simulation::movement::validate_movement) - .before(crate::perception::observer::compute_observer_snapshot), + crate::simulation::dialogue::process_talk_interaction, + crate::simulation::dialogue::process_walk_away + .after(crate::simulation::dialogue::process_talk_interaction), + crate::simulation::dialogue::process_confrontation_response, + crate::simulation::dialogue::process_dialogue_response + .after(crate::simulation::dialogue::process_talk_interaction), + ) + .in_set(TickPhase::Input), + ) + // Simulation: NPC behavior — vision, awareness, mood, tolerance, routine, + // disclosure, background tick. Intra-phase ordering where needed. + .add_systems( + Update, + ( + vision::compute_npc_vision, + vision::emit_npc_vision_events + .after(vision::compute_npc_vision), + awareness::detect_player_awareness + .after(vision::compute_npc_vision), + mood::update_mood, tolerance::check_tolerance_threshold .after(mood::update_mood) - .before(crate::simulation::time::advance_tick), + .after(awareness::detect_player_awareness), + routine::enter_activity, routine::detect_routine_deviation - .after(routine::enter_activity) - .before(crate::perception::observer::compute_observer_snapshot), - tell_state::derive_tell_state - .after(mood::update_mood) - .after(routine::detect_routine_deviation) - .before(crate::perception::observer::compute_observer_snapshot), - background::background_tick - .after(crate::simulation::movement::validate_movement) - .before(crate::simulation::time::advance_tick), - disclosure::derive_disclosure_candidates - .before(crate::perception::observer::compute_observer_snapshot), + .after(routine::enter_activity), + background::background_tick, + disclosure::derive_disclosure_candidates, disclosure::process_unprompted_disclosure - .after(disclosure::derive_disclosure_candidates) - .before(crate::perception::observer::compute_observer_snapshot), - // NPC player-awareness (#244) - awareness::detect_player_awareness - .after(vision::compute_npc_vision) - .before(tolerance::check_tolerance_threshold), - // NPC vision system (#115, D-011) - vision::compute_npc_vision - .after(crate::simulation::movement::validate_movement) - .after(crate::simulation::tier::update_tier_markers) - .before(crate::perception::observer::compute_observer_snapshot), - vision::emit_npc_vision_events - .after(vision::compute_npc_vision) - .before(crate::knowledge::events::process_knowledge_events), - vision::degrade_npc_inferences - .after(vision::emit_npc_vision_events) - .before(crate::simulation::time::advance_tick), - crate::simulation::dialogue::process_talk_interaction - .after(crate::simulation::input::process_player_input), - crate::simulation::dialogue::process_walk_away - .after(crate::simulation::input::process_player_input) - .after(crate::simulation::dialogue::process_talk_interaction), - crate::simulation::dialogue::process_confrontation_response - .after(crate::simulation::input::process_player_input), - crate::simulation::dialogue::process_dialogue_response - .after(crate::simulation::input::process_player_input) - .after(crate::simulation::dialogue::process_talk_interaction), - ), + .after(disclosure::derive_disclosure_candidates), + ) + .in_set(TickPhase::Simulation), + ) + // Storyteller: tell state derivation (reads mood + routine deviation) + .add_systems( + Update, + tell_state::derive_tell_state + .in_set(TickPhase::Storyteller), + ) + // Knowledge: relationships (trust, propagation, dynamics), vision degradation + .add_systems( + Update, + ( + relationships::update_trust, + relationships::propagate_social_actions + .after(relationships::update_trust), + relationships::update_relationship_dynamics + .after(relationships::update_trust), + vision::degrade_npc_inferences, + ) + .in_set(TickPhase::Knowledge), ); tracing::debug!("NpcPlugin initialized"); diff --git a/server/src/perception/mod.rs b/server/src/perception/mod.rs index 722c88465..45af5ac68 100644 --- a/server/src/perception/mod.rs +++ b/server/src/perception/mod.rs @@ -20,21 +20,30 @@ pub struct PerceptionPlugin; impl Plugin for PerceptionPlugin { fn build(&self, app: &mut App) { + use crate::tick_phases::TickPhase; + app.init_resource::() .init_resource::() .init_resource::() + // Simulation: anomaly detection (feeds monologue recognition chain) .add_systems( Update, ( - anomaly::clear_anomaly_markers.before(anomaly::detect_anomalies), - anomaly::detect_anomalies.before(observation::emit_observation_events), - cognitive_delay::process_cognitive_delay - .after(observation::emit_observation_events) - .before(crate::knowledge::events::process_knowledge_events), - interpretation::generate_observation_events - .after(observation::emit_observation_events) - .before(crate::knowledge::events::process_knowledge_events), - ), + anomaly::clear_anomaly_markers, + anomaly::detect_anomalies + .after(anomaly::clear_anomaly_markers), + ) + .in_set(TickPhase::Simulation), + ) + // Knowledge: cognitive delay + observation interpretation + // (runs after PostSnapshot emit_observation_events, feeds process_knowledge_events) + .add_systems( + Update, + ( + cognitive_delay::process_cognitive_delay, + interpretation::generate_observation_events, + ) + .in_set(TickPhase::Knowledge), ); tracing::debug!("PerceptionPlugin initialized"); } diff --git a/server/src/settings/mod.rs b/server/src/settings/mod.rs index 59bbcf7a8..5de1784d4 100644 --- a/server/src/settings/mod.rs +++ b/server/src/settings/mod.rs @@ -192,11 +192,11 @@ pub struct SettingsPlugin; impl Plugin for SettingsPlugin { fn build(&self, app: &mut App) { + use crate::tick_phases::TickPhase; + app.init_resource::().add_systems( Update, - process_settings_commands - .after(crate::simulation::input::process_player_input) - .before(crate::perception::observer::compute_observer_snapshot), + process_settings_commands.in_set(TickPhase::Snapshot), ); tracing::debug!("SettingsPlugin initialized"); } diff --git a/server/src/simulation/economy_plugin.rs b/server/src/simulation/economy_plugin.rs new file mode 100644 index 000000000..9076e864f --- /dev/null +++ b/server/src/simulation/economy_plugin.rs @@ -0,0 +1,33 @@ +//! Economy phase plugin — tâtonnement simulation tick and IPC query serving. +//! +//! All systems run in [`TickPhase::Economy`]. Intra-phase ordering: +//! - tick_economy_simulation → serve_econ_state_query (query reads fresh signals) + +use bevy_app::prelude::*; +use bevy_ecs::schedule::IntoScheduleConfigs; + +use crate::tick_phases::TickPhase; + +pub struct EconomyPlugin; + +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) { + app.insert_resource(econ_sim).insert_resource(econ_state); + } + // EconQueryBuffer: always registered so EconStateQuery PlayerActions are accepted + // even when the economy DB is absent (queries just produce no response). + app.init_resource::() + .add_systems( + Update, + ( + super::economy::tick_economy_simulation, + super::economy::serve_econ_state_query + .after(super::economy::tick_economy_simulation), + ) + .in_set(TickPhase::Economy), + ); + } +} diff --git a/server/src/simulation/input_plugin.rs b/server/src/simulation/input_plugin.rs new file mode 100644 index 000000000..535a3d745 --- /dev/null +++ b/server/src/simulation/input_plugin.rs @@ -0,0 +1,36 @@ +//! Input phase plugin — processes player actions and interaction commands. +//! +//! All systems run in [`TickPhase::Input`]. Intra-phase ordering: +//! - `process_player_input` runs first (dispatches queued actions) +//! - Door/terminal/examine/triangle run after input (consume dispatched events) + +use bevy_app::prelude::*; +use bevy_ecs::schedule::IntoScheduleConfigs; + +use crate::tick_phases::TickPhase; + +pub struct InputPlugin; + +impl Plugin for InputPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + // Triangle resolution resources (#250) + .init_resource::() + .add_systems( + Update, + ( + super::input::process_player_input, + super::interaction::process_door_interaction + .after(super::input::process_player_input), + super::interaction::process_terminal_interaction + .after(super::input::process_player_input), + super::examine::process_examine_interaction + .after(super::input::process_player_input), + super::triangle::apply_resolve_triangle + .after(super::input::process_player_input), + ) + .in_set(TickPhase::Input), + ); + } +} diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs index 6f2edd14f..3a411c50d 100644 --- a/server/src/simulation/mod.rs +++ b/server/src/simulation/mod.rs @@ -5,6 +5,12 @@ use bevy_app::prelude::*; use bevy_ecs::schedule::IntoScheduleConfigs; pub mod chunk_streaming; +// Phase sub-plugins (#843) +pub mod economy_plugin; +pub mod input_plugin; +pub mod movement_plugin; +pub mod social_plugin; +pub mod time_plugin; pub mod contraband; pub mod conversation; pub mod dialogue; @@ -39,8 +45,11 @@ pub mod time; pub mod triangle; pub mod zone; -/// Core simulation plugin -/// Manages simulation time, RNG, input processing, and tier transitions +/// Core simulation plugin — shell that delegates to phase sub-plugins (#843). +/// +/// 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; impl Plugin for SimulationPlugin { @@ -48,151 +57,33 @@ impl Plugin for SimulationPlugin { // Tier marker components (D-026) — must register before behavior systems app.add_plugins(tier::TierPlugin); - // Initialize core simulation resources - app.init_resource::() - .insert_resource(rng::SimRng::new(0)) - .init_resource::() + // Shared resources needed by multiple sub-plugins. + // Each sub-plugin inits its own domain-specific resources. + app.insert_resource(rng::SimRng::new(0)) .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - // Triangle escalation resources (#250) - .init_resource::() - .init_resource::() - // discover_pois reads VisibilityGeometry (also populated by PerceptionPlugin). - // Init here so SimulationPlugin works standalone in tests without PerceptionPlugin. + // Triangle escalation event queue (#250) — consumed by storyteller + npc plugins + .init_resource::() + // Cross-plugin resource safety: init resources that may be read by other plugins + // before those plugins register. Prevents "resource not found" in standalone tests. .init_resource::() - // transfer_npc_knowledge reads RelationshipGraph (also init by NpcPlugin) and - // KnowledgeEventQueue (also init by KnowledgePlugin). - // Init here so SimulationPlugin works standalone in tests without those plugins. .init_resource::() - .init_resource::() - .init_resource::() - // Zone crossing event queue (#512, D-077): detect when player moves between zones. - .init_resource::() - .init_resource::() - .add_systems( - Update, - ( - input::process_player_input, - // execute_save_load is an exclusive system (takes &mut World). - // Must run after process_player_input (which queues the command) - // and before compute_observer_snapshot (which consumes the result). - save_io::execute_save_load - .after(input::process_player_input) - .before(crate::perception::observer::compute_observer_snapshot), - pathfinding::compute_paths.after(input::process_player_input), - path_follow::follow_paths.after(pathfinding::compute_paths), - movement::validate_movement.after(path_follow::follow_paths), - path_follow::cleanup_path_blocked.after(movement::validate_movement), - spatial::sync_spatial_index.after(movement::validate_movement), - listening::update_listening_focus.after(movement::validate_movement), - contraband::check_contraband_scan - .after(movement::validate_movement) - .before(crate::perception::observer::compute_observer_snapshot), - conversation::run_npc_conversations - .after(movement::validate_movement) - .before(sound::collect_sound_events), - npc_knowledge_transfer::transfer_npc_knowledge - .after(conversation::run_npc_conversations), - sound::collect_sound_events - .after(movement::validate_movement) - .before(crate::perception::observer::compute_observer_snapshot), - 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), - // Character pressure (#248) — reads NPC awareness + relationship graph - pressure::update_character_pressure - .after(crate::npc::awareness::detect_player_awareness) - .before(crate::perception::observer::compute_observer_snapshot), - // Triangle escalation (#250) — runs on game-minute boundaries (every 10 ticks) - crate::simulation::triangle::tick_triangle_escalation - .after(crate::npc::tolerance::check_tolerance_threshold) - .before(crate::perception::observer::compute_observer_snapshot), - // Triangle resolution (#250, D-089) — apply player resolve commands - crate::simulation::triangle::apply_resolve_triangle - .after(input::process_player_input) - .before(crate::perception::observer::compute_observer_snapshot), - time::advance_tick.after(path_follow::cleanup_path_blocked), - ), - ) - // Zone crossing detection (#512, D-077) — separate call to stay within - // Bevy's 20-element system tuple limit. - .add_systems( - Update, - zone::detect_zone_crossings.after(movement::validate_movement), - ) - // Chunk streaming (#578, D-012) — loads/unloads chunks around the player. - // Runs before input processing so chunks are available for the current tick. - .add_systems( - Update, - chunk_streaming::chunk_streaming.before(input::process_player_input), - ) - // News ticker rotation (#591) — deterministic via SimRng, before snapshot. - .add_systems( - Update, - ticker::tick_news_ticker - .before(crate::perception::observer::compute_observer_snapshot), - ); + .init_resource::(); - // Voice enrichment (D-138, Phase 3) — rewrite NPC text with voiced - // variants from cache before the observer snapshot is assembled. - // No-op when VoiceCacheResource is absent (voice pipeline disabled). + // Phase sub-plugins (#843) — each owns its domain + app.add_plugins(time_plugin::TimePlugin); + app.add_plugins(input_plugin::InputPlugin); + app.add_plugins(movement_plugin::MovementPlugin); + app.add_plugins(social_plugin::SocialPlugin); + app.add_plugins(economy_plugin::EconomyPlugin); + + // save_load is an exclusive system (takes &mut World). + // Exclusive systems in Bevy 0.18 cannot use .in_set() — use .before()/.after() + // to position it within the Snapshot phase window. app.add_systems( Update, - ( - crate::voice::integration::voice_enrich_dialogue_response - .after(crate::simulation::dialogue::process_talk_interaction) - .before(crate::perception::observer::compute_observer_snapshot), - crate::voice::integration::voice_enrich_conversation_events - .after(conversation::run_npc_conversations) - .before(crate::perception::observer::compute_observer_snapshot), - ), - ); - - // Initialize TickerPool with empty default; populated by ContentPlugin at Startup. - app.init_resource::(); - - // 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)) = economy::try_load_economy(0) { - app.insert_resource(econ_sim).insert_resource(econ_state); - } - // EconQueryBuffer: always registered so EconStateQuery PlayerActions are accepted - // even when the economy DB is absent (queries just produce no response). - app.init_resource::(); - // tick_economy_simulation + serve_econ_state_query use Option> — safe to - // register unconditionally. They no-op when EconSimResource / EconStateResource absent. - app.add_systems( - Update, - ( - economy::tick_economy_simulation - .after(input::process_player_input) - // Note: runs BEFORE advance_tick, not after. The economy checks - // time.tick which is the CURRENT tick (not yet advanced). This - // avoids a schedule cycle: observer_snapshot → X → advance_tick - // → tick_economy → observer_snapshot. Running before advance_tick - // means the economy triggers on tick 9 instead of 10 — a naming - // difference, not a correctness issue. - .before(crate::perception::observer::compute_observer_snapshot), - economy::serve_econ_state_query - .after(economy::tick_economy_simulation) - .before(crate::perception::observer::compute_observer_snapshot), - ), + save_io::execute_save_load + .before(crate::perception::observer::compute_observer_snapshot), ); tracing::debug!("SimulationPlugin initialized"); diff --git a/server/src/simulation/movement_plugin.rs b/server/src/simulation/movement_plugin.rs new file mode 100644 index 000000000..9b035ae50 --- /dev/null +++ b/server/src/simulation/movement_plugin.rs @@ -0,0 +1,41 @@ +//! Movement phase plugin — position resolution and spatial indexing. +//! +//! All systems run in [`TickPhase::Movement`]. Intra-phase ordering: +//! - pathfinding → follow_paths → validate_movement → cleanup_path_blocked +//! - After validate_movement: spatial index, listening focus, zone crossings, tier systems + +use bevy_app::prelude::*; +use bevy_ecs::schedule::IntoScheduleConfigs; + +use crate::tick_phases::TickPhase; + +pub struct MovementPlugin; + +impl Plugin for MovementPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Update, + ( + // Core movement chain (strict sequence) + super::pathfinding::compute_paths, + super::path_follow::follow_paths + .after(super::pathfinding::compute_paths), + super::movement::validate_movement + .after(super::path_follow::follow_paths), + super::path_follow::cleanup_path_blocked + .after(super::movement::validate_movement), + // Post-movement indexing (all after validate_movement, parallel with each other) + super::spatial::sync_spatial_index + .after(super::movement::validate_movement), + super::listening::update_listening_focus + .after(super::movement::validate_movement), + super::zone::detect_zone_crossings + .after(super::movement::validate_movement), + ) + .in_set(TickPhase::Movement), + ); + } +} diff --git a/server/src/simulation/social_plugin.rs b/server/src/simulation/social_plugin.rs new file mode 100644 index 000000000..f142c9c71 --- /dev/null +++ b/server/src/simulation/social_plugin.rs @@ -0,0 +1,47 @@ +//! Social simulation plugin — NPC conversations, knowledge transfer, disclosure. +//! +//! All systems run in [`TickPhase::Simulation`]. Intra-phase ordering: +//! - conversations → knowledge_transfer (transfer reads conversation results) +//! - conversations → sound collection (sound reads conversation events) + +use bevy_app::prelude::*; +use bevy_ecs::schedule::IntoScheduleConfigs; + +use crate::tick_phases::TickPhase; + +pub struct SocialPlugin; + +impl Plugin for SocialPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Update, + ( + super::conversation::run_npc_conversations, + super::npc_knowledge_transfer::transfer_npc_knowledge + .after(super::conversation::run_npc_conversations), + super::sound::collect_sound_events + .after(super::conversation::run_npc_conversations), + // Voice enrichment (D-138) — rewrite NPC text with voiced variants. + // No-op when VoiceCacheResource is absent. + crate::voice::integration::voice_enrich_dialogue_response, + crate::voice::integration::voice_enrich_conversation_events + .after(super::conversation::run_npc_conversations), + // POI discovery reads visibility geometry (also Simulation phase) + super::poi_discovery::discover_pois, + // Follow state reads visibility geometry + movement + super::follow::update_follow_state, + // Contraband scan reads movement + NPC awareness + super::contraband::check_contraband_scan, + // Pressure reads NPC awareness + relationship graph + super::pressure::update_character_pressure, + // Triangle escalation (#250) — runs on game-minute boundaries + super::triangle::tick_triangle_escalation, + ) + .in_set(TickPhase::Simulation), + ); + } +} diff --git a/server/src/simulation/time_plugin.rs b/server/src/simulation/time_plugin.rs new file mode 100644 index 000000000..1f1c76dfc --- /dev/null +++ b/server/src/simulation/time_plugin.rs @@ -0,0 +1,37 @@ +//! Time and streaming plugin — tick advancement, chunk streaming, news ticker. +//! +//! Systems span two phases: +//! - [`TickPhase::PreInput`]: chunk_streaming (loads chunks before input processing) +//! - [`TickPhase::TickAdvance`]: advance_tick (must be last in the pipeline) +//! - [`TickPhase::Snapshot`]: tick_news_ticker (assembles display content) + +use bevy_app::prelude::*; +use bevy_ecs::schedule::IntoScheduleConfigs; + +use crate::tick_phases::TickPhase; + +pub struct TimePlugin; + +impl Plugin for TimePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Update, + super::chunk_streaming::chunk_streaming + .in_set(TickPhase::PreInput), + ) + .add_systems( + Update, + super::ticker::tick_news_ticker + .in_set(TickPhase::Snapshot), + ) + .add_systems( + Update, + super::time::advance_tick + .in_set(TickPhase::TickAdvance), + ); + } +} diff --git a/server/src/storyteller/mod.rs b/server/src/storyteller/mod.rs index eb0a3e6a2..4553b2262 100644 --- a/server/src/storyteller/mod.rs +++ b/server/src/storyteller/mod.rs @@ -354,32 +354,28 @@ pub struct StorytellerPlugin; impl Plugin for StorytellerPlugin { fn build(&self, app: &mut App) { + use crate::tick_phases::TickPhase; + app.init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .add_systems(Update, tick_contamination_activation) + // Storyteller phase: contamination, activation, tell escalation. + // Intra-phase ordering: contamination + history → activation → escalation .add_systems( Update, - append_player_history.after(crate::simulation::movement::validate_movement), - ) - .add_systems( - Update, - activation_pass - .after(append_player_history) - .after(tick_contamination_activation) - .before(crate::simulation::time::advance_tick), - ) - .add_systems( - Update, - escalate_tells_on_activation - .after(activation_pass) - .before(crate::npc::tell_state::derive_tell_state), - ) - .add_systems( - Update, - expire_routine_deviations.before(crate::npc::tell_state::derive_tell_state), + ( + tick_contamination_activation, + append_player_history, + activation_pass + .after(append_player_history) + .after(tick_contamination_activation), + escalate_tells_on_activation + .after(activation_pass), + expire_routine_deviations, + ) + .in_set(TickPhase::Storyteller), ); tracing::debug!("StorytellerPlugin initialized"); @@ -510,7 +506,7 @@ pub fn activation_pass( .collect(); if simmering.is_empty() { - tracing::warn!("activation_pass: no Simmering triangles — holding"); + // Normal state — no triangles ready for activation yet. Silent return. return; } diff --git a/server/src/tick_phases.rs b/server/src/tick_phases.rs new file mode 100644 index 000000000..3cccddc97 --- /dev/null +++ b/server/src/tick_phases.rs @@ -0,0 +1,71 @@ +//! Server tick cycle — named system set phases (#843). +//! +//! Replaces ad-hoc `.after()`/`.before()` constraints with a linear pipeline. +//! Each system belongs to exactly one phase. Phases execute sequentially. +//! Within a phase, systems with no intra-phase ordering run in parallel +//! (when the multi-threaded executor is enabled). +//! +//! ```text +//! PreInput → Input → Movement → Simulation → Economy +//! → Storyteller → Snapshot → PostSnapshot → Knowledge → TickAdvance +//! ``` +//! +//! ## Rules +//! +//! 1. Every system in the Update schedule MUST have `.in_set(TickPhase::X)`. +//! 2. Cross-phase `.after()`/`.before()` is FORBIDDEN — if you need it, +//! the system is in the wrong phase. +//! 3. Intra-phase `.after()`/`.before()` is allowed for systems within +//! the same phase that have a real data dependency. + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_ecs::schedule::IntoScheduleConfigs; + +/// The 10 phases of the server tick cycle. +/// +/// Ordered linearly: each phase completes before the next begins. +/// Configure once in the App with `configure_sets(Update, ...)`. +#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TickPhase { + /// Setup before player input: chunk streaming, bridge input, NPC routine transitions. + PreInput, + /// Process player actions: input dispatch, door/terminal/examine interactions, dialogue. + Input, + /// Position resolution: pathfinding, path following, movement validation, spatial indexing. + Movement, + /// Game logic: NPC behavior, vision, conversations, monologue, sound, visibility geometry. + Simulation, + /// Economics: tick the economy simulation, serve economy state queries. + Economy, + /// Storyteller: contamination, activation, tell escalation, tell state derivation. + Storyteller, + /// Assemble tick output: observer snapshot, ticker, settings, debug commands, save/load. + Snapshot, + /// React to snapshot: observation events, send to client, anomaly detection. + PostSnapshot, + /// Update persistent state: knowledge events, cognitive delay, relationships. + Knowledge, + /// Increment the clock. Must be last. + TickAdvance, +} + +impl TickPhase { + /// Configure the linear phase ordering on the app. + /// + /// Call once during plugin setup: + /// ```ignore + /// TickPhase::configure(&mut app); + /// ``` + pub fn configure(app: &mut bevy_app::App) { + use TickPhase::*; + app.configure_sets( + Update, + ( + PreInput, Input, Movement, Simulation, Economy, Storyteller, Snapshot, + PostSnapshot, Knowledge, TickAdvance, + ) + .chain(), + ); + } +}