feat(replay): Add Godot engine replay extensions and enhance event handling for replay processing

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-05-19 21:24:52 -07:00
parent 39aa1f25e2
commit e55e8c5ec6
2 changed files with 199 additions and 0 deletions

View file

@ -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
}

View file

@ -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<TurnEvent>, usize) =
bincode::serde::decode_from_slice(&bytes, cfg).expect("decode");
assert_eq!(decoded, events);
}
}