diff --git a/server/src/simulation/save_io.rs b/server/src/simulation/save_io.rs index 47b155b57..0f8644cba 100644 --- a/server/src/simulation/save_io.rs +++ b/server/src/simulation/save_io.rs @@ -16,6 +16,7 @@ use thiserror::Error; use crate::bridge::types::SaveLoadResultWire; use crate::bridge::types::SnapshotBuffer; +use crate::content::template::TemplateReferenceMap; use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::registry::EntityRegistry; use crate::npc::Npc; @@ -105,6 +106,13 @@ pub fn save_to_file(path: &Path, world: &mut World) -> Result<(), SaveLoadError> npc_states.sort_by_key(|s| s.stable_id.0); let npc_count = npc_states.len(); + + // Capture TemplateReferenceMap if present — default to empty if not yet initialised. + let template_references = world + .get_resource::() + .cloned() + .unwrap_or_default(); + let state = SaveStateV1 { format_version: SAVE_FORMAT_VERSION, tick, @@ -113,6 +121,7 @@ pub fn save_to_file(path: &Path, world: &mut World) -> Result<(), SaveLoadError> player_knowledge, relationship_graph, npc_states, + template_references, }; let bytes = state @@ -200,6 +209,7 @@ pub fn load_from_file(path: &Path, world: &mut World) -> Result<(), SaveLoadErro // Restore simulation resources. world.insert_resource(state.relationship_graph); + world.insert_resource(state.template_references); { let mut t = world.resource_mut::(); t.tick = state.tick; @@ -487,6 +497,7 @@ mod tests { #[test] fn load_from_file_rejects_wrong_format_version() { + use crate::content::template::TemplateReferenceMap; // Craft a save with a wrong format_version let bad_state = SaveStateV1 { format_version: 0xFF, // deliberately wrong @@ -496,6 +507,7 @@ mod tests { player_knowledge: KnowledgeGraph::new(), relationship_graph: RelationshipGraph::new(), npc_states: vec![], + template_references: TemplateReferenceMap::default(), }; let bytes = bad_state.to_bytes().expect("serialize"); let path = temp_path(); diff --git a/server/src/simulation/save_state.rs b/server/src/simulation/save_state.rs index c9d8a2936..1a81f7a09 100644 --- a/server/src/simulation/save_state.rs +++ b/server/src/simulation/save_state.rs @@ -39,6 +39,7 @@ use bevy_ecs::entity::Entity; use bevy_ecs::world::World; use serde::{Deserialize, Serialize}; +use crate::content::template::{TemplateOwnership, TemplateReferenceMap}; use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::registry::StableEntityId; use crate::knowledge::types::StableId; @@ -83,6 +84,11 @@ pub struct SaveStateV1 { /// Per-NPC summary state for each simulated NPC. /// Order is deterministic (sorted by stable_id in ascending order). pub npc_states: Vec, + /// Cross-template reference links (#165). + /// Preserved across save/load so that tier-evicted templates retain their + /// relationship metadata even when their NPCs are not in Active tier. + #[serde(default)] + pub template_references: TemplateReferenceMap, } /// Per-NPC state snapshot for `SaveStateV1`. @@ -184,6 +190,12 @@ pub struct NpcSaveState { /// Job performance score — drifts over time, persist across tier transitions. #[serde(default)] pub job_performance: Option, + + /// Template ownership (#165): which template owns this NPC and which role it fills. + /// `None` for NPCs that predate the template system or were hand-authored without + /// template assignment. Preserved across tier transitions (D-025 single-ownership). + #[serde(default)] + pub template_ownership: Option, } impl SaveStateV1 { @@ -258,6 +270,7 @@ pub fn serialize_npc_to_frozen(entity: Entity, world: &World) -> NpcSaveState { combat_capability: world.get::(entity).cloned(), mood_state: world.get::(entity).cloned(), job_performance: world.get::(entity).cloned(), + template_ownership: world.get::(entity).cloned(), } } @@ -353,6 +366,10 @@ pub fn deserialize_npc_from_frozen(state: &NpcSaveState, world: &mut World) -> E if let Some(combat) = state.combat_capability.clone() { em.insert(combat); } + // Restore template ownership if present — never reassigned after initial spawn (D-025). + if let Some(ownership) = state.template_ownership.clone() { + em.insert(ownership); + } } entity @@ -381,6 +398,7 @@ mod tests { player_knowledge: KnowledgeGraph::new(), relationship_graph: RelationshipGraph::new(), npc_states: vec![], + template_references: TemplateReferenceMap::default(), } } @@ -449,6 +467,7 @@ mod tests { combat_capability: None, mood_state: None, job_performance: None, + template_ownership: None, }, NpcSaveState { stable_id: StableId(202), @@ -469,6 +488,7 @@ mod tests { combat_capability: None, mood_state: None, job_performance: None, + template_ownership: None, }, ]; @@ -577,6 +597,7 @@ mod tests { combat_capability: None, mood_state: None, job_performance: None, + template_ownership: None, }]; let bytes = state.to_bytes().expect("serialize"); @@ -734,6 +755,7 @@ mod tests { combat_capability: None, mood_state: None, job_performance: None, + template_ownership: None, }; let mut world = World::new(); @@ -771,6 +793,7 @@ mod tests { player_knowledge: KnowledgeGraph::new(), relationship_graph: RelationshipGraph::new(), npc_states: vec![frozen], + template_references: TemplateReferenceMap::default(), }; let bytes = save.to_bytes().expect("serialize");