From 69133d5a4ee26b504e4fe0bdcbd95e3a4fdd2eb9 Mon Sep 17 00:00:00 2001 From: autocommit Date: Tue, 19 May 2026 22:46:08 -0700 Subject: [PATCH] =?UTF-8?q?feat(mc-player-api):=20=E2=9C=A8=20Introduce=20?= =?UTF-8?q?message=20types=20and=20dispatch=20logic=20for=20player=20API?= =?UTF-8?q?=20communication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/simulator/crates/mc-comms/src/lib.rs | 9 ++++++ .../mc-player-api/src/comms_dispatch.rs | 32 +++++++++++++++++-- .../crates/mc-player-api/src/dispatch.rs | 26 +++++++++++++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/simulator/crates/mc-comms/src/lib.rs b/src/simulator/crates/mc-comms/src/lib.rs index 446a5fb9..d66af697 100644 --- a/src/simulator/crates/mc-comms/src/lib.rs +++ b/src/simulator/crates/mc-comms/src/lib.rs @@ -81,6 +81,15 @@ pub struct CommsState { /// map each turn. #[serde(default)] pub vision_shares: BTreeMap, + /// Phase 6 — side-channel buffer for chronicle events emitted from + /// non-end-of-turn paths (e.g. `PlayerAction::NameSeatOfPower`). + /// Drained by `mc-player-api`'s end-of-turn pass into the replay + /// archive. Skipped by serde — never persists across saves; if a + /// save is taken mid-turn the events will be re-emitted on resume. + /// Carries serde-encoded `mc_replay::TurnEvent` strings to avoid a + /// reverse mc-comms → mc-replay dep. + #[serde(skip)] + pub pending_chronicle_json: Vec, } impl CommsState { diff --git a/src/simulator/crates/mc-player-api/src/comms_dispatch.rs b/src/simulator/crates/mc-player-api/src/comms_dispatch.rs index 0a18ad06..c585fcbf 100644 --- a/src/simulator/crates/mc-player-api/src/comms_dispatch.rs +++ b/src/simulator/crates/mc-player-api/src/comms_dispatch.rs @@ -59,6 +59,22 @@ const DEFAULT_COURIER_ERA_TIER: u8 = 2; pub fn dispatch_war_declaration(state: &mut GameState, sender: PlayerId, target: PlayerId) { let mut sink: Vec = Vec::new(); dispatch_war_declaration_with_events(state, sender, target, &mut sink); + // Phase 6 — flush dispatched-event into the side-channel so + // production action handlers (PlayerAction::DeclareWar via + // `apply_declare_war`) surface the event without threading a vec + // through `apply_action`'s wire-event return type. + flush_to_pending_chronicle(&mut state.comms, sink); +} + +/// Phase 6 — serialise a batch of `TurnEvent`s into +/// `CommsState.pending_chronicle_json`. Drained by +/// `run_end_of_turn_comms_passes` at the next end-of-turn pass. +fn flush_to_pending_chronicle(comms: &mut mc_comms::CommsState, events: Vec) { + for ev in events { + if let Ok(json) = serde_json::to_string(&ev) { + comms.pending_chronicle_json.push(json); + } + } } /// Like [`dispatch_war_declaration`] but appends an @@ -87,6 +103,7 @@ pub fn dispatch_war_declaration_with_events( pub fn dispatch_peace_proposal(state: &mut GameState, sender: PlayerId, target: PlayerId) { let mut sink: Vec = Vec::new(); dispatch_peace_proposal_with_events(state, sender, target, &mut sink); + flush_to_pending_chronicle(&mut state.comms, sink); } /// Event-emitting variant of [`dispatch_peace_proposal`]. @@ -120,14 +137,17 @@ pub fn dispatch_treaty_offer( if (sender as usize) >= state.players.len() || (target as usize) >= state.players.len() { return; } - dispatch_envelope( + let mut sink: Vec = Vec::new(); + dispatch_envelope_with_events( state, sender, target, Payload::TreatyOffer { agreement_kind: agreement_kind.to_string(), }, + &mut sink, ); + flush_to_pending_chronicle(&mut state.comms, sink); } /// Dispatch a treaty-accept envelope. The `agreement_kind` carries the @@ -145,14 +165,17 @@ pub fn dispatch_treaty_accept( if (sender as usize) >= state.players.len() || (target as usize) >= state.players.len() { return; } - dispatch_envelope( + let mut sink: Vec = Vec::new(); + dispatch_envelope_with_events( state, sender, target, Payload::TreatyAccept { agreement_kind: agreement_kind.to_string(), }, + &mut sink, ); + flush_to_pending_chronicle(&mut state.comms, sink); } /// Dispatch a treaty-decline envelope. @@ -168,14 +191,17 @@ pub fn dispatch_treaty_decline( if (sender as usize) >= state.players.len() || (target as usize) >= state.players.len() { return; } - dispatch_envelope( + let mut sink: Vec = Vec::new(); + dispatch_envelope_with_events( state, sender, target, Payload::TreatyDecline { agreement_kind: agreement_kind.to_string(), }, + &mut sink, ); + flush_to_pending_chronicle(&mut state.comms, sink); } /// Allocate + populate + stash a generic envelope. Shared by every diff --git a/src/simulator/crates/mc-player-api/src/dispatch.rs b/src/simulator/crates/mc-player-api/src/dispatch.rs index 3bd85e0b..82b09f87 100644 --- a/src/simulator/crates/mc-player-api/src/dispatch.rs +++ b/src/simulator/crates/mc-player-api/src/dispatch.rs @@ -314,9 +314,20 @@ fn apply_name_seat_of_power( let pos = player_state.city_positions[idx]; player_state.capital_position = Some(pos); let _ = mc_comms::blackout::end_blackout(&mut state.comms, player); - // No wire-event counterpart for `CapitalBlackoutEnded` yet, but the - // replay archive consumer reads it via the side-channel like the - // other Phase 3 events. + // Phase 6 — emit `CapitalBlackoutEnded` into the side-channel + // `pending_chronicle_json` buffer on `CommsState`. The next + // `run_end_of_turn_comms_passes` drains it into the replay + // archive. Stored as a JSON string so this crate (mc-player-api) + // doesn't need to thread a TurnEvent vector across the `apply_action` + // return type (which returns wire `Event`s, not chronicle ones). + let ended_event = mc_replay::TurnEvent::CapitalBlackoutEnded { + turn: state.turn, + player: mc_replay::ClanId(player as u32), + new_capital_city_id: mc_replay::CityName(city_id.to_string()), + }; + if let Ok(json) = serde_json::to_string(&ended_event) { + state.comms.pending_chronicle_json.push(json); + } Ok(Vec::new()) } @@ -441,6 +452,15 @@ pub(crate) fn run_end_of_turn_comms_passes( let mut events: Vec = Vec::new(); let turn_now = state.turn; + // 0. Drain the side-channel chronicle buffer (events emitted from + // non-end-of-turn paths such as `PlayerAction::NameSeatOfPower`). + let pending: Vec = std::mem::take(&mut state.comms.pending_chronicle_json); + for json in pending { + if let Ok(ev) = serde_json::from_str::(&json) { + events.push(ev); + } + } + // 1. Drive envelopes through delivery / interception. let _ = crate::comms_dispatch::step_comms_with_events(state, &mut events);