khora_io/serialization/
service.rs1use super::*;
22use khora_core::scene::{SceneFile, SceneHeader, SerializationGoal};
23use khora_data::ecs::World;
24use std::collections::HashMap;
25
26pub const CURRENT_SCENE_VERSION: u32 = 1;
30
31#[derive(Debug)]
33pub enum SerializationServiceError {
34 StrategyNotFound,
36 InvalidHeader,
38 ProcessingError(String),
40}
41
42pub struct SerializationService {
47 strategies: HashMap<String, Box<dyn SerializationStrategy>>,
48}
49
50impl SerializationService {
51 pub fn new() -> Self {
53 let mut strategies: HashMap<String, Box<dyn SerializationStrategy>> = HashMap::new();
54
55 let definition_strategy = DefinitionSerializationStrategy::new();
56 strategies.insert(
57 definition_strategy.get_strategy_id().to_string(),
58 Box::new(definition_strategy),
59 );
60
61 let recipe_strategy = RecipeSerializationStrategy::new();
62 strategies.insert(
63 recipe_strategy.get_strategy_id().to_string(),
64 Box::new(recipe_strategy),
65 );
66
67 let archetype_strategy = ArchetypeSerializationStrategy::new();
68 strategies.insert(
69 archetype_strategy.get_strategy_id().to_string(),
70 Box::new(archetype_strategy),
71 );
72
73 let messagepack_strategy = MessagePackSerializationStrategy::new();
74 strategies.insert(
75 messagepack_strategy.get_strategy_id().to_string(),
76 Box::new(messagepack_strategy),
77 );
78
79 Self { strategies }
80 }
81
82 pub fn save_world(
84 &self,
85 world: &World,
86 goal: SerializationGoal,
87 ) -> Result<SceneFile, SerializationServiceError> {
88 let strategy_id = match goal {
89 SerializationGoal::HumanReadableDebug | SerializationGoal::LongTermStability => {
90 "KH_DEFINITION_RON_V1"
91 }
92 SerializationGoal::SmallestFileSize | SerializationGoal::EditorInterchange => {
93 "KH_RECIPE_V1"
94 }
95 SerializationGoal::FastestLoad => "KH_ARCHETYPE_V1",
96 SerializationGoal::PortableBinary => "KH_MESSAGEPACK_V1",
97 };
98
99 let strategy = self
100 .strategies
101 .get(strategy_id)
102 .ok_or(SerializationServiceError::StrategyNotFound)?;
103
104 let payload = strategy
105 .serialize(world)
106 .map_err(|e| SerializationServiceError::ProcessingError(e.to_string()))?;
107
108 let strategy_id_str = strategy.get_strategy_id();
109 let mut strategy_id_bytes = [0u8; 32];
110 strategy_id_bytes[..strategy_id_str.len()].copy_from_slice(strategy_id_str.as_bytes());
111
112 let header = SceneHeader {
113 magic_bytes: khora_core::scene::HEADER_MAGIC_BYTES,
114 format_version: CURRENT_SCENE_VERSION as u8,
115 strategy_id: strategy_id_bytes,
116 payload_length: payload.len() as u64,
117 };
118
119 Ok(SceneFile { header, payload })
120 }
121
122 pub fn load_world(
124 &self,
125 file: &SceneFile,
126 world: &mut World,
127 ) -> Result<(), SerializationServiceError> {
128 let strategy_id = str::from_utf8(&file.header.strategy_id)
129 .map_err(|_| SerializationServiceError::InvalidHeader)?
130 .trim_end_matches('\0');
131
132 let payload = migrate_payload(
137 file.payload.clone(),
138 file.header.format_version as u32,
139 CURRENT_SCENE_VERSION,
140 )
141 .map_err(|e| SerializationServiceError::ProcessingError(e.to_string()))?;
142
143 let strategy = self
144 .strategies
145 .get(strategy_id)
146 .ok_or(SerializationServiceError::StrategyNotFound)?;
147
148 strategy
149 .deserialize(&payload, world)
150 .map_err(|e| SerializationServiceError::ProcessingError(e.to_string()))
151 }
152}
153
154impl Default for SerializationService {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use khora_core::math::Vec3;
164 use khora_core::scene::SerializationGoal;
165 use khora_data::ecs::{GlobalTransform, Parent, Transform, Without, World};
166
167 #[test]
168 fn test_definition_serialization_round_trip() {
169 let mut source_world = World::new();
170
171 let root_transform = Transform {
172 translation: Vec3::new(10.0, 0.0, 0.0),
173 ..Default::default()
174 };
175 let _root_id = source_world.spawn((root_transform, GlobalTransform::identity()));
176
177 let service = SerializationService::new();
178 let scene_file = service
179 .save_world(&source_world, SerializationGoal::LongTermStability)
180 .unwrap();
181
182 let mut dest_world = World::new();
183 service.load_world(&scene_file, &mut dest_world).unwrap();
184
185 let mut root_query = dest_world.query::<(&Transform, Without<Parent>)>();
186 let (new_root_transform, _) = root_query.next().expect("Should be one root entity");
187 assert_eq!(*new_root_transform, root_transform);
188 }
189
190 #[test]
191 fn test_recipe_serialization_round_trip() {
192 let mut source_world = World::new();
193
194 let root_transform = Transform {
195 translation: Vec3::new(25.0, 0.0, 0.0),
196 ..Default::default()
197 };
198 source_world.spawn((root_transform, GlobalTransform::identity()));
199
200 let service = SerializationService::new();
201 let scene_file = service
202 .save_world(&source_world, SerializationGoal::EditorInterchange)
203 .unwrap();
204
205 let mut dest_world = World::new();
206 service.load_world(&scene_file, &mut dest_world).unwrap();
207
208 assert_eq!(
209 str::from_utf8(&scene_file.header.strategy_id)
210 .unwrap()
211 .trim_end_matches('\0'),
212 "KH_RECIPE_V1"
213 );
214
215 let mut root_query = dest_world.query::<(&Transform, Without<Parent>)>();
216 let (new_root_transform, _) = root_query.next().expect("Should be one root entity");
217 assert_eq!(*new_root_transform, root_transform);
218 }
219
220 #[test]
221 fn test_archetype_serialization_round_trip() {
222 let mut source_world = World::new();
223
224 let root_transform = Transform {
225 translation: Vec3::new(10.0, 0.0, 0.0),
226 ..Default::default()
227 };
228 let root_id = source_world.spawn((root_transform, GlobalTransform::identity()));
229
230 let service = SerializationService::new();
231 let scene_file = service
232 .save_world(&source_world, SerializationGoal::FastestLoad)
233 .unwrap();
234
235 let mut dest_world = World::new();
236 service.load_world(&scene_file, &mut dest_world).unwrap();
237
238 assert_eq!(
239 str::from_utf8(&scene_file.header.strategy_id)
240 .unwrap()
241 .trim_end_matches('\0'),
242 "KH_ARCHETYPE_V1"
243 );
244
245 assert!(
246 dest_world.get::<Transform>(root_id).is_some(),
247 "Root entity should exist with the same ID"
248 );
249 }
250}