Skip to main content

libdqg/
scene_file.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use crate::ecs::Entity;
6use crate::renderer::{Model, Renderer, Sprite, Texture};
7use crate::scripting::{ScriptAttachment, ScriptList};
8use crate::world::{CameraComponent, Renderable, Transform, World};
9
10/// The on-disk form of a scene: one of an editor project's `scenes/*.ron` files, or one of an
11/// exported game's `res/scenes/*.ron` files — both the editor and the `runtime` player
12/// deserialize this same type. A project can hold more than one (see
13/// `editor::Project::list_scenes`); which one a game boots into is
14/// [`GameManifest::start_scene`], and scripts switch between them via `scene.change(path)`
15/// (see `crate::scripting`'s `ScriptScene`/`WorldCommand::ChangeScene`).
16#[derive(serde::Serialize, serde::Deserialize, Default)]
17pub struct SceneFile {
18    pub entities: Vec<EntityRecord>,
19}
20
21impl SceneFile {
22    /// Reads and parses the `.ron` scene file at `path` — the single source of truth for the
23    /// on-disk format, used by the editor, `runtime`, and `ScriptRuntime`'s `scene.change(...)`
24    /// handling alike.
25    pub fn load(path: &Path) -> anyhow::Result<Self> {
26        Ok(ron::from_str(&fs::read_to_string(path)?)?)
27    }
28
29    /// Serializes and writes `self` to `path` as pretty-printed RON — the save-side counterpart
30    /// to [`SceneFile::load`].
31    pub fn save(&self, path: &Path) -> anyhow::Result<()> {
32        fs::write(path, ron::ser::to_string_pretty(self, Default::default())?)?;
33        Ok(())
34    }
35}
36
37#[derive(serde::Serialize, serde::Deserialize)]
38pub struct EntityRecord {
39    pub name: String,
40    pub transform: Transform,
41    pub renderable: Option<RenderableAsset>,
42    #[serde(default)]
43    pub scripts: Vec<ScriptAttachment>,
44    #[serde(default)]
45    pub camera: Option<CameraComponent>,
46}
47
48impl EntityRecord {
49    /// Spawns this record into `world`: creates the entity with its saved name/transform, loads
50    /// its `renderable` (if any, resolved against `base_dir` — an editor project's root, or an
51    /// exported game's `res/` directory) via [`RenderableAsset::load`], and copies over its
52    /// `scripts`/`camera` components verbatim (starting the scripts is a separate step — see
53    /// `crate::scripting::ScriptRuntime::start_all_scripts`). A renderable load failure is
54    /// reported to stderr and leaves the entity without one, rather than failing the whole spawn.
55    /// Returns the spawned entity plus the [`RenderableAsset`] if one loaded successfully — the
56    /// editor's asset-tracking map needs it back for re-saving; other callers can ignore it.
57    pub fn spawn_into(self, world: &mut World, renderer: Option<&Renderer>, base_dir: &Path) -> (Entity, Option<RenderableAsset>) {
58        let entity = world.spawn_empty(self.name, self.transform);
59        let mut loaded_asset = None;
60
61        if let Some(asset) = self.renderable {
62            match renderer {
63                Some(renderer) => match asset.load(renderer, base_dir) {
64                    Ok(renderable) => {
65                        world.set_renderable(entity, renderable);
66                        loaded_asset = Some(asset);
67                    }
68                    Err(e) => eprintln!("Failed to load renderable: {e}"),
69                },
70                None => eprintln!("Failed to load renderable: no renderer available"),
71            }
72        }
73        if !self.scripts.is_empty() {
74            world.scripts.insert(entity, ScriptList(self.scripts));
75        }
76        if let Some(component) = self.camera {
77            world.set_camera(entity, component);
78        }
79
80        (entity, loaded_asset)
81    }
82}
83
84#[derive(Clone, Copy, PartialEq, Eq)]
85pub enum RenderableKind {
86    Sprite,
87    Model,
88}
89
90/// The serializable stand-in for a [`Renderable`]: just the source asset path(s) (relative to
91/// the project root, or an exported game's `res/` directory) plus the handful of params needed
92/// to reconstruct it, since the runtime `Renderable` itself holds live GPU resources that can't
93/// be serialized.
94#[derive(Clone, serde::Serialize, serde::Deserialize)]
95pub enum RenderableAsset {
96    Sprite { texture_path: PathBuf, width: f32, height: f32 },
97    Model { model_path: PathBuf },
98}
99
100impl RenderableAsset {
101    /// `base_dir` resolves the stored relative asset path — an editor project's root when called
102    /// from the editor, or an exported game's `res/` directory when called from `runtime`.
103    pub fn load(&self, renderer: &Renderer, base_dir: &Path) -> anyhow::Result<Renderable> {
104        match self {
105            RenderableAsset::Sprite { texture_path, width, height } => {
106                let path = base_dir.join(texture_path);
107                let texture = Arc::new(
108                    Texture::from_path(renderer, &path).map_err(|e| anyhow::anyhow!(e))?,
109                );
110                let mut sprite = Sprite::new(texture);
111                if *width > 0.0 && *height > 0.0 {
112                    sprite.width = *width;
113                    sprite.height = *height;
114                } else {
115                    // No saved/explicit size: fall back to a unit-square fit. Covers both the
116                    // editor's `attach_renderable` probe (before it knows the real size) and any
117                    // other zero-sized `width`/`height` this shared loader is handed.
118                    sprite.fit_within_unit_square();
119                }
120                Ok(Renderable::Sprite(sprite))
121            }
122            RenderableAsset::Model { model_path } => {
123                let path = base_dir.join(model_path);
124                let model = Model::load(renderer, &path)?;
125                Ok(Renderable::Model(model))
126            }
127        }
128    }
129
130    pub fn kind(&self) -> RenderableKind {
131        match self {
132            RenderableAsset::Sprite { .. } => RenderableKind::Sprite,
133            RenderableAsset::Model { .. } => RenderableKind::Model,
134        }
135    }
136
137    pub fn path(&self) -> &PathBuf {
138        match self {
139            RenderableAsset::Sprite { texture_path, .. } => texture_path,
140            RenderableAsset::Model { model_path, .. } => model_path,
141        }
142    }
143}
144
145/// Window settings for an exported game: written to `res/game.ron` by the editor's export
146/// feature and read by the `runtime` binary at startup, alongside `res/scenes/*.ron`.
147#[derive(serde::Serialize, serde::Deserialize)]
148pub struct GameManifest {
149    pub title: String,
150    pub width: u32,
151    pub height: u32,
152    /// Project-relative path (mirrored 1:1 under `res/`) to the scene `runtime` boots into —
153    /// e.g. `"scenes/main.ron"`, resolved as `res_dir.join(start_scene)`.
154    pub start_scene: PathBuf,
155}