Skip to main content

khora_data/scene/
messagepack_strategy.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! MessagePack scene strategy — portable schema-less binary.
10//!
11//! Same shape as [`super::RecipeSerializationStrategy`] (a topo-sorted
12//! [`super::SceneRecipe`]) but encoded with `rmp-serde` so any language
13//! with a MessagePack reader can consume it. Slightly larger on disk
14//! than bincode but the broad ecosystem support makes it the natural
15//! "interop" format for asset pipeline tooling outside Rust.
16
17use super::{DeserializationError, SerializationError, SerializationStrategy};
18use crate::{
19    ecs::World,
20    scene::{registry::ComponentRegistration, SceneCommand, SceneRecipe},
21};
22use khora_core::{ecs::entity::EntityId, graph::topological_sort};
23use std::collections::HashMap;
24
25/// MessagePack-encoded `SceneRecipe`. Identifier `KH_MESSAGEPACK_V1`.
26pub struct MessagePackSerializationStrategy;
27
28impl Default for MessagePackSerializationStrategy {
29    fn default() -> Self {
30        Self
31    }
32}
33
34impl MessagePackSerializationStrategy {
35    /// Constructs a fresh `MessagePackSerializationStrategy`. The type
36    /// has no state — the constructor exists for parity with the other
37    /// strategies and `Default`.
38    pub fn new() -> Self {
39        Self
40    }
41}
42
43impl SerializationStrategy for MessagePackSerializationStrategy {
44    fn get_strategy_id(&self) -> &'static str {
45        "KH_MESSAGEPACK_V1"
46    }
47
48    fn serialize(&self, world: &World) -> Result<Vec<u8>, SerializationError> {
49        let mut commands = Vec::new();
50
51        // Same topological-sort + walk as Recipe — MessagePack only
52        // changes the wire format.
53        let nodes: Vec<EntityId> = world.iter_entities().collect();
54        let mut edges: Vec<(EntityId, EntityId)> = Vec::new();
55        for &entity_id in &nodes {
56            if let Some(parent) = world.get::<crate::ecs::Parent>(entity_id) {
57                edges.push((parent.0, entity_id));
58            }
59        }
60        let sorted_entities = topological_sort(nodes, edges).map_err(|_| {
61            SerializationError::ProcessingFailed("Cycle detected in scene hierarchy.".to_string())
62        })?;
63
64        for entity_id in sorted_entities {
65            commands.push(SceneCommand::Spawn { id: entity_id });
66            for (component_type, component_data) in
67                crate::scene::serialize_all_components(world, entity_id)
68            {
69                commands.push(SceneCommand::AddComponent {
70                    entity_id,
71                    component_type,
72                    component_data,
73                });
74            }
75            if let Some(parent) = world.get::<crate::ecs::Parent>(entity_id) {
76                commands.push(SceneCommand::SetParent {
77                    child_id: entity_id,
78                    parent_id: parent.0,
79                });
80            }
81        }
82
83        let scene_recipe = SceneRecipe { commands };
84        rmp_serde::to_vec_named(&scene_recipe)
85            .map_err(|e| SerializationError::ProcessingFailed(e.to_string()))
86    }
87
88    fn deserialize(&self, data: &[u8], world: &mut World) -> Result<(), DeserializationError> {
89        let recipe: SceneRecipe = rmp_serde::from_slice(data)
90            .map_err(|e| DeserializationError::InvalidFormat(e.to_string()))?;
91
92        let mut id_map = HashMap::<EntityId, EntityId>::new();
93
94        for command in recipe.commands {
95            match command {
96                SceneCommand::Spawn { id } => {
97                    let new_id = world.spawn(());
98                    id_map.insert(id, new_id);
99                }
100                SceneCommand::AddComponent {
101                    entity_id,
102                    component_type,
103                    component_data,
104                } => {
105                    let live_id = id_map.get(&entity_id).copied().ok_or_else(|| {
106                        DeserializationError::WorldPopulationFailed(format!(
107                            "Recipe AddComponent for unspawned entity {:?}",
108                            entity_id
109                        ))
110                    })?;
111                    let reg = inventory::iter::<ComponentRegistration>
112                        .into_iter()
113                        .find(|r| r.type_name == component_type.as_str())
114                        .ok_or_else(|| {
115                            DeserializationError::WorldPopulationFailed(format!(
116                                "Unknown component type '{}' in recipe",
117                                component_type
118                            ))
119                        })?;
120                    (reg.deserialize_recipe)(world, live_id, &component_data).map_err(|e| {
121                        DeserializationError::WorldPopulationFailed(format!(
122                            "Failed to apply component '{}': {:?}",
123                            component_type, e
124                        ))
125                    })?;
126                }
127                SceneCommand::SetParent {
128                    child_id,
129                    parent_id,
130                } => {
131                    let child = id_map.get(&child_id).copied().ok_or_else(|| {
132                        DeserializationError::WorldPopulationFailed(format!(
133                            "Recipe SetParent unknown child {:?}",
134                            child_id
135                        ))
136                    })?;
137                    let parent = id_map.get(&parent_id).copied().ok_or_else(|| {
138                        DeserializationError::WorldPopulationFailed(format!(
139                            "Recipe SetParent unknown parent {:?}",
140                            parent_id
141                        ))
142                    })?;
143                    crate::scene::link_parent_child(world, child, parent);
144                }
145            }
146        }
147        Ok(())
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::ecs::{GlobalTransform, Transform, World};
155    use khora_core::math::Vec3;
156
157    #[test]
158    fn round_trip_preserves_transform() {
159        let mut src = World::new();
160        let t = Transform {
161            translation: Vec3::new(7.0, 0.0, -3.0),
162            ..Default::default()
163        };
164        src.spawn((t, GlobalTransform::identity()));
165
166        let strat = MessagePackSerializationStrategy::new();
167        let bytes = strat.serialize(&src).unwrap();
168        assert!(!bytes.is_empty());
169
170        let mut dst = World::new();
171        strat.deserialize(&bytes, &mut dst).unwrap();
172
173        let mut q = dst.query::<&Transform>();
174        let got = q.next().expect("at least one entity restored");
175        assert_eq!(*got, t);
176    }
177}