feat(simulation): Sprint 19 — 7 server systems

Protocol handshake (#555): HandshakeMessage as first IPC frame,
HandshakeState resource, forward-compatible input handling.

State serialization (#96): serialize_npc_to_frozen/deserialize with
full D-024 axis coverage (10 new optional fields on NpcSaveState).

Scope tags (#98): ScopeTagKind enum, ScopePinned marker, automatic
assignment from KnowledgeGraph and RelationshipGraph.

Timestamp eviction (#97): LastInteractionTick, SimSpacePressure,
BinaryHeap LRU eviction respecting ScopePinned entities.

Save/load (#553): save_to_file/load_from_file via MessagePack,
SaveGame/LoadGame IPC commands, SaveLoadResultWire on snapshot.

Test infrastructure (#200): Layer 3 integration test entry point,
three-layer architecture documented per D-030.

Information boundary tests (#272): 4 negative tests proving no
passive KG leakage, LOS fog holds, tier boundary holds, save
isolation per NPC.

1063 tests passing, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 12:13:03 +01:00
co-authored by Claude Opus 4.6
parent 0dd33690f7
commit 6ed8d11502
22 changed files with 2561 additions and 35 deletions
+32
View File
@@ -83,6 +83,38 @@ impl EntityRegistry {
self.next_id = target;
}
/// Register an entity with a specific pre-existing StableId (used during save/load).
///
/// Unlike `register`, this does NOT advance `next_id`. After bulk-registering
/// all restored entities, call `advance_past(max_stable_id)` so future `register()`
/// calls produce IDs that don't conflict with the restored set.
///
/// No-op if the entity is already mapped to the same `stable_id`.
/// Panics in debug builds if `stable_id` is already mapped to a different entity.
pub fn register_existing(&mut self, entity: Entity, stable_id: StableId) {
if let Some(&existing) = self.by_stable_id.get(&stable_id) {
debug_assert_eq!(
existing, entity,
"register_existing: StableId {:?} already mapped to a different entity",
stable_id
);
return;
}
self.by_stable_id.insert(stable_id, entity);
self.by_entity.insert(entity, stable_id);
}
/// Advance `next_id` past `id` so future `register()` calls don't conflict.
///
/// Unlike `reserve_up_to`, this never panics: if the counter is already past `id`,
/// this is a no-op. Use after `register_existing` bulk-load to position the counter.
pub fn advance_past(&mut self, id: u64) {
let target = id.saturating_add(1);
if target > self.next_id {
self.next_id = target;
}
}
/// Number of registered entities.
pub fn len(&self) -> usize {
self.by_entity.len()