# Asset Server & Handles `oxide_engine::asset` is the engine's central way to load and own external data (meshes, and later textures, audio, fonts, …). It lands in Stage 5 and underpins three later stages: live reload (Stage 10), open-world streaming (Stage 21), and export packing (Stage 16). Putting it in early means later loaders plug into one system instead of each inventing its own. ## Handles own assets The unit of ownership is a [`Handle`]: a typed, reference-counted reference to a loaded asset. It is cheap to clone (an `Arc` bump), and the asset behind it lives exactly as long as at least one handle does. The [`AssetServer`] keeps only a `Weak` reference in its dedup cache, so it never keeps an otherwise-unused asset alive — drop the last handle and the asset is freed. ```rust use oxide_engine::asset::{AssetServer, GltfModel, Handle}; let assets = AssetServer::new(); // built-in loaders (glTF) registered let model: Handle = assets.load("assets/models/cube.gltf"); if let Some(model) = model.get() { // Option>, None until loaded println!("{} meshes", model.meshes.len()); } ``` Key handle methods: | Method | Meaning | |--------|---------| | `get()` | `Some(Arc)` once loaded, else `None` (cheap clone of the value `Arc`) | | `state()` / `is_loaded()` | lifecycle: `Loading` / `Loaded` / `Failed` | | `wait()` | block until ready; `Some(value)` or `None` on failure | | `error()` | the failure message, if any | | `source()` | the path it was loaded from (in-memory assets have none) | | `ref_count()` | number of live handles (the server holds none) | ## Deduplication and freeing Loading the same path+type twice returns handles to **one** shared asset — the loader runs once: ```rust # use oxide_engine::asset::{AssetServer, GltfModel, Handle}; # let assets = AssetServer::new(); let a: Handle = assets.load("model.gltf"); let b: Handle = assets.load("model.gltf"); assert_eq!(a.id(), b.id()); // same underlying asset assert_eq!(assets.live_asset_count(), 1); ``` When the last handle drops, the weak cache entry dies and the asset is collected; `live_asset_count()` prunes such entries as it counts. ## Synchronous vs background loading `load` blocks until the asset is ready. `load_async` returns immediately with a handle in the `Loading` state and fills it on a background thread: ```rust # use oxide_engine::asset::{AssetServer, GltfModel, Handle}; # let assets = AssetServer::new(); let handle: Handle = assets.load_async("big.gltf"); // ... do other work while it loads ... let model = handle.wait(); // or poll handle.state() ``` `add(value)` stores an already-constructed, in-memory asset (no path, not deduplicated) and returns a handle to it — useful for procedurally generated or test data. ## Writing a loader Formats are pluggable via the [`AssetLoader`] trait: declare the output type and the extensions, and implement `load`. Register it with `server.register_loader(...)`; the server dispatches by extension and verifies the loader's output type matches the requested `T`. ```rust use std::path::Path; use oxide_engine::asset::{AssetError, AssetLoader}; struct TextLoader; impl AssetLoader for TextLoader { type Asset = String; fn extensions(&self) -> &'static [&'static str] { &["txt"] } fn load(&self, path: &Path) -> Result { std::fs::read_to_string(path).map_err(|e| AssetError::Load { path: path.to_path_buf(), message: e.to_string(), }) } } let assets = oxide_engine::asset::AssetServer::empty(); // no built-ins assets.register_loader(TextLoader); ``` The built-in [`GltfLoader`] is exactly this pattern wrapping the Stage-4 [`load_gltf`] importer; `AssetServer::new()` registers it for `.gltf`/`.glb`. Errors are specific — `NoExtension`, `NoLoader`, `TypeMismatch`, and `Load` — so callers can tell "no loader" from "the file is broken". ## Live reload foundation `reload(path)` re-runs the loader and updates the existing asset **in place**, so every live handle observes the new contents on its next `get()`: ```rust # use oxide_engine::asset::{AssetServer, GltfModel, Handle}; # let assets = AssetServer::new(); let model: Handle = assets.load("model.gltf"); // ... the file changes on disk ... let same = assets.reload::("model.gltf"); assert_eq!(same.id(), model.id()); // same asset, new data ``` Stage 10 wires this to the file watcher to hot-reload changed assets while the editor runs. ## Asset database — stable, project-relative references The [`AssetServer`] loads by *path*, but a scene or UI document must not bake an **absolute** system path into its saved data — that breaks the moment the project moves to another directory or machine, and it is the chief obstacle to a clean game export (Stage 16). The [`AssetDatabase`] is the bridge. Every imported asset lives under a **typed subfolder** of the project's `assets/` directory and gets a stable [`AssetUid`]. The database records, per project, the mapping **`AssetUid` ↔ assets-relative path** (e.g. `"fonts/Inter-Regular.ttf"`) in a manifest at the project root (`assets.manifest`). Saved documents reference assets by `AssetUid`; resolving a uid yields the relative path, which combined with the *current* project root gives an absolute path the server loads and deduplicates. ```rust use oxide_engine::asset::{AssetDatabase, AssetServer, GltfModel}; let mut db = AssetDatabase::open(project_root); // reads assets.manifest if present db.scan(); // discover files in fonts/ models/ … db.save().unwrap(); // persist any newly-assigned uids let uid = db.uid_of("models/cube.glb").unwrap(); let server = AssetServer::new(); let model = db.load::(&server, uid); // Option> ``` Because the stored mapping is purely relative, a reference resolves to the **same handle** across save/load *and* after the whole project directory moves — open the database from the new location (or call `set_root`) and every uid keeps resolving. The uid layer (rather than referencing by relative path directly) also lets an asset be renamed or moved *within* the project later without breaking references, since the uid travels with the file in the manifest. ### Typed folders [`AssetKind`] fixes each asset's subfolder and the extensions that belong to it: | Kind | Folder | Extensions | |------|--------|-----------| | `Font` | `fonts/` | `ttf`, `otf` | | `Texture` | `textures/` | `png`, `jpg`, `jpeg`, `tga`, `bmp`, `dds`, `ktx2` | | `Model` | `models/` | `gltf`, `glb`, `obj` | | `Audio` | `audio/` | `wav`, `ogg`, `mp3`, `flac` | | `Ui` | `ui/` | (recognised by folder — shares `.ron` with scenes) | | `Script` | `scripts/` | `rhai` | `AssetKind::classify(path)` infers the kind: the leading folder wins, with the file extension as a fallback for files dropped directly in `assets/`. ### File operations — rename, move, delete The database also *performs* file reorganisation, so the editor's file explorer (and any tool) can rename or move assets **without breaking saved references** — the disk operation and the uid map are updated together: ```rust # use oxide_engine::asset::{AssetDatabase, AssetUid}; # fn demo(db: &mut AssetDatabase, uid: AssetUid) -> Result<(), oxide_engine::asset::AssetDbError> { db.move_asset(uid, "textures/env/brick.png")?; // rename/move; uid unchanged db.move_folder("textures/env", "textures/world")?; // every entry under it follows db.delete_asset(uid)?; // file + entry; uid never reused db.save()?; // persist the new paths # Ok(()) # } ``` All three refuse to touch anything outside `assets/` (`..` is rejected) and refuse to overwrite an existing destination ([`AssetDbError`] has a variant per failure). Kinds are re-classified from the new path, since a move can change the typed folder. Deleting drops the entry but never reuses its uid — a dangling reference stays dangling instead of silently pointing at a new file. ### Asset-reference fields in the inspector A component points at an asset with an [`AssetRef`] field — **not** a live `Handle`. A handle is process-local and not serializable, so persisting one would be wrong; an `AssetRef` is a thin, serializable wrapper over `Option` that resolves to a handle on demand: ```rust use oxide_engine::asset::{AssetRef, AssetServer, AssetDatabase}; use oxide_engine::ui::Font; #[derive(serde::Serialize, serde::Deserialize)] struct Label { font: AssetRef } // serializes as just the uid # fn demo(label: &Label, db: &AssetDatabase, server: &AssetServer) { let handle = label.font.resolve(db, server); // Option> # } ``` Because `AssetRef` round-trips through reflection's RON path, the field is editable in the inspector with **no per-type code**. Its [`type_name`](../engine/src/reflect.rs) is the syntactic spelling `"AssetRef < Font >"`; [`asset_ref_target`] unwraps that to the target type name (`"Font"`), and [`AssetKind::for_handle_target`] maps it to the kind the editor's asset picker filters by — so selecting a UI element and picking a font lists only the assets under `fonts/`. A bare `Handle` field (e.g. on a non-persisted type) is recognised the same way. ### In the editor `EditorState` holds an `asset_db` whenever a project is open: the editor opens and scans it on project open/create and rescans when the file watcher reports changes under `assets/`, so importing an asset is just **dropping the file into the matching typed folder** (`fonts/`, `textures/`, …) — no separate import step. The **Project panel** is an asset browser listing each typed folder's assets from the database, and an asset-reference field in the inspector renders as a **picker** populated from it, filtered to the field's target kind. New projects are seeded with a bundled default UI font (Inter, SIL OFL) under `fonts/`, referenced by its project-relative path like any other asset. [`AssetDatabase`]: ../engine/src/asset/database.rs [`AssetDbError`]: ../engine/src/asset/database.rs [`AssetKind`]: ../engine/src/asset/database.rs [`AssetUid`]: ../engine/src/asset/database.rs [`AssetRef`]: ../engine/src/asset/database.rs [`asset_ref_target`]: ../engine/src/asset/database.rs [`AssetKind::for_handle_target`]: ../engine/src/asset/database.rs [`Handle`]: ../engine/src/asset/handle.rs [`AssetServer`]: ../engine/src/asset/server.rs [`AssetLoader`]: ../engine/src/asset/server.rs [`GltfLoader`]: ../engine/src/asset/gltf.rs [`load_gltf`]: ../engine/src/asset/gltf.rs