refactor(simulation): replace ad-hoc system ordering with TickPhase pipeline (#843)
10-phase linear pipeline: PreInput → Input → Movement → Simulation → Economy → Storyteller → Snapshot → PostSnapshot → Knowledge → TickAdvance. Each system assigned to exactly one phase via .in_set(TickPhase::X). Cross-phase .after()/.before() eliminated — only intra-phase ordering remains. Prevents schedule cycles by construction. SimulationPlugin refactored into sub-plugins by domain: - InputPlugin (player actions, interactions, dialogue dispatch) - MovementPlugin (pathfinding, movement validation, spatial indexing) - SocialPlugin (conversations, sound, voice enrichment, follow state) - EconomyPlugin (tâtonnement tick, IPC query serving) - TimePlugin (chunk streaming, news ticker, tick advancement) All other plugins (NPC, Knowledge, Perception, Storyteller, Settings, Bridge) updated to use TickPhase assignments instead of cross-plugin ordering constraints. BridgePlugin trimmed to bridge I/O concerns only. Part A of #843. Parts B (multi-threaded executor) and C (background workers) follow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+44
-35
@@ -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::<SnapshotBuffer>()
|
||||
.init_resource::<ServerRunning>()
|
||||
.init_resource::<HandshakeState>()
|
||||
.init_resource::<SimErrorBuffer>()
|
||||
.init_resource::<debug::DebugCommandBuffer>()
|
||||
.init_resource::<DebugEnabled>()
|
||||
.init_resource::<crate::perception::query::VisibilityGeometry>()
|
||||
.init_resource::<crate::perception::query::ActivePerceptionMode>()
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<KnowledgeEventQueue>()
|
||||
.init_resource::<ContradictionDetectedQueue>()
|
||||
.init_resource::<EntityRegistry>()
|
||||
.init_resource::<ContentEntityRegistry>()
|
||||
.init_resource::<DecayThresholds>()
|
||||
// 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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+55
-60
@@ -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::<relationships::RelationshipGraph>()
|
||||
.init_resource::<relationships::TrustEventQueue>()
|
||||
.init_resource::<relationships::PropagationQueue>()
|
||||
@@ -41,73 +43,66 @@ impl Plugin for NpcPlugin {
|
||||
.init_resource::<routine::RoutineDeviationEventQueue>()
|
||||
.init_resource::<disclosure::DisclosureGlobalRateLimit>()
|
||||
.init_resource::<trait_modifiers::TraitModifierConfig>()
|
||||
// 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");
|
||||
|
||||
@@ -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::<interpretation::ObservationEventQueue>()
|
||||
.init_resource::<query::VisibilityGeometry>()
|
||||
.init_resource::<query::ActivePerceptionMode>()
|
||||
// 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");
|
||||
}
|
||||
|
||||
@@ -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::<SettingsCommandBuffer>().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");
|
||||
}
|
||||
|
||||
@@ -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::<super::economy::EconQueryBuffer>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
super::economy::tick_economy_simulation,
|
||||
super::economy::serve_econ_state_query
|
||||
.after(super::economy::tick_economy_simulation),
|
||||
)
|
||||
.in_set(TickPhase::Economy),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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::<super::input::InputQueue>()
|
||||
.init_resource::<super::interaction::TerminalInteractedQueue>()
|
||||
// Triangle resolution resources (#250)
|
||||
.init_resource::<super::triangle::ResolveTriangleQueue>()
|
||||
.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),
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
-140
@@ -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::<time::SimulationTime>()
|
||||
.insert_resource(rng::SimRng::new(0))
|
||||
.init_resource::<input::InputQueue>()
|
||||
// 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::<save_io::SaveLoadPending>()
|
||||
.init_resource::<crate::knowledge::EntityRegistry>()
|
||||
.init_resource::<sound::SoundEventQueue>()
|
||||
.init_resource::<spatial::NaiveSpatialIndex>()
|
||||
.init_resource::<chunk_streaming::ChunkLoadRadius>()
|
||||
.init_resource::<chunk_streaming::ChunkStreamingCadence>()
|
||||
.init_resource::<follow::FollowEndEventQueue>()
|
||||
.init_resource::<monologue::PostConversationQueue>()
|
||||
.init_resource::<poi_discovery::PoiDiscoveryEventQueue>()
|
||||
// Triangle escalation resources (#250)
|
||||
.init_resource::<crate::simulation::triangle::TriangleCrisisEventQueue>()
|
||||
.init_resource::<crate::simulation::triangle::ResolveTriangleQueue>()
|
||||
// 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::<triangle::TriangleCrisisEventQueue>()
|
||||
// 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::<crate::perception::query::VisibilityGeometry>()
|
||||
// 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::<crate::npc::relationships::RelationshipGraph>()
|
||||
.init_resource::<crate::knowledge::KnowledgeEventQueue>()
|
||||
.init_resource::<interaction::TerminalInteractedQueue>()
|
||||
// Zone crossing event queue (#512, D-077): detect when player moves between zones.
|
||||
.init_resource::<zone::ZoneCrossEventQueue>()
|
||||
.init_resource::<zone::PreviousPlayerZone>()
|
||||
.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::<crate::knowledge::KnowledgeEventQueue>();
|
||||
|
||||
// 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::<ticker::TickerPool>();
|
||||
|
||||
// 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::<economy::EconQueryBuffer>();
|
||||
// tick_economy_simulation + serve_econ_state_query use Option<ResMut<...>> — 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");
|
||||
|
||||
@@ -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::<super::spatial::NaiveSpatialIndex>()
|
||||
.init_resource::<super::zone::ZoneCrossEventQueue>()
|
||||
.init_resource::<super::zone::PreviousPlayerZone>()
|
||||
.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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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::<super::sound::SoundEventQueue>()
|
||||
.init_resource::<super::follow::FollowEndEventQueue>()
|
||||
.init_resource::<super::monologue::PostConversationQueue>()
|
||||
.init_resource::<super::poi_discovery::PoiDiscoveryEventQueue>()
|
||||
.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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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::<super::time::SimulationTime>()
|
||||
.init_resource::<super::chunk_streaming::ChunkLoadRadius>()
|
||||
.init_resource::<super::chunk_streaming::ChunkStreamingCadence>()
|
||||
.init_resource::<super::ticker::TickerPool>()
|
||||
.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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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::<ContaminationActive>()
|
||||
.init_resource::<ContaminationEventQueue>()
|
||||
.init_resource::<ActivationState>()
|
||||
.init_resource::<TriangleActivatedQueue>()
|
||||
.init_resource::<MovementHistoryBuffer>()
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user