Skip to main content

goud_engine/assets/server/
operations.rs

1//! Asset loading and access operations for `AssetServer`.
2
3use super::core::AssetServer;
4use crate::assets::{Asset, AssetHandle, AssetLoadError, AssetPath, AssetState, LoadContext};
5#[cfg(feature = "native")]
6use crate::assets::{HotReloadConfig, HotReloadWatcher};
7use std::path::Path;
8
9impl AssetServer {
10    /// Loads an asset from a path (relative to asset root), returning a handle immediately.
11    ///
12    /// The asset loads synchronously (blocking). The handle is valid even if loading
13    /// fails — check with `get_load_state()`.
14    pub fn load<A: Asset>(&mut self, path: impl AsRef<Path>) -> AssetHandle<A> {
15        let asset_path = AssetPath::new(path.as_ref().to_str().unwrap_or_default());
16
17        // Check if already loaded
18        if let Some(handle) = self.storage.get_handle_by_path::<A>(asset_path.as_str()) {
19            return handle;
20        }
21
22        // Reserve a handle for this asset
23        let handle = self.storage.reserve_with_path::<A>(asset_path.clone());
24
25        // Load the asset synchronously
26        match self.load_asset_sync::<A>(&asset_path) {
27            Ok((asset, dependencies)) => {
28                self.storage.set_loaded(&handle, asset);
29                // Record dependencies in the graph
30                let asset_str = asset_path.as_str().to_string();
31                for dep in &dependencies {
32                    if let Err(e) = self.dependency_graph.add_dependency(&asset_str, dep) {
33                        log::warn!("Dependency cycle detected loading '{}': {}", asset_str, e);
34                    }
35                }
36            }
37            Err(error) => {
38                // Mark as failed
39                if let Some(entry) = self.storage.get_entry_mut::<A>(&handle) {
40                    entry.set_failed(error.to_string());
41                }
42            }
43        }
44
45        handle
46    }
47
48    /// Runs raw bytes through the registered loader for the given asset path's extension.
49    ///
50    /// Returns the loaded asset and any dependencies declared by the loader.
51    pub(super) fn parse_bytes<A: Asset>(
52        &self,
53        path: &AssetPath,
54        bytes: &[u8],
55    ) -> Result<(A, Vec<String>), AssetLoadError> {
56        let extension = path
57            .extension()
58            .ok_or_else(|| AssetLoadError::unsupported_format(""))?;
59
60        // Try compound extension first (e.g. "mat.json", "anim.json"), then simple
61        let loader = self
62            .find_loader_for_path(path, extension)
63            .ok_or_else(|| AssetLoadError::unsupported_format(extension))?;
64
65        let mut context = LoadContext::new(path.clone().into_owned());
66        let boxed = loader.load_erased(bytes, &mut context)?;
67
68        let dependencies = context.into_dependencies();
69
70        let asset = boxed
71            .downcast::<A>()
72            .map(|boxed| *boxed)
73            .map_err(|_| AssetLoadError::custom("Type mismatch after loading"))?;
74
75        Ok((asset, dependencies))
76    }
77
78    /// Reads a file from disk and parses it into an asset.
79    fn load_asset_sync<A: Asset>(
80        &self,
81        path: &AssetPath,
82    ) -> Result<(A, Vec<String>), AssetLoadError> {
83        let full_path = self.asset_root.join(path.as_str());
84        let bytes =
85            std::fs::read(&full_path).map_err(|e| AssetLoadError::io_error(&full_path, e))?;
86        self.parse_bytes::<A>(path, &bytes)
87    }
88
89    /// Loads an asset from pre-fetched bytes, returning a handle.
90    ///
91    /// This is the platform-agnostic entry point for loading assets when you
92    /// already have the raw bytes (e.g., from a web fetch, embedded resource,
93    /// or custom I/O layer). The bytes are run through the registered loader
94    /// matched by the path's file extension.
95    ///
96    /// Returns an existing handle if an asset with the same path is already loaded.
97    ///
98    /// # Arguments
99    ///
100    /// * `path` - Logical asset path (used for loader lookup and deduplication)
101    /// * `bytes` - Raw asset bytes to parse
102    ///
103    /// # Example
104    ///
105    /// ```
106    /// use goud_engine::assets::{Asset, AssetServer, AssetLoader, LoadContext, AssetLoadError};
107    ///
108    /// #[derive(Clone)]
109    /// struct JsonAsset { data: String }
110    /// impl Asset for JsonAsset {}
111    ///
112    /// #[derive(Clone)]
113    /// struct JsonLoader;
114    /// impl AssetLoader for JsonLoader {
115    ///     type Asset = JsonAsset;
116    ///     type Settings = ();
117    ///     fn extensions(&self) -> &[&str] { &["json"] }
118    ///     fn load<'a>(
119    ///         &'a self, bytes: &'a [u8], _: &'a (), _: &'a mut LoadContext,
120    ///     ) -> Result<Self::Asset, AssetLoadError> {
121    ///         Ok(JsonAsset { data: String::from_utf8_lossy(bytes).into_owned() })
122    ///     }
123    /// }
124    ///
125    /// let mut server = AssetServer::new();
126    /// server.register_loader(JsonLoader);
127    ///
128    /// let bytes = br#"{"key": "value"}"#;
129    /// let handle = server.load_from_bytes::<JsonAsset>("config.json", bytes);
130    /// assert!(server.is_loaded(&handle));
131    /// ```
132    pub fn load_from_bytes<A: Asset>(
133        &mut self,
134        path: impl AsRef<Path>,
135        bytes: &[u8],
136    ) -> AssetHandle<A> {
137        let asset_path = AssetPath::new(path.as_ref().to_str().unwrap_or_default());
138
139        if let Some(handle) = self.storage.get_handle_by_path::<A>(asset_path.as_str()) {
140            return handle;
141        }
142
143        let handle = self.storage.reserve_with_path::<A>(asset_path.clone());
144
145        match self.parse_bytes::<A>(&asset_path, bytes) {
146            Ok((asset, dependencies)) => {
147                self.storage.set_loaded(&handle, asset);
148                // Record dependencies in the graph
149                let asset_str = asset_path.as_str().to_string();
150                for dep in &dependencies {
151                    if let Err(e) = self.dependency_graph.add_dependency(&asset_str, dep) {
152                        log::warn!("Dependency cycle detected loading '{}': {}", asset_str, e);
153                    }
154                }
155            }
156            Err(error) => {
157                if let Some(entry) = self.storage.get_entry_mut::<A>(&handle) {
158                    entry.set_failed(error.to_string());
159                }
160            }
161        }
162
163        handle
164    }
165
166    /// Gets a reference to a loaded asset.
167    ///
168    /// Returns `None` if the asset is not loaded or the handle is invalid.
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use goud_engine::assets::{Asset, AssetServer};
174    ///
175    /// struct MyAsset { value: i32 }
176    /// impl Asset for MyAsset {}
177    ///
178    /// let mut server = AssetServer::new();
179    /// let handle = server.load::<MyAsset>("data/config.json");
180    ///
181    /// if let Some(asset) = server.get(&handle) {
182    ///     println!("Asset value: {}", asset.value);
183    /// }
184    /// ```
185    #[inline]
186    pub fn get<A: Asset>(&self, handle: &AssetHandle<A>) -> Option<&A> {
187        self.storage.get(handle)
188    }
189
190    /// Gets a mutable reference to a loaded asset.
191    ///
192    /// Returns `None` if the asset is not loaded or the handle is invalid.
193    #[inline]
194    pub fn get_mut<A: Asset>(&mut self, handle: &AssetHandle<A>) -> Option<&mut A> {
195        self.storage.get_mut(handle)
196    }
197
198    /// Returns true if the handle points to a valid, loaded asset.
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// use goud_engine::assets::{Asset, AssetServer, AssetHandle};
204    ///
205    /// struct MyAsset;
206    /// impl Asset for MyAsset {}
207    ///
208    /// let server = AssetServer::new();
209    /// let handle = AssetHandle::<MyAsset>::INVALID;
210    ///
211    /// assert!(!server.is_loaded(&handle));
212    /// ```
213    pub fn is_loaded<A: Asset>(&self, handle: &AssetHandle<A>) -> bool {
214        self.storage
215            .get_state(handle)
216            .map(|s| s.is_ready())
217            .unwrap_or(false)
218    }
219
220    /// Returns the loading state for a handle, or `None` if not tracked.
221    #[inline]
222    pub fn get_load_state<A: Asset>(&self, handle: &AssetHandle<A>) -> Option<AssetState> {
223        self.storage.get_state(handle)
224    }
225
226    /// Unloads an asset, freeing its memory.
227    ///
228    /// The handle becomes invalid after this call.
229    ///
230    /// # Returns
231    ///
232    /// The unloaded asset if it was loaded, otherwise `None`.
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// use goud_engine::assets::{Asset, AssetServer};
238    ///
239    /// struct Texture { width: u32 }
240    /// impl Asset for Texture {}
241    ///
242    /// let mut server = AssetServer::new();
243    /// let handle = server.load::<Texture>("texture.png");
244    ///
245    /// // Later, unload to free memory
246    /// let texture = server.unload(&handle);
247    /// assert!(!server.is_loaded(&handle));
248    /// ```
249    pub fn unload<A: Asset>(&mut self, handle: &AssetHandle<A>) -> Option<A> {
250        // Clean up dependency graph for unloaded asset
251        if let Some(entry) = self.storage.get_entry(handle) {
252            if let Some(path) = entry.path() {
253                let path_str = path.as_str().to_string();
254                self.dependency_graph.remove_asset(&path_str);
255            }
256        }
257        self.storage.remove(handle)
258    }
259
260    /// Returns a reference to the dependency graph.
261    ///
262    /// Useful for inspecting asset relationships or computing
263    /// cascade reload orders externally.
264    pub fn dependency_graph(&self) -> &crate::assets::dependency::DependencyGraph {
265        &self.dependency_graph
266    }
267
268    /// Returns a mutable reference to the dependency graph.
269    pub fn dependency_graph_mut(&mut self) -> &mut crate::assets::dependency::DependencyGraph {
270        &mut self.dependency_graph
271    }
272
273    /// Reloads an asset from disk by its path, using the type-erased loader.
274    ///
275    /// This is used by the hot-reload watcher to reload assets without knowing
276    /// their concrete type at compile time.
277    ///
278    /// # Returns
279    ///
280    /// `true` if the asset was successfully reloaded, `false` if the path
281    /// has no registered loader or the reload failed.
282    #[cfg(feature = "native")]
283    pub fn reload_by_path(&mut self, path: &str) -> bool {
284        let asset_path = AssetPath::new(path);
285        let extension = match asset_path.extension() {
286            Some(ext) => ext.to_string(),
287            None => return false,
288        };
289
290        // Check if we have a loader for this extension
291        let loader = match self.loaders.get(&extension) {
292            Some(l) => l.clone_boxed(),
293            None => return false,
294        };
295
296        // Read file from disk
297        let full_path = self.asset_root.join(path);
298        let bytes = match std::fs::read(&full_path) {
299            Ok(b) => b,
300            Err(_) => return false,
301        };
302
303        // Parse using erased loader
304        let mut context = LoadContext::new(asset_path.into_owned());
305        match loader.load_erased(&bytes, &mut context) {
306            Ok(boxed_asset) => {
307                // Update in storage if the asset exists
308                self.storage.replace_erased(path, boxed_asset);
309
310                // Update dependency graph with new dependencies from reload
311                let new_deps = context.into_dependencies();
312                let path_str = path.to_string();
313                self.dependency_graph.remove_asset(&path_str);
314                for dep in &new_deps {
315                    if let Err(e) = self.dependency_graph.add_dependency(&path_str, dep) {
316                        log::warn!(
317                            "Dependency cycle detected during hot-reload of '{}': {}",
318                            path,
319                            e
320                        );
321                    }
322                }
323
324                true
325            }
326            Err(_) => false,
327        }
328    }
329
330    /// Returns the cascade reload order for a changed asset path.
331    ///
332    /// This delegates to [`DependencyGraph::get_cascade_order`] and
333    /// returns the list of asset paths that should be reloaded, in
334    /// the correct order, when `changed_path` changes.
335    pub fn get_cascade_order(&self, changed_path: &str) -> Vec<String> {
336        self.dependency_graph.get_cascade_order(changed_path)
337    }
338
339    /// Finds a loader for a path, trying compound extensions first.
340    ///
341    /// For a path like `"brick.mat.json"`, tries `"mat.json"` first,
342    /// then falls back to `"json"`.
343    fn find_loader_for_path(
344        &self,
345        path: &AssetPath,
346        simple_ext: &str,
347    ) -> Option<&dyn crate::assets::ErasedAssetLoader> {
348        // Try compound extension (everything after first dot in filename)
349        if let Some(file_name) = path.file_name() {
350            if let Some(first_dot) = file_name.find('.') {
351                let compound_ext = &file_name[first_dot + 1..];
352                if compound_ext != simple_ext {
353                    if let Some(loader) = self.loaders.get(compound_ext) {
354                        return Some(loader.as_ref());
355                    }
356                }
357            }
358        }
359        // Fall back to simple extension
360        self.loaders.get(simple_ext).map(|b| b.as_ref())
361    }
362
363    /// Returns the number of loaded assets of a specific type.
364    ///
365    /// # Example
366    ///
367    /// ```
368    /// use goud_engine::assets::{Asset, AssetServer};
369    ///
370    /// struct Texture;
371    /// impl Asset for Texture {}
372    ///
373    /// let server = AssetServer::new();
374    /// assert_eq!(server.loaded_count::<Texture>(), 0);
375    /// ```
376    #[inline]
377    pub fn loaded_count<A: Asset>(&self) -> usize {
378        self.storage.len::<A>()
379    }
380
381    /// Returns the total number of loaded assets across all types.
382    #[inline]
383    pub fn total_loaded_count(&self) -> usize {
384        self.storage.total_len()
385    }
386
387    /// Returns the number of registered asset types.
388    #[inline]
389    pub fn registered_type_count(&self) -> usize {
390        self.storage.type_count()
391    }
392
393    /// Clears all loaded assets of a specific type.
394    ///
395    /// This frees memory but does not affect registered loaders.
396    pub fn clear_type<A: Asset>(&mut self) {
397        self.storage.clear_type::<A>();
398    }
399
400    /// Clears all loaded assets from all types.
401    ///
402    /// This frees memory but does not affect registered loaders.
403    pub fn clear(&mut self) {
404        self.storage.clear();
405    }
406
407    /// Returns an iterator over all loaded asset handles of a specific type.
408    pub fn handles<A: Asset>(&self) -> impl Iterator<Item = AssetHandle<A>> + '_ {
409        self.storage.handles::<A>()
410    }
411
412    /// Returns an iterator over all loaded assets of a specific type.
413    pub fn iter<A: Asset>(&self) -> impl Iterator<Item = (AssetHandle<A>, &A)> {
414        self.storage.iter::<A>()
415    }
416
417    /// Creates a hot reload watcher for this asset server.
418    ///
419    /// The watcher will detect file changes in the asset root directory
420    /// and automatically reload modified assets.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if the file watcher cannot be initialized.
425    ///
426    /// # Example
427    ///
428    /// ```no_run
429    /// use goud_engine::assets::AssetServer;
430    ///
431    /// let mut server = AssetServer::new();
432    /// let mut watcher = server.create_hot_reload_watcher().unwrap();
433    ///
434    /// // In game loop
435    /// loop {
436    ///     watcher.process_events(&mut server);
437    ///     // ... rest of game loop
438    /// }
439    /// ```
440    #[cfg(feature = "native")]
441    pub fn create_hot_reload_watcher(&self) -> notify::Result<HotReloadWatcher> {
442        HotReloadWatcher::new(self)
443    }
444
445    /// Creates a hot reload watcher with custom configuration.
446    ///
447    /// # Errors
448    ///
449    /// Returns an error if the file watcher cannot be initialized.
450    ///
451    /// # Example
452    ///
453    /// ```no_run
454    /// use goud_engine::assets::{AssetServer, HotReloadConfig};
455    /// use std::time::Duration;
456    ///
457    /// let mut server = AssetServer::new();
458    /// let config = HotReloadConfig::new()
459    ///     .with_debounce(Duration::from_millis(200))
460    ///     .watch_extension("png")
461    ///     .watch_extension("json");
462    ///
463    /// let mut watcher = server.create_hot_reload_watcher_with_config(config).unwrap();
464    ///
465    /// // In game loop
466    /// loop {
467    ///     watcher.process_events(&mut server);
468    ///     // ... rest of game loop
469    /// }
470    /// ```
471    #[cfg(feature = "native")]
472    pub fn create_hot_reload_watcher_with_config(
473        &self,
474        config: HotReloadConfig,
475    ) -> notify::Result<HotReloadWatcher> {
476        HotReloadWatcher::with_config(self, config)
477    }
478}