# Scene Serialization Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add JSON scene serialization, binary scene format, and registration-based arbitrary component serialization to voltex_ecs. **Architecture:** ComponentRegistry stores per-type serialize/deserialize functions. JSON and binary serializers use the registry to handle arbitrary components. Existing .vscn text format unchanged. **Tech Stack:** Pure Rust, no external dependencies. Mini JSON writer/parser within voltex_ecs (not reusing voltex_renderer's json_parser to avoid dependency inversion). --- ### Task 1: Mini JSON Writer + Parser for voltex_ecs **Files:** - Create: `crates/voltex_ecs/src/json.rs` - Modify: `crates/voltex_ecs/src/lib.rs` - [ ] **Step 1: Write tests for JSON writer** ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_write_null() { assert_eq!(json_write_null(), "null"); } #[test] fn test_write_number() { assert_eq!(json_write_f32(3.14), "3.14"); assert_eq!(json_write_f32(1.0), "1"); } #[test] fn test_write_string() { assert_eq!(json_write_string("hello"), "\"hello\""); assert_eq!(json_write_string("a\"b"), "\"a\\\"b\""); } #[test] fn test_write_array() { assert_eq!(json_write_array(&["1", "2", "3"]), "[1,2,3]"); } #[test] fn test_write_object() { let pairs = vec![("name", "\"test\""), ("value", "42")]; let result = json_write_object(&pairs); assert_eq!(result, r#"{"name":"test","value":42}"#); } #[test] fn test_parse_number() { match json_parse("42.5").unwrap() { JsonVal::Number(n) => assert!((n - 42.5).abs() < 1e-10), _ => panic!("expected number"), } } #[test] fn test_parse_string() { assert_eq!(json_parse("\"hello\"").unwrap(), JsonVal::Str("hello".into())); } #[test] fn test_parse_array() { match json_parse("[1,2,3]").unwrap() { JsonVal::Array(a) => assert_eq!(a.len(), 3), _ => panic!("expected array"), } } #[test] fn test_parse_object() { let val = json_parse(r#"{"x":1,"y":2}"#).unwrap(); assert!(matches!(val, JsonVal::Object(_))); } #[test] fn test_parse_null() { assert_eq!(json_parse("null").unwrap(), JsonVal::Null); } #[test] fn test_parse_nested() { let val = json_parse(r#"{"a":[1,2],"b":{"c":3}}"#).unwrap(); assert!(matches!(val, JsonVal::Object(_))); } } ``` - [ ] **Step 2: Implement JSON writer and parser** Implement in `crates/voltex_ecs/src/json.rs`: - `JsonVal` enum: Null, Bool(bool), Number(f64), Str(String), Array(Vec), Object(Vec<(String, JsonVal)>) - Writer helpers: `json_write_null`, `json_write_f32`, `json_write_string`, `json_write_array`, `json_write_object` - `json_parse(input: &str) -> Result` — minimal recursive descent parser - `JsonVal::get(key)`, `as_f64()`, `as_str()`, `as_array()`, `as_object()` accessors Register in lib.rs: `pub mod json;` - [ ] **Step 3: Run tests, commit** ```bash cargo test --package voltex_ecs -- json::tests -v git add crates/voltex_ecs/src/json.rs crates/voltex_ecs/src/lib.rs git commit -m "feat(ecs): add mini JSON writer and parser for scene serialization" ``` --- ### Task 2: ComponentRegistry **Files:** - Create: `crates/voltex_ecs/src/component_registry.rs` - Modify: `crates/voltex_ecs/src/lib.rs` - [ ] **Step 1: Write tests** ```rust #[cfg(test)] mod tests { use super::*; use crate::{World, Transform}; use voltex_math::Vec3; #[test] fn test_register_and_serialize() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); assert!(registry.find("transform").is_some()); assert!(registry.find("tag").is_some()); } #[test] fn test_serialize_transform() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); let mut world = World::new(); let e = world.spawn(); world.add(e, Transform::from_position(Vec3::new(1.0, 2.0, 3.0))); let entry = registry.find("transform").unwrap(); let data = (entry.serialize)(&world, e); assert!(data.is_some()); } #[test] fn test_roundtrip_transform() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); let mut world = World::new(); let e = world.spawn(); world.add(e, Transform::from_position(Vec3::new(1.0, 2.0, 3.0))); let entry = registry.find("transform").unwrap(); let data = (entry.serialize)(&world, e).unwrap(); let mut world2 = World::new(); let e2 = world2.spawn(); (entry.deserialize)(&mut world2, e2, &data).unwrap(); let t = world2.get::(e2).unwrap(); assert!((t.position.x - 1.0).abs() < 1e-6); } } ``` - [ ] **Step 2: Implement ComponentRegistry** ```rust // crates/voltex_ecs/src/component_registry.rs use crate::entity::Entity; use crate::world::World; pub type SerializeFn = fn(&World, Entity) -> Option>; pub type DeserializeFn = fn(&mut World, Entity, &[u8]) -> Result<(), String>; pub struct ComponentEntry { pub name: String, pub serialize: SerializeFn, pub deserialize: DeserializeFn, } pub struct ComponentRegistry { entries: Vec, } impl ComponentRegistry { pub fn new() -> Self { Self { entries: Vec::new() } } pub fn register(&mut self, name: &str, ser: SerializeFn, deser: DeserializeFn) { self.entries.push(ComponentEntry { name: name.to_string(), serialize: ser, deserialize: deser, }); } pub fn find(&self, name: &str) -> Option<&ComponentEntry> { self.entries.iter().find(|e| e.name == name) } pub fn entries(&self) -> &[ComponentEntry] { &self.entries } pub fn register_defaults(&mut self) { // Transform: serialize as 9 f32s (pos.xyz, rot.xyz, scale.xyz) self.register("transform", serialize_transform, deserialize_transform); // Tag: serialize as UTF-8 string bytes self.register("tag", serialize_tag, deserialize_tag); // Parent handled specially (entity reference) } } fn serialize_transform(world: &World, entity: Entity) -> Option> { let t = world.get::(entity)?; let mut data = Vec::with_capacity(36); for &v in &[t.position.x, t.position.y, t.position.z, t.rotation.x, t.rotation.y, t.rotation.z, t.scale.x, t.scale.y, t.scale.z] { data.extend_from_slice(&v.to_le_bytes()); } Some(data) } fn deserialize_transform(world: &mut World, entity: Entity, data: &[u8]) -> Result<(), String> { if data.len() < 36 { return Err("Transform data too short".into()); } let f = |off: usize| f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]); let t = crate::Transform { position: voltex_math::Vec3::new(f(0), f(4), f(8)), rotation: voltex_math::Vec3::new(f(12), f(16), f(20)), scale: voltex_math::Vec3::new(f(24), f(28), f(32)), }; world.add(entity, t); Ok(()) } fn serialize_tag(world: &World, entity: Entity) -> Option> { let tag = world.get::(entity)?; Some(tag.0.as_bytes().to_vec()) } fn deserialize_tag(world: &mut World, entity: Entity, data: &[u8]) -> Result<(), String> { let s = std::str::from_utf8(data).map_err(|_| "Invalid UTF-8 in tag")?; world.add(entity, crate::scene::Tag(s.to_string())); Ok(()) } ``` - [ ] **Step 3: Run tests, commit** ```bash cargo test --package voltex_ecs -- component_registry -v git add crates/voltex_ecs/src/component_registry.rs crates/voltex_ecs/src/lib.rs git commit -m "feat(ecs): add ComponentRegistry for arbitrary component serialization" ``` --- ### Task 3: JSON Scene Serialization **Files:** - Modify: `crates/voltex_ecs/src/scene.rs` - [ ] **Step 1: Write tests** ```rust #[test] fn test_json_roundtrip() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); let mut world = World::new(); let e = world.spawn(); world.add(e, Transform::from_position(Vec3::new(1.0, 2.0, 3.0))); world.add(e, Tag("player".into())); let json = serialize_scene_json(&world, ®istry); assert!(json.contains("\"version\":1")); assert!(json.contains("\"player\"")); let mut world2 = World::new(); let entities = deserialize_scene_json(&mut world2, &json, ®istry).unwrap(); assert_eq!(entities.len(), 1); let t = world2.get::(entities[0]).unwrap(); assert!((t.position.x - 1.0).abs() < 1e-4); } #[test] fn test_json_with_parent() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); let mut world = World::new(); let parent = world.spawn(); let child = world.spawn(); world.add(parent, Transform::new()); world.add(child, Transform::new()); add_child(&mut world, parent, child); let json = serialize_scene_json(&world, ®istry); let mut world2 = World::new(); let entities = deserialize_scene_json(&mut world2, &json, ®istry).unwrap(); assert!(world2.get::(entities[1]).is_some()); } ``` - [ ] **Step 2: Implement serialize_scene_json and deserialize_scene_json** Use the json module's writer/parser. Serialize components via registry. Format: `{"version":1,"entities":[{"parent":null_or_idx,"components":{"transform":"base64_data","tag":"base64_data",...}}]}` Use base64 encoding for component binary data in JSON (reuse decoder pattern or simple hex encoding). - [ ] **Step 3: Run tests, commit** --- ### Task 4: Binary Scene Format **Files:** - Create: `crates/voltex_ecs/src/binary_scene.rs` - Modify: `crates/voltex_ecs/src/lib.rs` - [ ] **Step 1: Write tests** ```rust #[test] fn test_binary_roundtrip() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); let mut world = World::new(); let e = world.spawn(); world.add(e, Transform::from_position(Vec3::new(5.0, 0.0, -1.0))); world.add(e, Tag("enemy".into())); let data = serialize_scene_binary(&world, ®istry); assert_eq!(&data[0..4], b"VSCN"); let mut world2 = World::new(); let entities = deserialize_scene_binary(&mut world2, &data, ®istry).unwrap(); assert_eq!(entities.len(), 1); let t = world2.get::(entities[0]).unwrap(); assert!((t.position.x - 5.0).abs() < 1e-6); } #[test] fn test_binary_with_hierarchy() { let mut registry = ComponentRegistry::new(); registry.register_defaults(); let mut world = World::new(); let a = world.spawn(); let b = world.spawn(); world.add(a, Transform::new()); world.add(b, Transform::new()); add_child(&mut world, a, b); let data = serialize_scene_binary(&world, ®istry); let mut world2 = World::new(); let entities = deserialize_scene_binary(&mut world2, &data, ®istry).unwrap(); assert!(world2.get::(entities[1]).is_some()); } #[test] fn test_binary_invalid_magic() { let data = vec![0u8; 20]; let mut world = World::new(); let registry = ComponentRegistry::new(); assert!(deserialize_scene_binary(&mut world, &data, ®istry).is_err()); } ``` - [ ] **Step 2: Implement binary format** Header: `VSCN` + version(1) u32 LE + entity_count u32 LE Per entity: parent_index i32 LE (-1 = no parent) + component_count u32 LE Per component: name_len u16 LE + name bytes + data_len u32 LE + data bytes - [ ] **Step 3: Run tests, commit** --- ### Task 5: Exports and Full Verification - [ ] **Step 1: Update lib.rs exports** ```rust pub mod json; pub mod component_registry; pub mod binary_scene; pub use component_registry::ComponentRegistry; pub use binary_scene::{serialize_scene_binary, deserialize_scene_binary}; // scene.rs already exports serialize_scene, deserialize_scene // Add: pub use scene::{serialize_scene_json, deserialize_scene_json}; ``` - [ ] **Step 2: Run full tests** ```bash cargo test --package voltex_ecs -v cargo build --workspace ``` - [ ] **Step 3: Commit** ```bash git commit -m "feat(ecs): complete JSON and binary scene serialization with component registry" ```