khora_agents/serialization_agent/
mod.rs1use khora_core::scene::{SceneFile, SceneHeader, SerializationGoal};
22use khora_data::ecs::World;
23use khora_lanes::scene_lane::{
24 ArchetypeSerializationLane, DefinitionSerializationLane, RecipeSerializationLane,
25 SerializationStrategy,
26};
27use std::collections::HashMap;
28
29#[derive(Debug)]
31pub enum AgentError {
32 StrategyNotFound,
34 InvalidHeader,
36 ProcessingError(String),
38}
39
40pub struct SerializationAgent {
42 strategies: HashMap<String, Box<dyn SerializationStrategy>>,
44}
45
46impl SerializationAgent {
47 pub fn new() -> Self {
49 let mut strategies: HashMap<String, Box<dyn SerializationStrategy>> = HashMap::new();
50
51 let definition_lane = DefinitionSerializationLane::new();
53 strategies.insert(
54 definition_lane.get_strategy_id().to_string(),
55 Box::new(definition_lane),
56 );
57
58 let recipe_lane = RecipeSerializationLane::new();
59 strategies.insert(
60 recipe_lane.get_strategy_id().to_string(),
61 Box::new(recipe_lane),
62 );
63
64 let archetype_lane = ArchetypeSerializationLane::new();
65 strategies.insert(
66 archetype_lane.get_strategy_id().to_string(),
67 Box::new(archetype_lane),
68 );
69
70 Self { strategies }
71 }
72
73 pub fn save_world(
75 &self,
76 world: &World,
77 goal: SerializationGoal,
78 ) -> Result<SceneFile, AgentError> {
79 let strategy_id = match goal {
80 SerializationGoal::HumanReadableDebug | SerializationGoal::LongTermStability => {
81 "KH_DEFINITION_RON_V1"
82 }
83 SerializationGoal::SmallestFileSize | SerializationGoal::EditorInterchange => {
84 "KH_RECIPE_V1"
85 }
86 SerializationGoal::FastestLoad => "KH_ARCHETYPE_V1",
87 };
88
89 let strategy = self
90 .strategies
91 .get(strategy_id)
92 .ok_or(AgentError::StrategyNotFound)?;
93
94 let payload = strategy
95 .serialize(world)
96 .map_err(|e| AgentError::ProcessingError(e.to_string()))?;
97
98 let strategy_id_str = strategy.get_strategy_id();
99 let mut strategy_id_bytes = [0u8; 32];
100 strategy_id_bytes[..strategy_id_str.len()].copy_from_slice(strategy_id_str.as_bytes());
101
102 let header = SceneHeader {
103 magic_bytes: khora_core::scene::HEADER_MAGIC_BYTES,
104 format_version: 1,
105 strategy_id: strategy_id_bytes,
106 payload_length: payload.len() as u64,
107 };
108
109 Ok(SceneFile { header, payload })
110 }
111
112 pub fn load_world(&self, file: &SceneFile, world: &mut World) -> Result<(), AgentError> {
114 let strategy_id = str::from_utf8(&file.header.strategy_id)
116 .map_err(|_| AgentError::InvalidHeader)? .trim_end_matches('\0');
118
119 let strategy = self
120 .strategies
121 .get(strategy_id)
122 .ok_or(AgentError::StrategyNotFound)?;
123
124 strategy
125 .deserialize(&file.payload, world)
126 .map_err(|e| AgentError::ProcessingError(e.to_string()))
127 }
128}
129
130impl Default for SerializationAgent {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use khora_core::math::Vec3;
140 use khora_data::ecs::{GlobalTransform, Parent, Transform, Without, World};
141 use khora_lanes::scene_lane::transform_propagation_system;
142
143 #[test]
144 fn test_serialization_round_trip() {
145 let mut source_world = World::new();
148
149 let root_transform = Transform {
150 translation: Vec3::new(10.0, 0.0, 0.0),
151 ..Default::default()
152 };
153 let root_id = source_world.spawn((root_transform, GlobalTransform::identity()));
154
155 let child_transform = Transform {
156 translation: Vec3::new(0.0, 5.0, 0.0),
157 ..Default::default()
158 };
159 let child_id = source_world.spawn((
160 child_transform,
161 GlobalTransform::identity(),
162 Parent(root_id),
163 ));
164
165 transform_propagation_system(&mut source_world);
167 let expected_child_global = source_world.get::<GlobalTransform>(child_id).unwrap().0;
168
169 let agent = SerializationAgent::new();
172 let scene_file = agent
173 .save_world(&source_world, SerializationGoal::LongTermStability)
174 .unwrap();
175
176 let mut dest_world = World::new();
178 agent.load_world(&scene_file, &mut dest_world).unwrap();
179
180 transform_propagation_system(&mut dest_world);
182
183 let mut root_query = dest_world.query::<(&Transform, Without<Parent>)>();
188 let (new_root_transform, _) = root_query.next().expect("Should be one root entity");
189 assert_eq!(*new_root_transform, root_transform);
190
191 let mut child_query = dest_world.query::<(&Transform, &Parent, &GlobalTransform)>();
193 let (new_child_transform, _new_parent, new_child_global) =
194 child_query.next().expect("Should be one child entity");
195
196 assert_eq!(*new_child_transform, child_transform);
198 assert_eq!(new_child_global.0, expected_child_global);
200 }
201
202 #[test]
203 fn test_recipe_serialization_round_trip() {
204 let mut source_world = World::new();
206
207 let root_transform = Transform {
208 translation: Vec3::new(25.0, 0.0, 0.0),
209 ..Default::default()
210 };
211 source_world.spawn((root_transform, GlobalTransform::identity()));
212
213 let agent = SerializationAgent::new();
215 let scene_file = agent
217 .save_world(&source_world, SerializationGoal::EditorInterchange)
218 .unwrap();
219
220 let mut dest_world = World::new();
221 agent.load_world(&scene_file, &mut dest_world).unwrap();
222
223 assert_eq!(
226 str::from_utf8(&scene_file.header.strategy_id)
227 .unwrap()
228 .trim_end_matches('\0'),
229 "KH_RECIPE_V1"
230 );
231
232 let mut root_query = dest_world.query::<(&Transform, Without<Parent>)>();
234 let (new_root_transform, _) = root_query.next().expect("Should be one root entity");
235 assert_eq!(*new_root_transform, root_transform);
236 }
237
238 #[test]
239 fn test_archetype_serialization_round_trip() {
240 let mut source_world = World::new();
243
244 let root_transform = Transform {
245 translation: Vec3::new(10.0, 0.0, 0.0),
246 ..Default::default()
247 };
248 let root_id = source_world.spawn((root_transform, GlobalTransform::identity()));
249
250 let child_transform = Transform {
251 translation: Vec3::new(0.0, 5.0, 0.0),
252 ..Default::default()
253 };
254 let child_id = source_world.spawn((
255 child_transform,
256 GlobalTransform::identity(),
257 Parent(root_id),
258 ));
259
260 transform_propagation_system(&mut source_world);
262 let expected_child_global = source_world.get::<GlobalTransform>(child_id).unwrap().0;
263
264 let agent = SerializationAgent::new();
268 let scene_file = agent
269 .save_world(&source_world, SerializationGoal::FastestLoad)
270 .unwrap();
271
272 let mut dest_world = World::new();
275 agent.load_world(&scene_file, &mut dest_world).unwrap();
276
277 assert_eq!(
280 str::from_utf8(&scene_file.header.strategy_id)
281 .unwrap()
282 .trim_end_matches('\0'),
283 "KH_ARCHETYPE_V1"
284 );
285
286 let new_child_global = dest_world
290 .get::<GlobalTransform>(child_id)
291 .expect("Child entity should exist with the same ID")
292 .0;
293
294 assert_eq!(new_child_global, expected_child_global);
299
300 assert!(
302 dest_world.get::<Transform>(root_id).is_some(),
303 "Root entity should exist with the same ID"
304 );
305 }
306}