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}