feat(simulation): #523 add zone_id to ObserverSnapshot (D-077, protocol v11)

Server-tracked zone_id on VisibleTile for D-073 zone crossfade and
D-059 deep fog temperature tint. ZoneMap resource backed by BTreeMap,
observer enrichment in snapshot assembly. Backwards-compatible: v10
payloads deserialize with zone_id: None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 18:10:06 +01:00
co-authored by Claude Opus 4.6
parent c2de509c01
commit 51abc3e3c1
20 changed files with 213 additions and 6 deletions
+3
View File
@@ -188,6 +188,7 @@ fn generate_msgpack_fixtures() {
z: 0,
visibility: VisibilitySector::Forward,
tile_kind: TileKind::Floor,
zone_id: Some(1),
},
VisibleTile {
x: 11,
@@ -195,6 +196,7 @@ fn generate_msgpack_fixtures() {
z: 0,
visibility: VisibilitySector::Peripheral,
tile_kind: TileKind::Floor,
zone_id: Some(1),
},
VisibleTile {
x: 10,
@@ -202,6 +204,7 @@ fn generate_msgpack_fixtures() {
z: 0,
visibility: VisibilitySector::Forward,
tile_kind: TileKind::Floor,
zone_id: None,
},
],
nearby_interactions: vec![],
+81 -1
View File
@@ -241,6 +241,7 @@ fn snapshot_v2_fields_roundtrip() {
z: 0,
visibility: VisibilitySector::Forward,
tile_kind: TileKind::Floor,
zone_id: None,
},
VisibleTile {
x: 6,
@@ -248,6 +249,7 @@ fn snapshot_v2_fields_roundtrip() {
z: 0,
visibility: VisibilitySector::Peripheral,
tile_kind: TileKind::Wall,
zone_id: None,
},
],
nearby_interactions: vec![],
@@ -312,7 +314,7 @@ fn protocol_version_constant_matches_snapshot() {
let snapshot = test_snapshot(0, vec![]);
assert_eq!(snapshot.version, PROTOCOL_VERSION);
assert_eq!(
PROTOCOL_VERSION, 10,
PROTOCOL_VERSION, 11,
"bump this assertion when protocol version changes"
);
}
@@ -862,6 +864,7 @@ fn boundary_value_in_tile_position() {
z: 0,
visibility: VisibilitySector::Forward,
tile_kind: TileKind::Floor,
zone_id: None,
}];
let bytes = rmp_serde::to_vec_named(&snapshot)
.unwrap_or_else(|e| panic!("encode tile x/y={} failed: {}", tile_val, e));
@@ -1264,6 +1267,83 @@ fn v9_payload_deserializes_into_v10_struct() {
);
}
/// v10 payload (without zone_id on VisibleTile) deserializes into the v11 struct
/// via #[serde(default)]. Guards backwards compat during migration (#523, D-077).
#[test]
fn v10_payload_deserializes_into_v11_struct() {
// V10 VisibleTile: no zone_id field
#[derive(serde::Serialize)]
struct VisibleTileV10 {
x: i32,
y: i32,
z: i32,
visibility: VisibilitySector,
tile_kind: TileKind,
}
#[derive(serde::Serialize)]
struct ObserverSnapshotV10 {
version: u8,
tick: u64,
game_time: GameTime,
player_facing: FacingDirection,
player_stance: MovementStance,
player_inventory: Vec<InventoryItem>,
entities: Vec<VisibleEntity>,
visible_tiles: Vec<VisibleTileV10>,
nearby_interactions: Vec<NearbyInteraction>,
current_monologue: Option<MonologueEvent>,
pending_recognitions: Vec<PendingRecognitionWire>,
dialogue_response: Option<DialogueResponseEvent>,
blocked_entities: Vec<u64>,
scan_events: Vec<settled_reach_server::simulation::contraband::ScanEvent>,
sound_events: Vec<settled_reach_server::simulation::sound::SoundEvent>,
rng_seed: Option<u64>,
}
let v10 = ObserverSnapshotV10 {
version: 10,
tick: 300,
game_time: GameTime {
day: 0,
time_of_day: 0,
day_phase: DayPhase::Morning,
tick_rate: TickRate::Full,
},
player_facing: FacingDirection::North,
player_stance: MovementStance::Walk,
player_inventory: vec![],
entities: vec![],
visible_tiles: vec![VisibleTileV10 {
x: 5,
y: 10,
z: 0,
visibility: VisibilitySector::Forward,
tile_kind: TileKind::Floor,
}],
nearby_interactions: vec![],
current_monologue: None,
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: Some(42),
};
let bytes = rmp_serde::to_vec_named(&v10).expect("serialize v10");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes)
.expect("v10 payload should deserialize into v11 struct via serde(default)");
assert_eq!(decoded.version, 10, "version field preserved from v10");
assert_eq!(decoded.tick, 300);
assert_eq!(decoded.visible_tiles.len(), 1);
assert_eq!(
decoded.visible_tiles[0].zone_id, None,
"missing zone_id should default to None"
);
}
/// NearbyInteraction.object_type round-trips through MessagePack (#422).
/// Verifies object_type=Some(Container) survives the wire.
#[test]