12 KiB
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
#[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:
JsonValenum: 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<JsonVal, String>— minimal recursive descent parserJsonVal::get(key),as_f64(),as_str(),as_array(),as_object()accessors
Register in lib.rs: pub mod json;
- Step 3: Run tests, commit
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
#[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::<Transform>(e2).unwrap();
assert!((t.position.x - 1.0).abs() < 1e-6);
}
}
- Step 2: Implement ComponentRegistry
// crates/voltex_ecs/src/component_registry.rs
use crate::entity::Entity;
use crate::world::World;
pub type SerializeFn = fn(&World, Entity) -> Option<Vec<u8>>;
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<ComponentEntry>,
}
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<Vec<u8>> {
let t = world.get::<crate::Transform>(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<Vec<u8>> {
let tag = world.get::<crate::scene::Tag>(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
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
#[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::<Transform>(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::<Parent>(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
#[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::<Transform>(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::<Parent>(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
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
cargo test --package voltex_ecs -v
cargo build --workspace
- Step 3: Commit
git commit -m "feat(ecs): complete JSON and binary scene serialization with component registry"