From e55e8c5ec6079d2543b00893cb9b9267d37f279c Mon Sep 17 00:00:00 2001 From: autocommit Date: Tue, 19 May 2026 21:24:52 -0700 Subject: [PATCH] =?UTF-8?q?feat(replay):=20=E2=9C=A8=20Add=20Godot=20engin?= =?UTF-8?q?e=20replay=20extensions=20and=20enhance=20event=20handling=20fo?= =?UTF-8?q?r=20replay=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/simulator/api-gdext/src/replay.rs | 60 +++++++++ src/simulator/crates/mc-replay/src/event.rs | 139 ++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/src/simulator/api-gdext/src/replay.rs b/src/simulator/api-gdext/src/replay.rs index a6db16ff..4459f395 100644 --- a/src/simulator/api-gdext/src/replay.rs +++ b/src/simulator/api-gdext/src/replay.rs @@ -302,6 +302,66 @@ fn event_to_dict(evt: &TurnEvent) -> Dictionary { d.set("party_a", parties[0].0 as i64); d.set("party_b", parties[1].0 as i64); } + TurnEvent::EnvelopeDispatched { + turn, + envelope_id, + sender, + recipient, + payload_kind, + eta_turn, + } => { + d.set("kind", GString::from("EnvelopeDispatched")); + d.set("turn", *turn as i64); + d.set("envelope_id", *envelope_id as i64); + d.set("sender", sender.0 as i64); + d.set("recipient", recipient.0 as i64); + d.set("payload_kind", GString::from(payload_kind.as_str())); + d.set("eta_turn", *eta_turn as i64); + } + TurnEvent::EnvelopeDelivered { + turn, + envelope_id, + sender, + recipient, + payload_kind, + } => { + d.set("kind", GString::from("EnvelopeDelivered")); + d.set("turn", *turn as i64); + d.set("envelope_id", *envelope_id as i64); + d.set("sender", sender.0 as i64); + d.set("recipient", recipient.0 as i64); + d.set("payload_kind", GString::from(payload_kind.as_str())); + } + TurnEvent::EnvelopeIntercepted { + turn, + envelope_id, + sender, + recipient, + payload_kind, + reason, + } => { + d.set("kind", GString::from("EnvelopeIntercepted")); + d.set("turn", *turn as i64); + d.set("envelope_id", *envelope_id as i64); + d.set("sender", sender.0 as i64); + d.set("recipient", recipient.0 as i64); + d.set("payload_kind", GString::from(payload_kind.as_str())); + d.set("reason", GString::from(reason.as_str())); + } + TurnEvent::LinkSevered { turn, player_a, player_b, tile } => { + d.set("kind", GString::from("LinkSevered")); + d.set("turn", *turn as i64); + d.set("player_a", player_a.0 as i64); + d.set("player_b", player_b.0 as i64); + d.set("col", tile.q as i64); + d.set("row", tile.r as i64); + } + TurnEvent::LinkRestored { turn, player_a, player_b } => { + d.set("kind", GString::from("LinkRestored")); + d.set("turn", *turn as i64); + d.set("player_a", player_a.0 as i64); + d.set("player_b", player_b.0 as i64); + } } d } diff --git a/src/simulator/crates/mc-replay/src/event.rs b/src/simulator/crates/mc-replay/src/event.rs index fd9d6692..c3be5744 100644 --- a/src/simulator/crates/mc-replay/src/event.rs +++ b/src/simulator/crates/mc-replay/src/event.rs @@ -380,6 +380,80 @@ pub enum TurnEvent { /// The two parties to the share, in canonical (min, max) order. parties: [ClanId; 2], }, + /// Communications Phase 6 — envelope-flow: a new envelope was + /// dispatched. Fires from `mc_player_api::comms_dispatch:: + /// dispatch_envelope` at the moment of allocation. + EnvelopeDispatched { + /// Turn the event fired on. + turn: u32, + /// Stable envelope id. + envelope_id: u32, + /// Sender player slot. + sender: ClanId, + /// Recipient player slot. + recipient: ClanId, + /// Stable payload kind string. + payload_kind: String, + /// Turn the envelope is expected to arrive. + eta_turn: u32, + }, + /// Communications Phase 6 — envelope-flow: an envelope arrived + /// intact at its recipient and the payload's delivery effect + /// applied. Fires from `step_comms`. + EnvelopeDelivered { + /// Turn the event fired on. + turn: u32, + /// Stable envelope id. + envelope_id: u32, + /// Sender player slot. + sender: ClanId, + /// Recipient player slot. + recipient: ClanId, + /// Stable payload kind string. + payload_kind: String, + }, + /// Communications Phase 6 — envelope-flow: an envelope failed to + /// arrive (route severed, capital lost, courier killed). Distinct + /// from `EnvelopeTapped`, which is read-but-still-delivered. + EnvelopeIntercepted { + /// Turn the event fired on. + turn: u32, + /// Stable envelope id. + envelope_id: u32, + /// Sender player slot. + sender: ClanId, + /// Recipient player slot. + recipient: ClanId, + /// Stable payload kind string. + payload_kind: String, + /// Stable discard-reason string: `"path_severed"`, + /// `"capital_lost"`, `"courier_killed"`. + reason: String, + }, + /// Communications Phase 6 — link-flow: a courier link between + /// `player_a` and `player_b` was severed (pillage of a wire, + /// blackout, courier killed) at `tile`. + LinkSevered { + /// Turn the event fired on. + turn: u32, + /// First party (canonical min slot). + player_a: ClanId, + /// Second party (canonical max slot). + player_b: ClanId, + /// Hex where the severance occurred. + tile: TileCoord, + }, + /// Communications Phase 6 — link-flow: a previously-severed link + /// between `player_a` and `player_b` was restored (wire rebuilt, + /// new route established, blackout ended). + LinkRestored { + /// Turn the event fired on. + turn: u32, + /// First party (canonical min slot). + player_a: ClanId, + /// Second party (canonical max slot). + player_b: ClanId, + }, /// p2-48: the game has ended. Emitted at most once per game, at the tail /// of `TurnProcessor::step` when `end_conditions::evaluate_conditions` /// returns `Some`. @@ -445,6 +519,11 @@ impl TurnEvent { | Self::HeartbeatMissed { turn, .. } | Self::VisionShareCollapsed { turn, .. } | Self::VisionShareRestored { turn, .. } + | Self::EnvelopeDispatched { turn, .. } + | Self::EnvelopeDelivered { turn, .. } + | Self::EnvelopeIntercepted { turn, .. } + | Self::LinkSevered { turn, .. } + | Self::LinkRestored { turn, .. } | Self::GameOver { turn, .. } => turn, } } @@ -694,4 +773,64 @@ mod tests { bincode::serde::decode_from_slice(&bytes, cfg).expect("decode"); assert_eq!(decoded, events); } + + /// Communications Phase 6: verify the five envelope-flow + link-flow + /// event variants round-trip through serde (JSON + bincode) and + /// `turn()` returns the correct value for each. + #[test] + fn link_severed_restored_round_trip() { + let dispatched = TurnEvent::EnvelopeDispatched { + turn: 5, + envelope_id: 11, + sender: ClanId(0), + recipient: ClanId(1), + payload_kind: "war_declaration".into(), + eta_turn: 12, + }; + let delivered = TurnEvent::EnvelopeDelivered { + turn: 12, + envelope_id: 11, + sender: ClanId(0), + recipient: ClanId(1), + payload_kind: "war_declaration".into(), + }; + let intercepted = TurnEvent::EnvelopeIntercepted { + turn: 7, + envelope_id: 12, + sender: ClanId(0), + recipient: ClanId(2), + payload_kind: "treaty_offer".into(), + reason: "path_severed".into(), + }; + let severed = TurnEvent::LinkSevered { + turn: 7, + player_a: ClanId(0), + player_b: ClanId(1), + tile: TileCoord::new(3, 4), + }; + let restored = TurnEvent::LinkRestored { + turn: 14, + player_a: ClanId(0), + player_b: ClanId(1), + }; + + assert_eq!(dispatched.turn(), 5); + assert_eq!(delivered.turn(), 12); + assert_eq!(intercepted.turn(), 7); + assert_eq!(severed.turn(), 7); + assert_eq!(restored.turn(), 14); + + let events = vec![dispatched, delivered, intercepted, severed, restored]; + for ev in &events { + let json = serde_json::to_string(ev).expect("serialize"); + let back: TurnEvent = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(&back, ev); + } + + let cfg = bincode::config::standard(); + let bytes = bincode::serde::encode_to_vec(&events, cfg).expect("encode"); + let (decoded, _): (Vec, usize) = + bincode::serde::decode_from_slice(&bytes, cfg).expect("decode"); + assert_eq!(decoded, events); + } }