diff --git a/crates/voltex_ecs/src/binary_scene.rs b/crates/voltex_ecs/src/binary_scene.rs new file mode 100644 index 0000000..62b5b19 --- /dev/null +++ b/crates/voltex_ecs/src/binary_scene.rs @@ -0,0 +1,263 @@ +/// Binary scene format (.vscn binary). +/// +/// Format: +/// Header: "VSCN" (4 bytes) + version 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 + +use std::collections::HashMap; +use crate::entity::Entity; +use crate::world::World; +use crate::transform::Transform; +use crate::hierarchy::{add_child, Parent}; +use crate::component_registry::ComponentRegistry; + +const MAGIC: &[u8; 4] = b"VSCN"; +const VERSION: u32 = 1; + +/// Serialize all entities with a Transform to the binary scene format. +pub fn serialize_scene_binary(world: &World, registry: &ComponentRegistry) -> Vec { + let entities_with_transform: Vec<(Entity, Transform)> = world + .query::() + .map(|(e, t)| (e, *t)) + .collect(); + + let entity_to_index: HashMap = entities_with_transform + .iter() + .enumerate() + .map(|(i, (e, _))| (*e, i)) + .collect(); + + let entity_count = entities_with_transform.len() as u32; + let mut buf = Vec::new(); + + // Header + buf.extend_from_slice(MAGIC); + buf.extend_from_slice(&VERSION.to_le_bytes()); + buf.extend_from_slice(&entity_count.to_le_bytes()); + + // Entities + for (entity, _) in &entities_with_transform { + // Parent index + let parent_idx: i32 = if let Some(parent_comp) = world.get::(*entity) { + entity_to_index.get(&parent_comp.0) + .map(|&i| i as i32) + .unwrap_or(-1) + } else { + -1 + }; + buf.extend_from_slice(&parent_idx.to_le_bytes()); + + // Collect serializable components + let mut comp_data: Vec<(&str, Vec)> = Vec::new(); + for entry in registry.entries() { + if let Some(data) = (entry.serialize)(world, *entity) { + comp_data.push((&entry.name, data)); + } + } + + let comp_count = comp_data.len() as u32; + buf.extend_from_slice(&comp_count.to_le_bytes()); + + for (name, data) in &comp_data { + let name_bytes = name.as_bytes(); + buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes()); + buf.extend_from_slice(name_bytes); + buf.extend_from_slice(&(data.len() as u32).to_le_bytes()); + buf.extend_from_slice(data); + } + } + + buf +} + +/// Deserialize entities from binary scene data. +pub fn deserialize_scene_binary( + world: &mut World, + data: &[u8], + registry: &ComponentRegistry, +) -> Result, String> { + if data.len() < 12 { + return Err("Binary scene data too short".into()); + } + + // Verify magic + if &data[0..4] != MAGIC { + return Err("Invalid magic bytes — expected VSCN".into()); + } + + let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + if version != 1 { + return Err(format!("Unsupported binary scene version: {}", version)); + } + + let entity_count = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let mut pos = 12; + + let mut created: Vec = Vec::with_capacity(entity_count); + let mut parent_indices: Vec = Vec::with_capacity(entity_count); + + for _ in 0..entity_count { + // Parent index + if pos + 4 > data.len() { + return Err("Unexpected end of data reading parent index".into()); + } + let parent_idx = i32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]); + pos += 4; + + // Component count + if pos + 4 > data.len() { + return Err("Unexpected end of data reading component count".into()); + } + let comp_count = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize; + pos += 4; + + let entity = world.spawn(); + + for _ in 0..comp_count { + // Name length + if pos + 2 > data.len() { + return Err("Unexpected end of data reading name length".into()); + } + let name_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize; + pos += 2; + + // Name + if pos + name_len > data.len() { + return Err("Unexpected end of data reading component name".into()); + } + let name = std::str::from_utf8(&data[pos..pos + name_len]) + .map_err(|_| "Invalid UTF-8 in component name".to_string())?; + pos += name_len; + + // Data length + if pos + 4 > data.len() { + return Err("Unexpected end of data reading data length".into()); + } + let data_len = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize; + pos += 4; + + // Data + if pos + data_len > data.len() { + return Err("Unexpected end of data reading component data".into()); + } + let comp_data = &data[pos..pos + data_len]; + pos += data_len; + + // Deserialize via registry + if let Some(entry) = registry.find(name) { + (entry.deserialize)(world, entity, comp_data)?; + } + } + + created.push(entity); + parent_indices.push(parent_idx); + } + + // Apply parent relationships + for (child_idx, &parent_idx) in parent_indices.iter().enumerate() { + if parent_idx >= 0 { + let pi = parent_idx as usize; + if pi < created.len() { + let child_entity = created[child_idx]; + let parent_entity = created[pi]; + add_child(world, parent_entity, child_entity); + } + } + } + + Ok(created) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scene::Tag; + use voltex_math::Vec3; + + #[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); + assert!((t.position.z - (-1.0)).abs() < 1e-6); + let tag = world2.get::(entities[0]).unwrap(); + assert_eq!(tag.0, "enemy"); + } + + #[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_eq!(entities.len(), 2); + assert!(world2.get::(entities[1]).is_some()); + let p = world2.get::(entities[1]).unwrap(); + assert_eq!(p.0, entities[0]); + } + + #[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()); + } + + #[test] + fn test_binary_too_short() { + let data = vec![0u8; 5]; + let mut world = World::new(); + let registry = ComponentRegistry::new(); + assert!(deserialize_scene_binary(&mut world, &data, ®istry).is_err()); + } + + #[test] + fn test_binary_multiple_entities() { + let mut registry = ComponentRegistry::new(); + registry.register_defaults(); + let mut world = World::new(); + for i in 0..3 { + let e = world.spawn(); + world.add(e, Transform::from_position(Vec3::new(i as f32, 0.0, 0.0))); + world.add(e, Tag(format!("e{}", i))); + } + + let data = serialize_scene_binary(&world, ®istry); + let mut world2 = World::new(); + let entities = deserialize_scene_binary(&mut world2, &data, ®istry).unwrap(); + assert_eq!(entities.len(), 3); + + for (i, &e) in entities.iter().enumerate() { + let t = world2.get::(e).unwrap(); + assert!((t.position.x - i as f32).abs() < 1e-6); + let tag = world2.get::(e).unwrap(); + assert_eq!(tag.0, format!("e{}", i)); + } + } +} diff --git a/crates/voltex_ecs/src/component_registry.rs b/crates/voltex_ecs/src/component_registry.rs new file mode 100644 index 0000000..b1b019b --- /dev/null +++ b/crates/voltex_ecs/src/component_registry.rs @@ -0,0 +1,198 @@ +/// Registration-based component serialization for scene formats. +/// Each registered component type has a name, a serialize function, +/// and a deserialize function. + +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 + } + + /// Register the default built-in component types: transform and tag. + pub fn register_defaults(&mut self) { + self.register("transform", serialize_transform, deserialize_transform); + self.register("tag", serialize_tag, deserialize_tag); + } +} + +impl Default for ComponentRegistry { + fn default() -> Self { + Self::new() + } +} + +// ── Transform: 9 f32s in little-endian (pos.xyz, rot.xyz, scale.xyz) ─ + +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(()) +} + +// ── Tag: UTF-8 string bytes ───────────────────────────────────────── + +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".to_string())?; + world.add(entity, crate::scene::Tag(s.to_string())); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{World, Transform}; + use voltex_math::Vec3; + + #[test] + fn test_register_and_find() { + let mut registry = ComponentRegistry::new(); + registry.register_defaults(); + assert!(registry.find("transform").is_some()); + assert!(registry.find("tag").is_some()); + assert!(registry.find("nonexistent").is_none()); + } + + #[test] + fn test_entries_count() { + let mut registry = ComponentRegistry::new(); + registry.register_defaults(); + assert_eq!(registry.entries().len(), 2); + } + + #[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()); + assert_eq!(data.unwrap().len(), 36); // 9 f32s * 4 bytes + } + + #[test] + fn test_serialize_missing_component() { + let mut registry = ComponentRegistry::new(); + registry.register_defaults(); + let mut world = World::new(); + let e = world.spawn(); + // no Transform added + + let entry = registry.find("transform").unwrap(); + assert!((entry.serialize)(&world, e).is_none()); + } + + #[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 { + position: Vec3::new(1.0, 2.0, 3.0), + rotation: Vec3::new(0.1, 0.2, 0.3), + scale: Vec3::new(4.0, 5.0, 6.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); + assert!((t.position.y - 2.0).abs() < 1e-6); + assert!((t.position.z - 3.0).abs() < 1e-6); + assert!((t.rotation.x - 0.1).abs() < 1e-6); + assert!((t.scale.x - 4.0).abs() < 1e-6); + } + + #[test] + fn test_roundtrip_tag() { + let mut registry = ComponentRegistry::new(); + registry.register_defaults(); + let mut world = World::new(); + let e = world.spawn(); + world.add(e, crate::scene::Tag("hello world".to_string())); + + let entry = registry.find("tag").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 tag = world2.get::(e2).unwrap(); + assert_eq!(tag.0, "hello world"); + } + + #[test] + fn test_deserialize_transform_too_short() { + let mut world = World::new(); + let e = world.spawn(); + let result = deserialize_transform(&mut world, e, &[0u8; 10]); + assert!(result.is_err()); + } +} diff --git a/crates/voltex_ecs/src/json.rs b/crates/voltex_ecs/src/json.rs new file mode 100644 index 0000000..796ced1 --- /dev/null +++ b/crates/voltex_ecs/src/json.rs @@ -0,0 +1,476 @@ +/// Mini JSON writer and parser for scene serialization. +/// No external dependencies — self-contained within voltex_ecs. + +#[derive(Debug, Clone, PartialEq)] +pub enum JsonVal { + Null, + Bool(bool), + Number(f64), + Str(String), + Array(Vec), + Object(Vec<(String, JsonVal)>), +} + +// ── Writer helpers ────────────────────────────────────────────────── + +pub fn json_write_null() -> String { + "null".to_string() +} + +pub fn json_write_f32(v: f32) -> String { + // Emit integer form when the value has no fractional part + if v.fract() == 0.0 && v.abs() < 1e15 { + format!("{}", v as i64) + } else { + format!("{}", v) + } +} + +pub fn json_write_string(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ => out.push(c), + } + } + out.push('"'); + out +} + +/// Write a JSON array from pre-formatted element strings. +pub fn json_write_array(elements: &[&str]) -> String { + let mut out = String::from("["); + for (i, elem) in elements.iter().enumerate() { + if i > 0 { + out.push(','); + } + out.push_str(elem); + } + out.push(']'); + out +} + +/// Write a JSON object from (key, pre-formatted-value) pairs. +pub fn json_write_object(pairs: &[(&str, &str)]) -> String { + let mut out = String::from("{"); + for (i, (key, val)) in pairs.iter().enumerate() { + if i > 0 { + out.push(','); + } + out.push_str(&json_write_string(key)); + out.push(':'); + out.push_str(val); + } + out.push('}'); + out +} + +// ── Accessors on JsonVal ──────────────────────────────────────────── + +impl JsonVal { + pub fn get(&self, key: &str) -> Option<&JsonVal> { + match self { + JsonVal::Object(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v), + _ => None, + } + } + + pub fn as_f64(&self) -> Option { + match self { + JsonVal::Number(n) => Some(*n), + _ => None, + } + } + + pub fn as_str(&self) -> Option<&str> { + match self { + JsonVal::Str(s) => Some(s.as_str()), + _ => None, + } + } + + pub fn as_array(&self) -> Option<&Vec> { + match self { + JsonVal::Array(a) => Some(a), + _ => None, + } + } + + pub fn as_object(&self) -> Option<&Vec<(String, JsonVal)>> { + match self { + JsonVal::Object(o) => Some(o), + _ => None, + } + } + + pub fn as_bool(&self) -> Option { + match self { + JsonVal::Bool(b) => Some(*b), + _ => None, + } + } +} + +// ── Parser (recursive descent) ────────────────────────────────────── + +pub fn json_parse(input: &str) -> Result { + let bytes = input.as_bytes(); + let (val, pos) = parse_value(bytes, skip_ws(bytes, 0))?; + let pos = skip_ws(bytes, pos); + if pos != bytes.len() { + return Err(format!("Unexpected trailing content at position {}", pos)); + } + Ok(val) +} + +fn skip_ws(b: &[u8], mut pos: usize) -> usize { + while pos < b.len() && matches!(b[pos], b' ' | b'\t' | b'\n' | b'\r') { + pos += 1; + } + pos +} + +fn parse_value(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + if pos >= b.len() { + return Err("Unexpected end of input".into()); + } + match b[pos] { + b'"' => parse_string(b, pos), + b'{' => parse_object(b, pos), + b'[' => parse_array(b, pos), + b't' | b'f' => parse_bool(b, pos), + b'n' => parse_null(b, pos), + _ => parse_number(b, pos), + } +} + +fn parse_string(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + let (s, end) = read_string(b, pos)?; + Ok((JsonVal::Str(s), end)) +} + +fn read_string(b: &[u8], pos: usize) -> Result<(String, usize), String> { + if pos >= b.len() || b[pos] != b'"' { + return Err(format!("Expected '\"' at position {}", pos)); + } + let mut i = pos + 1; + let mut s = String::new(); + while i < b.len() { + match b[i] { + b'"' => return Ok((s, i + 1)), + b'\\' => { + i += 1; + if i >= b.len() { + return Err("Unexpected end in string escape".into()); + } + match b[i] { + b'"' => s.push('"'), + b'\\' => s.push('\\'), + b'/' => s.push('/'), + b'n' => s.push('\n'), + b'r' => s.push('\r'), + b't' => s.push('\t'), + _ => { + s.push('\\'); + s.push(b[i] as char); + } + } + i += 1; + } + ch => { + s.push(ch as char); + i += 1; + } + } + } + Err("Unterminated string".into()) +} + +fn parse_number(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + let mut i = pos; + // optional minus + if i < b.len() && b[i] == b'-' { + i += 1; + } + // digits + while i < b.len() && b[i].is_ascii_digit() { + i += 1; + } + // fractional + if i < b.len() && b[i] == b'.' { + i += 1; + while i < b.len() && b[i].is_ascii_digit() { + i += 1; + } + } + // exponent + if i < b.len() && (b[i] == b'e' || b[i] == b'E') { + i += 1; + if i < b.len() && (b[i] == b'+' || b[i] == b'-') { + i += 1; + } + while i < b.len() && b[i].is_ascii_digit() { + i += 1; + } + } + if i == pos { + return Err(format!("Expected number at position {}", pos)); + } + let s = std::str::from_utf8(&b[pos..i]).unwrap(); + let n: f64 = s.parse().map_err(|e| format!("Invalid number '{}': {}", s, e))?; + Ok((JsonVal::Number(n), i)) +} + +fn parse_bool(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + if b[pos..].starts_with(b"true") { + Ok((JsonVal::Bool(true), pos + 4)) + } else if b[pos..].starts_with(b"false") { + Ok((JsonVal::Bool(false), pos + 5)) + } else { + Err(format!("Expected bool at position {}", pos)) + } +} + +fn parse_null(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + if b[pos..].starts_with(b"null") { + Ok((JsonVal::Null, pos + 4)) + } else { + Err(format!("Expected null at position {}", pos)) + } +} + +fn parse_array(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + let mut i = pos + 1; // skip '[' + let mut arr = Vec::new(); + i = skip_ws(b, i); + if i < b.len() && b[i] == b']' { + return Ok((JsonVal::Array(arr), i + 1)); + } + loop { + i = skip_ws(b, i); + let (val, next) = parse_value(b, i)?; + arr.push(val); + i = skip_ws(b, next); + if i >= b.len() { + return Err("Unterminated array".into()); + } + if b[i] == b']' { + return Ok((JsonVal::Array(arr), i + 1)); + } + if b[i] != b',' { + return Err(format!("Expected ',' or ']' at position {}", i)); + } + i += 1; // skip ',' + } +} + +fn parse_object(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> { + let mut i = pos + 1; // skip '{' + let mut pairs = Vec::new(); + i = skip_ws(b, i); + if i < b.len() && b[i] == b'}' { + return Ok((JsonVal::Object(pairs), i + 1)); + } + loop { + i = skip_ws(b, i); + let (key, next) = read_string(b, i)?; + i = skip_ws(b, next); + if i >= b.len() || b[i] != b':' { + return Err(format!("Expected ':' at position {}", i)); + } + i = skip_ws(b, i + 1); + let (val, next) = parse_value(b, i)?; + pairs.push((key, val)); + i = skip_ws(b, next); + if i >= b.len() { + return Err("Unterminated object".into()); + } + if b[i] == b'}' { + return Ok((JsonVal::Object(pairs), i + 1)); + } + if b[i] != b',' { + return Err(format!("Expected ',' or '}}' at position {}", i)); + } + i += 1; // skip ',' + } +} + +// ── JsonVal -> String serialization ───────────────────────────────── + +impl JsonVal { + /// Serialize this JsonVal to a compact JSON string. + pub fn to_json_string(&self) -> String { + match self { + JsonVal::Null => "null".to_string(), + JsonVal::Bool(b) => if *b { "true" } else { "false" }.to_string(), + JsonVal::Number(n) => { + if n.fract() == 0.0 && n.abs() < 1e15 { + format!("{}", *n as i64) + } else { + format!("{}", n) + } + } + JsonVal::Str(s) => json_write_string(s), + JsonVal::Array(arr) => { + let mut out = String::from("["); + for (i, v) in arr.iter().enumerate() { + if i > 0 { + out.push(','); + } + out.push_str(&v.to_json_string()); + } + out.push(']'); + out + } + JsonVal::Object(pairs) => { + let mut out = String::from("{"); + for (i, (k, v)) in pairs.iter().enumerate() { + if i > 0 { + out.push(','); + } + out.push_str(&json_write_string(k)); + out.push(':'); + out.push_str(&v.to_json_string()); + } + out.push('}'); + out + } + } + } +} + +#[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_negative_number() { + match json_parse("-3.14").unwrap() { + JsonVal::Number(n) => assert!((n - (-3.14)).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_string_with_escapes() { + assert_eq!( + json_parse(r#""a\"b\\c""#).unwrap(), + JsonVal::Str("a\"b\\c".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_empty_array() { + match json_parse("[]").unwrap() { + JsonVal::Array(a) => assert_eq!(a.len(), 0), + _ => panic!("expected array"), + } + } + + #[test] + fn test_parse_object() { + let val = json_parse(r#"{"x":1,"y":2}"#).unwrap(); + assert!(matches!(val, JsonVal::Object(_))); + assert_eq!(val.get("x").unwrap().as_f64().unwrap(), 1.0); + assert_eq!(val.get("y").unwrap().as_f64().unwrap(), 2.0); + } + + #[test] + fn test_parse_null() { + assert_eq!(json_parse("null").unwrap(), JsonVal::Null); + } + + #[test] + fn test_parse_bool() { + assert_eq!(json_parse("true").unwrap(), JsonVal::Bool(true)); + assert_eq!(json_parse("false").unwrap(), JsonVal::Bool(false)); + } + + #[test] + fn test_parse_nested() { + let val = json_parse(r#"{"a":[1,2],"b":{"c":3}}"#).unwrap(); + assert!(matches!(val, JsonVal::Object(_))); + let arr = val.get("a").unwrap().as_array().unwrap(); + assert_eq!(arr.len(), 2); + let inner = val.get("b").unwrap(); + assert_eq!(inner.get("c").unwrap().as_f64().unwrap(), 3.0); + } + + #[test] + fn test_parse_whitespace() { + let val = json_parse(" { \"a\" : 1 , \"b\" : [ 2 , 3 ] } ").unwrap(); + assert!(matches!(val, JsonVal::Object(_))); + } + + #[test] + fn test_json_val_to_json_string_roundtrip() { + let val = JsonVal::Object(vec![ + ("name".into(), JsonVal::Str("test".into())), + ("count".into(), JsonVal::Number(42.0)), + ("items".into(), JsonVal::Array(vec![ + JsonVal::Number(1.0), + JsonVal::Null, + JsonVal::Bool(true), + ])), + ]); + let s = val.to_json_string(); + let parsed = json_parse(&s).unwrap(); + assert_eq!(parsed, val); + } +} diff --git a/crates/voltex_ecs/src/lib.rs b/crates/voltex_ecs/src/lib.rs index b3a74ac..de4e573 100644 --- a/crates/voltex_ecs/src/lib.rs +++ b/crates/voltex_ecs/src/lib.rs @@ -6,6 +6,9 @@ pub mod hierarchy; pub mod world_transform; pub mod scene; pub mod scheduler; +pub mod json; +pub mod component_registry; +pub mod binary_scene; pub use entity::{Entity, EntityAllocator}; pub use sparse_set::SparseSet; @@ -13,5 +16,7 @@ pub use world::World; pub use transform::Transform; pub use hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots}; pub use world_transform::{WorldTransform, propagate_transforms}; -pub use scene::{Tag, serialize_scene, deserialize_scene}; +pub use scene::{Tag, serialize_scene, deserialize_scene, serialize_scene_json, deserialize_scene_json}; pub use scheduler::{Scheduler, System}; +pub use component_registry::ComponentRegistry; +pub use binary_scene::{serialize_scene_binary, deserialize_scene_binary}; diff --git a/crates/voltex_ecs/src/scene.rs b/crates/voltex_ecs/src/scene.rs index bc04b09..859f809 100644 --- a/crates/voltex_ecs/src/scene.rs +++ b/crates/voltex_ecs/src/scene.rs @@ -4,6 +4,8 @@ use crate::entity::Entity; use crate::world::World; use crate::transform::Transform; use crate::hierarchy::{add_child, Parent}; +use crate::component_registry::ComponentRegistry; +use crate::json::{self, JsonVal}; /// String tag for entity identification. #[derive(Debug, Clone)] @@ -152,10 +154,160 @@ pub fn deserialize_scene(world: &mut World, source: &str) -> Vec { created } +// ── Hex encoding helpers ──────────────────────────────────────────── + +fn bytes_to_hex(data: &[u8]) -> String { + let mut s = String::with_capacity(data.len() * 2); + for &b in data { + s.push_str(&format!("{:02x}", b)); + } + s +} + +fn hex_to_bytes(hex: &str) -> Result, String> { + if hex.len() % 2 != 0 { + return Err("Hex string has odd length".into()); + } + let mut bytes = Vec::with_capacity(hex.len() / 2); + let mut i = 0; + let chars: Vec = hex.bytes().collect(); + while i < chars.len() { + let hi = hex_digit(chars[i])?; + let lo = hex_digit(chars[i + 1])?; + bytes.push((hi << 4) | lo); + i += 2; + } + Ok(bytes) +} + +fn hex_digit(c: u8) -> Result { + match c { + b'0'..=b'9' => Ok(c - b'0'), + b'a'..=b'f' => Ok(c - b'a' + 10), + b'A'..=b'F' => Ok(c - b'A' + 10), + _ => Err(format!("Invalid hex digit: {}", c as char)), + } +} + +// ── JSON scene serialization ──────────────────────────────────────── + +/// Serialize all entities with a Transform to JSON format using the component registry. +/// Format: {"version":1,"entities":[{"parent":null_or_idx,"components":{"name":"hex",...}}]} +pub fn serialize_scene_json(world: &World, registry: &ComponentRegistry) -> String { + let entities_with_transform: Vec<(Entity, Transform)> = world + .query::() + .map(|(e, t)| (e, *t)) + .collect(); + + let entity_to_index: HashMap = entities_with_transform + .iter() + .enumerate() + .map(|(i, (e, _))| (*e, i)) + .collect(); + + // Build entity JSON values + let mut entity_vals = Vec::new(); + for (entity, _) in &entities_with_transform { + // Parent index + let parent_val = if let Some(parent_comp) = world.get::(*entity) { + if let Some(&idx) = entity_to_index.get(&parent_comp.0) { + JsonVal::Number(idx as f64) + } else { + JsonVal::Null + } + } else { + JsonVal::Null + }; + + // Components + let mut comp_pairs = Vec::new(); + for entry in registry.entries() { + if let Some(data) = (entry.serialize)(world, *entity) { + comp_pairs.push((entry.name.clone(), JsonVal::Str(bytes_to_hex(&data)))); + } + } + + entity_vals.push(JsonVal::Object(vec![ + ("parent".into(), parent_val), + ("components".into(), JsonVal::Object(comp_pairs)), + ])); + } + + let root = JsonVal::Object(vec![ + ("version".into(), JsonVal::Number(1.0)), + ("entities".into(), JsonVal::Array(entity_vals)), + ]); + + root.to_json_string() +} + +/// Deserialize entities from a JSON scene string. +pub fn deserialize_scene_json( + world: &mut World, + json_str: &str, + registry: &ComponentRegistry, +) -> Result, String> { + let root = json::json_parse(json_str)?; + + let version = root.get("version") + .and_then(|v| v.as_f64()) + .ok_or("Missing or invalid 'version'")?; + if version as u32 != 1 { + return Err(format!("Unsupported version: {}", version)); + } + + let entities_arr = root.get("entities") + .and_then(|v| v.as_array()) + .ok_or("Missing or invalid 'entities'")?; + + // First pass: create entities and deserialize components + let mut created: Vec = Vec::with_capacity(entities_arr.len()); + let mut parent_indices: Vec> = Vec::with_capacity(entities_arr.len()); + + for entity_val in entities_arr { + let entity = world.spawn(); + + // Parse parent index + let parent_idx = match entity_val.get("parent") { + Some(JsonVal::Number(n)) => Some(*n as usize), + _ => None, + }; + parent_indices.push(parent_idx); + + // Deserialize components + if let Some(comps) = entity_val.get("components").and_then(|v| v.as_object()) { + for (name, hex_val) in comps { + if let Some(hex_str) = hex_val.as_str() { + let data = hex_to_bytes(hex_str)?; + if let Some(entry) = registry.find(name) { + (entry.deserialize)(world, entity, &data)?; + } + } + } + } + + created.push(entity); + } + + // Second pass: apply parent relationships + for (child_idx, parent_idx_opt) in parent_indices.iter().enumerate() { + if let Some(parent_idx) = parent_idx_opt { + if *parent_idx < created.len() { + let child_entity = created[child_idx]; + let parent_entity = created[*parent_idx]; + add_child(world, parent_entity, child_entity); + } + } + } + + Ok(created) +} + #[cfg(test)] mod tests { use super::*; use crate::hierarchy::{add_child, roots, Parent}; + use crate::component_registry::ComponentRegistry; use voltex_math::Vec3; #[test] @@ -270,4 +422,75 @@ entity 2 let scene_roots = roots(&world); assert_eq!(scene_roots.len(), 2, "should have exactly 2 root entities"); } + + // ── JSON scene tests ──────────────────────────────────────────── + + #[test] + fn test_hex_roundtrip() { + let data = vec![0u8, 1, 15, 16, 255]; + let hex = bytes_to_hex(&data); + assert_eq!(hex, "00010f10ff"); + let back = hex_to_bytes(&hex).unwrap(); + assert_eq!(back, data); + } + + #[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")); + + 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); + assert!((t.position.y - 2.0).abs() < 1e-4); + assert!((t.position.z - 3.0).abs() < 1e-4); + let tag = world2.get::(entities[0]).unwrap(); + assert_eq!(tag.0, "player"); + } + + #[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_eq!(entities.len(), 2); + assert!(world2.get::(entities[1]).is_some()); + let parent_comp = world2.get::(entities[1]).unwrap(); + assert_eq!(parent_comp.0, entities[0]); + } + + #[test] + fn test_json_multiple_entities() { + let mut registry = ComponentRegistry::new(); + registry.register_defaults(); + let mut world = World::new(); + for i in 0..5 { + let e = world.spawn(); + world.add(e, Transform::from_position(Vec3::new(i as f32, 0.0, 0.0))); + world.add(e, Tag(format!("entity_{}", i))); + } + + let json = serialize_scene_json(&world, ®istry); + let mut world2 = World::new(); + let entities = deserialize_scene_json(&mut world2, &json, ®istry).unwrap(); + assert_eq!(entities.len(), 5); + } }