Skip to main content

kvas_engine/
scene.rs

1//! Scene = ECS world + environment settings, saved as readable JSON.
2
3use crate::components::*;
4use crate::hierarchy;
5use crate::registry::Registry;
6use glam::Vec3;
7use kvas_ecs::{Entity, World};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::path::Path;
12
13/// Serializable scene environment. Missing JSON fields use these defaults.
14#[derive(Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct SceneSettings {
17    /// Ambient RGB illumination, normally in the 0..1 range.
18    pub ambient: [f32; 3],
19    /// Fallback background RGB colour when the atmosphere is disabled.
20    pub sky: [f32; 3],
21    /// World-space gravity acceleration in metres per second squared.
22    pub gravity: Vec3,
23    /// Cel shading: 0 = off, otherwise the number of light bands (3 = anime look).
24    pub toon_bands: u32,
25    /// Colour saturation multiplier; 1 preserves the rendered colours.
26    pub saturation: f32,
27    /// Display contrast multiplier; 1 is neutral.
28    pub contrast: f32,
29    /// Exposure multiplier applied before display conversion; 1 is neutral.
30    pub exposure: f32,
31    /// PlayStation-style look: wobbling vertex snapping, point-sampled textures, dithered 15-bit colour.
32    pub psx: bool,
33    /// Render resolution scale (0.25..1). Below 1 the picture is upscaled with point filtering.
34    pub render_scale: f32,
35    /// Procedural sky and time of day.
36    pub atmosphere: crate::sky::SkySettings,
37    /// Sun shadow quality and look.
38    pub shadows: crate::fx::ShadowSettings,
39    /// Screen-space effects (bloom, blur, motion blur, flares, grain...).
40    pub post: crate::fx::PostSettings,
41    /// Upscaling method used when `render_scale` is below 1.
42    pub upscaler: crate::fx::Upscaler,
43    /// Experimental optical-flow midpoint presentation in Game view (not NVIDIA DLSS).
44    /// Alternates a synthesized frame with its real frame, adding visual latency.
45    pub frame_generation: bool,
46    /// Sharpening of the FSR-style upscaler, 0..1.
47    pub sharpness: f32,
48    /// Objects farther than this many metres are neither drawn nor kept in GPU memory (0 = unlimited).
49    pub view_distance: f32,
50    /// Filmic (ACES-style) tone mapping strength: 0 = plain clamp, 1 = full highlight roll-off.
51    pub filmic: f32,
52    /// Distance haze density (0 = off, 0.005 light, 0.02 thick). The colour follows the sky.
53    pub fog: f32,
54    /// Equirectangular .hdr/.png/.jpg asset used as background and mirror environment; empty uses sky.
55    pub environment_map: String,
56    /// Environment rotation around +Y, in degrees.
57    pub environment_rotation: f32,
58    /// Environment radiance multiplier.
59    pub environment_intensity: f32,
60}
61
62impl Default for SceneSettings {
63    fn default() -> Self {
64        Self {
65            ambient: [0.25, 0.27, 0.32],
66            sky: [0.35, 0.5, 0.75],
67            gravity: Vec3::new(0.0, -9.81, 0.0),
68            toon_bands: 0,
69            saturation: 1.0,
70            contrast: 1.0,
71            exposure: 1.0,
72            psx: false,
73            render_scale: 1.0,
74            atmosphere: Default::default(),
75            shadows: Default::default(),
76            post: Default::default(),
77            upscaler: Default::default(),
78            frame_generation: false,
79            sharpness: 0.5,
80            view_distance: 0.0,
81            filmic: 0.7,
82            fog: 0.0,
83            environment_map: String::new(),
84            environment_rotation: 0.0,
85            environment_intensity: 1.0,
86        }
87    }
88}
89
90/// ECS world plus environment settings. Default registers every built-in component.
91///
92/// Serialized scenes omit transient entities and rebuild world transforms on load.
93/// Entity handles are remapped on load and must not be persisted outside the scene.
94pub struct Scene {
95    /// Live entity/component storage, including runtime-only components.
96    pub world: World,
97    /// Scene lighting, physics and display configuration.
98    pub settings: SceneSettings,
99    /// Bounded next-frame script messages. Not serialized or copied into scene snapshots.
100    pub signals: crate::signals::SignalBus,
101    /// Sound requests for the shell's audio backend. Not serialized.
102    pub audio: crate::audio::AudioBus,
103    /// Named values shared by all scripts (score, difficulty...). Not serialized; see `Ctx::set_global`.
104    pub globals: std::collections::BTreeMap<String, Value>,
105}
106
107impl Default for Scene {
108    fn default() -> Self {
109        let mut world = World::new();
110        register_all(&mut world);
111        Scene { world, settings: SceneSettings::default(), signals: Default::default(), audio: Default::default(), globals: Default::default() }
112    }
113}
114
115#[derive(Serialize, Deserialize)]
116struct ComponentData {
117    #[serde(rename = "type")]
118    kind: String,
119    data: Value,
120}
121
122#[derive(Serialize, Deserialize)]
123struct EntityData {
124    parent: Option<usize>,
125    components: Vec<ComponentData>,
126}
127
128#[derive(Serialize, Deserialize, Default)]
129struct SceneFile {
130    #[serde(default)]
131    settings: SceneSettings,
132    entities: Vec<EntityData>,
133}
134
135impl Scene {
136    /// Serializes persistent entities and registered components as pretty JSON.
137    /// Runtime-only state is omitted. Prefer [`Self::try_to_json`] when saving files.
138    pub fn to_json(&self, reg: &Registry) -> String {
139        self.try_to_json(reg).unwrap_or_default()
140    }
141
142    /// Serializes a scene, reporting JSON encoding errors instead of returning an empty string.
143    /// Component serializers in the registry may omit components they cannot encode.
144    pub fn try_to_json(&self, reg: &Registry) -> Result<String, String> {
145        let entities: Vec<Entity> = self.world.entities().into_iter().filter(|&e| !self.world.has::<Transient>(e)).collect();
146        let settings_json = serde_json::to_value(&self.settings).map_err(|e| format!("scene settings: {e}"))?;
147        serde_json::from_value::<SceneSettings>(settings_json)
148            .map_err(|e| format!("scene settings cannot round-trip through JSON: {e}"))?;
149        for &entity in &entities {
150            let errors = reg.validate_entity(&self.world, entity);
151            if !errors.is_empty() {
152                return Err(format!("entity {}: {}", entity.index(), errors.join("; ")));
153            }
154        }
155        let index: HashMap<Entity, usize> = entities.iter().enumerate().map(|(i, &e)| (e, i)).collect();
156        let entities = entities
157            .iter()
158            .map(|&e| EntityData {
159                parent: hierarchy::parent_of(&self.world, e).and_then(|p| index.get(&p).copied()),
160                components: reg
161                    .components
162                    .iter()
163                    .filter_map(|c| (c.to_json)(&self.world, e).map(|data| ComponentData { kind: c.name.into(), data }))
164                    .collect(),
165            })
166            .collect();
167        serde_json::to_string_pretty(&SceneFile { settings: self.settings.clone(), entities })
168            .map_err(|e| format!("scene serialization error: {e}"))
169    }
170
171    /// Returns the scene and any non-fatal warnings (unknown or malformed components).
172    pub fn from_json(reg: &Registry, text: &str) -> Result<(Scene, Vec<String>), String> {
173        let file: SceneFile = serde_json::from_str(text).map_err(|e| format!("scene parse error: {e}"))?;
174        let mut scene = Scene { settings: file.settings, ..Scene::default() };
175        reg.register_world(&mut scene.world);
176        let warnings = load_entities(&mut scene, reg, file.entities);
177        Ok((scene, warnings))
178    }
179
180    /// Adds every object of a scene file to this scene (its settings are ignored). Returns the
181    /// warnings of the load; the new root objects keep their places.
182    pub fn add_json(&mut self, reg: &Registry, text: &str) -> Result<Vec<String>, String> {
183        let file: SceneFile = serde_json::from_str(text).map_err(|e| format!("scene parse error: {e}"))?;
184        Ok(load_entities(self, reg, file.entities))
185    }
186
187
188    /// Loads a UTF-8 JSON scene from disk, preserving non-fatal component/hierarchy warnings.
189    pub fn load(reg: &Registry, path: &Path) -> Result<(Scene, Vec<String>), String> {
190        let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
191        Self::from_json(reg, &text)
192    }
193
194    /// Saves persistent scene data. Parent directories must already exist.
195    pub fn save(&self, reg: &Registry, path: &Path) -> Result<(), String> {
196        let text = self.try_to_json(reg)?;
197        crate::file_io::atomic_write(path, text.as_bytes()).map_err(|e| format!("{}: {e}", path.display()))
198    }
199
200    /// Creates an independent persistent snapshot, omitting transient/runtime-only state.
201    /// Returns the same warnings as loading the serialized scene.
202    pub fn snapshot(&self, reg: &Registry) -> Result<(Scene, Vec<String>), String> {
203        Self::from_json(reg, &self.try_to_json(reg)?)
204    }
205
206    /// Reports persistence errors, dangling parents and hierarchy cycles without mutation.
207    /// Missing transforms are allowed by the ECS but reported here for editor-visible entities.
208    /// This explicit diagnostic pass does not run automatically on every gameplay tick.
209    pub fn validate(&self, reg: &Registry) -> Vec<String> {
210        let mut issues = Vec::new();
211        if let Err(error) = serde_json::to_value(&self.settings).and_then(serde_json::from_value::<SceneSettings>) {
212            issues.push(format!("scene settings: {error}"));
213        }
214        for entity in self.world.iter_entities() {
215            if self.world.has::<Transient>(entity) {
216                continue;
217            }
218            issues.extend(reg.validate_entity(&self.world, entity).into_iter().map(|error| format!("entity {}: {error}", entity.index())));
219            if !self.world.has::<Transform>(entity) {
220                issues.push(format!("entity {} has no Transform", entity.index()));
221            }
222            let mut current = Some(entity);
223            let mut seen = std::collections::HashSet::new();
224            while let Some(e) = current {
225                if !self.world.is_alive(e) {
226                    issues.push(format!("entity {} has a dangling parent", entity.index()));
227                    break;
228                }
229                if !seen.insert(e) {
230                    issues.push(format!("entity {} has a cyclic parent chain", entity.index()));
231                    break;
232                }
233                current = self.world.get::<Parent>(e).map(|p| p.0);
234            }
235        }
236        issues
237    }
238
239    /// Spawns a named entity with an identity local transform.
240    pub fn spawn(&mut self, name: &str) -> Entity {
241        hierarchy::spawn_named(&mut self.world, name)
242    }
243
244    /// First exact name match, if present. Entity names need not be unique.
245    pub fn find(&self, name: &str) -> Option<Entity> {
246        hierarchy::find_by_name(&self.world, name)
247    }
248
249    /// Removes an entity and its complete subtree, including transient children.
250    pub fn despawn(&mut self, entity: Entity) {
251        hierarchy::despawn_recursive(&mut self.world, entity);
252    }
253}
254
255/// Spawns serialized entities into a scene and links their parents.
256fn load_entities(scene: &mut Scene, reg: &Registry, entities: Vec<EntityData>) -> Vec<String> {
257    let mut warnings = Vec::new();
258    let spawned: Vec<Entity> = entities.iter().map(|_| scene.world.spawn()).collect();
259        let mut parents = Vec::new();
260        for (data, &e) in entities.into_iter().zip(&spawned) {
261            for c in data.components {
262                match reg.get(&c.kind) {
263                    Some(info) => {
264                        if let Err(err) = (info.from_json)(&mut scene.world, e, c.data) {
265                            warnings.push(err);
266                        }
267                    }
268                    None => warnings.push(format!("unknown component '{}'", c.kind)),
269                }
270            }
271            parents.push((e, data.parent));
272        }
273        // Resolve separately so validation is independent of entity/component order.
274        let mut groups: Vec<usize> = (0..spawned.len()).collect();
275        fn root(groups: &mut [usize], mut index: usize) -> usize {
276            while groups[index] != index {
277                groups[index] = groups[groups[index]];
278                index = groups[index];
279            }
280            index
281        }
282        // Each child has at most one parent. Joining an already-connected pair
283        // would close a directed cycle; union-find checks this without walking
284        // every ancestor of every node in a deeply nested imported scene.
285        for (child_index, (e, parent)) in parents.into_iter().enumerate() {
286            if let Some(index) = parent {
287                match spawned.get(index).copied() {
288                    Some(p) => {
289                        let child_root = root(&mut groups, child_index);
290                        let parent_root = root(&mut groups, index);
291                        if child_root == parent_root {
292                            warnings.push(format!("entity {}: cyclic parent {index} ignored", e.index()));
293                        } else {
294                            groups[child_root] = parent_root;
295                            scene.world.insert(e, Parent(p));
296                        }
297                    }
298                    None => warnings.push(format!("entity {}: invalid parent index {index} ignored", e.index())),
299                }
300            }
301        }
302    hierarchy::update_globals(&mut scene.world);
303    warnings
304}
305
306
307/// Deep-copies registered components of a non-transient subtree.
308/// The copy is a sibling of the original; only the root gets a ` (Copy)` suffix.
309/// Runtime-only components are omitted and name-based references are not remapped.
310/// Traversal terminates even for malformed cyclic input hierarchies.
311///
312/// # Panics
313/// Panics when `e` is stale. The destination world must register every copied type.
314pub fn duplicate(world: &mut World, reg: &Registry, e: Entity) -> Entity {
315    assert!(world.is_alive(e), "cannot duplicate a stale entity");
316    let mut originals = vec![e];
317    let mut cursor = 0;
318    let mut seen = std::collections::HashSet::from([e]);
319    while cursor < originals.len() {
320        let current = originals[cursor];
321        for child in hierarchy::children(world, current) {
322            if seen.insert(child) {
323                originals.push(child);
324            }
325        }
326        cursor += 1;
327    }
328    let mut copies = HashMap::new();
329    for &original in &originals {
330        let copy = world.spawn();
331        reg.copy_entity(world, original, copy);
332        copies.insert(original, copy);
333    }
334    let copy = copies[&e];
335    if let Some(n) = world.get_mut::<Name>(copy) {
336        n.0.push_str(" (Copy)");
337    }
338    for original in originals {
339        if let Some(parent) = hierarchy::parent_of(world, original) {
340            let target = if original == e { (!copies.contains_key(&parent)).then_some(parent) } else { copies.get(&parent).copied() };
341            if let Some(target) = target {
342                hierarchy::set_parent_local(world, copies[&original], Some(target));
343            }
344        }
345    }
346    copy
347}