- Add character_archetype to StartupMessage with serde default (Detective) - Bump PROTOCOL_VERSION to 19 - Add escalate_tells_on_activation() and expire_routine_deviations() systems - RoutineDeviation inserted on triangle NPCs with 300-tick TTL - Add TickerPool resource with deterministic SimRng rotation (200 ticks) - Emit current_ticker in ObserverSnapshot when player is in bar zone - Load ticker YAML from district content directories Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -318,6 +318,7 @@ mod tests {
|
||||
state_hash: None,
|
||||
sim_errors: vec![],
|
||||
debug_response: None,
|
||||
current_ticker: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +458,7 @@ mod tests {
|
||||
state_hash: None,
|
||||
sim_errors: vec![],
|
||||
debug_response: None,
|
||||
current_ticker: None,
|
||||
};
|
||||
let text = format_snapshot_text(&snap);
|
||||
assert!(text.contains("Tick 0"));
|
||||
|
||||
@@ -17,7 +17,7 @@ pub use crate::simulation::time::{DayPhase, TickRate};
|
||||
/// negotiation is unnecessary. Client should reject snapshots with version !=
|
||||
/// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration
|
||||
/// period, then the default is removed once both sides are updated.
|
||||
pub const PROTOCOL_VERSION: u8 = 18;
|
||||
pub const PROTOCOL_VERSION: u8 = 19;
|
||||
|
||||
/// Handshake message sent as the very first framed message after connection (#555).
|
||||
/// Client reads this before entering the normal tick loop and validates
|
||||
@@ -46,6 +46,12 @@ pub struct StartupMessage {
|
||||
/// Generated by SessionManager.new_game() on the client.
|
||||
/// Same seed → same EntanglementConfig → same NPC population (D-029).
|
||||
pub world_seed: u64,
|
||||
/// Character archetype selected by the player (#587).
|
||||
/// Gates monologue pool selection, verb labels, and examine text.
|
||||
/// Defaults to Detective for backward compatibility (old clients
|
||||
/// that omit this field).
|
||||
#[serde(default)]
|
||||
pub character_archetype: CharacterArchetype,
|
||||
}
|
||||
|
||||
/// The ONLY data structure crossing the client-server boundary (D-020)
|
||||
@@ -73,10 +79,11 @@ pub struct StartupMessage {
|
||||
/// v17 adds: state_hash (#85, desync detection — fast hash of player pos + NPC count + tick),
|
||||
/// sim_errors (#85, structured error reporting to client).
|
||||
/// v18 adds: debug_response (#580, debug console server — command/response wire).
|
||||
/// v19 adds: character_archetype on StartupMessage (#587), current_ticker (#591).
|
||||
/// Future fields: ambient sound events, HUD state (D-020 expansion).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObserverSnapshot {
|
||||
/// Protocol version for forward compatibility. Current: 18.
|
||||
/// Protocol version for forward compatibility. Current: 19.
|
||||
pub version: u8,
|
||||
/// Simulation tick when this snapshot was produced
|
||||
pub tick: u64,
|
||||
@@ -197,6 +204,26 @@ pub struct ObserverSnapshot {
|
||||
/// the tilde console overlay. None in normal gameplay.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub debug_response: Option<DebugResponsePayload>,
|
||||
/// Current news ticker headline (#591).
|
||||
/// Populated only when player is in The Last Shift zone.
|
||||
/// Rotates every TICKER_ROTATION_TICKS ticks (deterministic via SimRng).
|
||||
/// None when player is outside the bar or no ticker content is loaded.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_ticker: Option<TickerLine>,
|
||||
}
|
||||
|
||||
/// A single news ticker headline crossing the wire boundary (#591).
|
||||
///
|
||||
/// Populated from `ticker/the-last-shift.yaml`. The `dual_lens` field
|
||||
/// in the source YAML is authoring metadata only — it is NOT included here.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TickerLine {
|
||||
/// Stable identifier for this headline (e.g. "ticker_001").
|
||||
pub id: String,
|
||||
/// The headline text displayed in the HUD ticker.
|
||||
pub text: String,
|
||||
/// Thematic category (freight, politics, infrastructure, sports, etc.).
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
/// Game time data for client display (D-031)
|
||||
@@ -421,6 +448,16 @@ pub enum CharacterArchetype {
|
||||
Detective,
|
||||
}
|
||||
|
||||
impl CharacterArchetype {
|
||||
/// String key for monologue pool filtering (#587).
|
||||
pub fn as_monologue_key(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Smuggler => "smuggler",
|
||||
Self::Detective => "detective",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Semantic player actions, not raw key events (D-020)
|
||||
/// Timestamped for deterministic processing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -906,16 +943,50 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn startup_message_roundtrip() {
|
||||
let msg = StartupMessage { world_seed: 0xDEADBEEF };
|
||||
let msg = StartupMessage {
|
||||
world_seed: 0xDEADBEEF,
|
||||
character_archetype: CharacterArchetype::Detective,
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded, msg);
|
||||
assert_eq!(decoded.world_seed, 0xDEADBEEF);
|
||||
assert_eq!(decoded.character_archetype, CharacterArchetype::Detective);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_message_smuggler_roundtrip() {
|
||||
let msg = StartupMessage {
|
||||
world_seed: 42,
|
||||
character_archetype: CharacterArchetype::Smuggler,
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded, msg);
|
||||
assert_eq!(decoded.character_archetype, CharacterArchetype::Smuggler);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_message_missing_archetype_defaults_to_detective() {
|
||||
// Simulate an old client that sends only world_seed (no character_archetype).
|
||||
// serde(default) on StartupMessage.character_archetype should default to Detective.
|
||||
#[derive(Serialize)]
|
||||
struct OldStartupMessage {
|
||||
world_seed: u64,
|
||||
}
|
||||
let old = OldStartupMessage { world_seed: 99 };
|
||||
let bytes = rmp_serde::to_vec_named(&old).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded.world_seed, 99);
|
||||
assert_eq!(decoded.character_archetype, CharacterArchetype::Detective);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_message_zero_seed() {
|
||||
let msg = StartupMessage { world_seed: 0 };
|
||||
let msg = StartupMessage {
|
||||
world_seed: 0,
|
||||
character_archetype: CharacterArchetype::default(),
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded.world_seed, 0);
|
||||
@@ -923,7 +994,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn startup_message_max_seed() {
|
||||
let msg = StartupMessage { world_seed: u64::MAX };
|
||||
let msg = StartupMessage {
|
||||
world_seed: u64::MAX,
|
||||
character_archetype: CharacterArchetype::default(),
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded.world_seed, u64::MAX);
|
||||
|
||||
Reference in New Issue
Block a user