Files
game_engine/crates/voltex_ecs/src/component_registry.rs
tolelom 164eead5ec feat(ecs): add JSON/binary scene serialization and component registry
- Mini JSON writer/parser in voltex_ecs (no renderer dependency)
- ComponentRegistry with register/find/register_defaults
- serialize_scene_json/deserialize_scene_json with hex-encoded components
- serialize_scene_binary/deserialize_scene_binary (VSCN binary format)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:38:56 +09:00

199 lines
6.2 KiB
Rust

/// 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<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
}
/// 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<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(())
}
// ── Tag: UTF-8 string bytes ─────────────────────────────────────────
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".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::<Transform>(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::<crate::scene::Tag>(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());
}
}