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}