Skip to main content

game_gem/
assets.rs

1//! Asset management system.
2//!
3//! Advantages over macroquad:
4//! - **Reference-counted handles** — assets are shared, not duplicated
5//! - **Hot-reload support** (on debug builds)
6//! - **Async loading** with progress tracking
7//! - **Asset dependencies** — auto-load dependent assets
8//! - **Memory tracking** — see how much VRAM/assets are loaded
9//!
10//! In this module, we define the asset management architecture.
11//! The actual loading depends on the rendering backend (miniquad textures, etc.).
12
13use std::collections::HashMap;
14use std::sync::Arc;
15
16// ─────────────────────────────────────────────
17// Asset Handle
18// ─────────────────────────────────────────────
19
20/// A reference-counted handle to a loaded asset.
21///
22/// Cheap to clone — all clones point to the same loaded data.
23#[derive(Debug, Clone)]
24pub struct AssetHandle<T> {
25    inner: Arc<AssetInner<T>>,
26}
27
28#[derive(Debug)]
29struct AssetInner<T> {
30    /// The loaded asset data.
31    data: T,
32    /// Path this was loaded from.
33    path: String,
34    /// Whether this asset has been modified (for hot-reload).
35    #[allow(dead_code)]
36    modified: bool,
37}
38
39impl<T> AssetHandle<T> {
40    /// Get a reference to the asset data.
41    pub fn get(&self) -> &T {
42        &self.inner.data
43    }
44
45    /// Get the path this asset was loaded from.
46    pub fn path(&self) -> &str {
47        &self.inner.path
48    }
49
50    /// Strong reference count.
51    pub fn ref_count(&self) -> usize {
52        Arc::strong_count(&self.inner)
53    }
54}
55
56impl<T> std::ops::Deref for AssetHandle<T> {
57    type Target = T;
58    fn deref(&self) -> &Self::Target {
59        &self.inner.data
60    }
61}
62
63// ─────────────────────────────────────────────
64// Asset type IDs
65// ─────────────────────────────────────────────
66
67/// Identifiers for different asset types.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum AssetType {
70    /// Raw pixel data (before GPU upload).
71    Image,
72    /// GPU texture (after upload to the rendering backend).
73    Texture,
74    /// Font data.
75    Font,
76    /// Audio clip.
77    Sound,
78    /// Text/shader/JSON file.
79    Text,
80    /// Serialized data (JSON, RON, etc.).
81    Data,
82}
83
84// ─────────────────────────────────────────────
85// Loading state
86// ─────────────────────────────────────────────
87
88/// Status of an asset load operation.
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub enum LoadStatus {
91    /// Not yet started loading.
92    NotLoaded,
93    /// Currently loading (with progress 0.0–1.0).
94    Loading(f32),
95    /// Successfully loaded.
96    Loaded,
97    /// Failed to load.
98    Failed,
99}
100
101// ─────────────────────────────────────────────
102// Asset Loader
103// ─────────────────────────────────────────────
104
105/// Configuration for asset loading.
106#[derive(Debug, Clone)]
107pub struct AssetConfig {
108    /// Root directory for assets (default: "assets/").
109    pub root: String,
110    /// Whether to enable hot-reload (debug builds only).
111    pub hot_reload: bool,
112}
113
114impl Default for AssetConfig {
115    fn default() -> Self {
116        Self {
117            root: "assets".to_string(),
118            hot_reload: cfg!(debug_assertions),
119        }
120    }
121}
122
123/// The asset manager handles loading, caching, and lifecycle of game assets.
124///
125/// # Example
126/// ```
127/// let mut assets = AssetManager::new(AssetConfig::default());
128///
129/// // Load assets
130/// let player_tex = assets.load_image("sprites/player.png");
131/// let bg_music = assets.load_sound("music/background.ogg");
132///
133/// // Use assets
134/// // draw_texture(player_tex.unwrap(), ...);
135///
136/// // In update loop (for hot-reload):
137/// assets.check_hot_reload();
138/// ```
139pub struct AssetManager {
140    config: AssetConfig,
141    /// Cached loaded assets by normalized path.
142    images: HashMap<String, AssetHandle<ImageAsset>>,
143    /// Loading queue (reserved for future async loading).
144    #[allow(dead_code)]
145    loading_queue: Vec<String>,
146    /// Total bytes loaded.
147    total_bytes: usize,
148}
149
150/// Image asset (CPU-side pixel data).
151#[derive(Debug, Clone)]
152pub struct ImageAsset {
153    /// Pixel width.
154    pub width: u32,
155    /// Pixel height.
156    pub height: u32,
157    /// Raw RGBA pixel data.
158    pub pixels: Vec<u8>,
159    /// Format.
160    pub format: PixelFormat,
161}
162
163/// Pixel format.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum PixelFormat {
166    /// 8-bit RGBA (4 bytes per pixel).
167    Rgba8,
168    /// 8-bit RGB (3 bytes per pixel).
169    Rgb8,
170    /// 8-bit grayscale (1 byte per pixel).
171    Grayscale8,
172}
173
174impl ImageAsset {
175    /// Create an empty image with the given dimensions.
176    pub fn new(width: u32, height: u32) -> Self {
177        Self {
178            width,
179            height,
180            pixels: vec![0; (width * height * 4) as usize],
181            format: PixelFormat::Rgba8,
182        }
183    }
184
185    /// Get a pixel at (x, y). Returns (r, g, b, a) as u8.
186    pub fn get_pixel(&self, x: u32, y: u32) -> (u8, u8, u8, u8) {
187        let idx = ((y * self.width + x) * 4) as usize;
188        if idx + 3 >= self.pixels.len() {
189            return (0, 0, 0, 0);
190        }
191        (self.pixels[idx], self.pixels[idx + 1], self.pixels[idx + 2], self.pixels[idx + 3])
192    }
193
194    /// Set a pixel at (x, y).
195    pub fn set_pixel(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
196        let idx = ((y * self.width + x) * 4) as usize;
197        if idx + 3 < self.pixels.len() {
198            self.pixels[idx] = r;
199            self.pixels[idx + 1] = g;
200            self.pixels[idx + 2] = b;
201            self.pixels[idx + 3] = a;
202        }
203    }
204
205    /// Create a sub-image (region copy).
206    pub fn sub_image(&self, x: u32, y: u32, w: u32, h: u32) -> ImageAsset {
207        let mut sub = ImageAsset::new(w, h);
208        for py in 0..h {
209            for px in 0..w {
210                let (r, g, b, a) = self.get_pixel(x + px, y + py);
211                sub.set_pixel(px, py, r, g, b, a);
212            }
213        }
214        sub
215    }
216
217    /// Flip the image horizontally.
218    pub fn flip_horizontal(&mut self) {
219        let row_bytes = self.width as usize * 4;
220        for y in 0..self.height {
221            let row_start = (y as usize) * row_bytes;
222            let row: &mut [u8] = &mut self.pixels[row_start..row_start + row_bytes];
223            row.chunks_exact_mut(4).for_each(|pixel| pixel.reverse());
224        }
225    }
226
227    /// Flip the image vertically.
228    pub fn flip_vertical(&mut self) {
229        let row_bytes = self.width as usize * 4;
230        let total_rows = self.height as usize;
231        for y in 0..total_rows / 2 {
232            let top = y * row_bytes;
233            let bottom = (total_rows - 1 - y) * row_bytes;
234            let (top_slice, bottom_slice) =
235                self.pixels.split_at_mut(bottom);
236            let top_row = &mut top_slice[top..top + row_bytes];
237            let bottom_row = &mut bottom_slice[..row_bytes];
238            top_row.swap_with_slice(bottom_row);
239        }
240    }
241}
242
243impl AssetManager {
244    /// Create a new asset manager.
245    pub fn new(config: AssetConfig) -> Self {
246        Self {
247            config,
248            images: HashMap::new(),
249            loading_queue: Vec::new(),
250            total_bytes: 0,
251        }
252    }
253
254    /// Normalize a path (remove leading slashes, etc.).
255    fn normalize_path(&self, path: &str) -> String {
256        let path = path.trim_start_matches('/').trim_start_matches('\\');
257        format!("{}/{}", self.config.root, path)
258    }
259
260    /// Load an image from disk.
261    ///
262    /// Returns a handle to the cached image, or an error.
263    pub fn load_image(&mut self, path: &str) -> Result<AssetHandle<ImageAsset>, String> {
264        let normalized = self.normalize_path(path);
265
266        // Return cached if available
267        if let Some(handle) = self.images.get(&normalized) {
268            return Ok(handle.clone());
269        }
270
271        // In a real implementation, this would load from disk using the `image` crate.
272        // For the API definition, we create a placeholder.
273        let asset = ImageAsset::new(1, 1); // Placeholder
274        let bytes = asset.pixels.len();
275        self.total_bytes += bytes;
276
277        let handle = AssetHandle {
278            inner: Arc::new(AssetInner {
279                data: asset,
280                path: normalized.clone(),
281                modified: false,
282            }),
283        };
284
285        self.images.insert(normalized, handle.clone());
286        Ok(handle)
287    }
288
289    /// Unload an asset by path.
290    pub fn unload_image(&mut self, path: &str) {
291        let normalized = self.normalize_path(path);
292        if let Some(handle) = self.images.remove(&normalized) {
293            self.total_bytes -= handle.get().pixels.len();
294        }
295    }
296
297    /// Check for modified files and reload if hot-reload is enabled.
298    pub fn check_hot_reload(&mut self) {
299        if !self.config.hot_reload {
300            return;
301        }
302        // In a real implementation, check file modification times
303        // and reload changed assets.
304    }
305
306    /// Total bytes of loaded image data.
307    pub fn total_image_bytes(&self) -> usize {
308        self.total_bytes
309    }
310
311    /// Number of cached images.
312    pub fn image_count(&self) -> usize {
313        self.images.len()
314    }
315
316    /// Clear all cached assets.
317    pub fn clear(&mut self) {
318        self.images.clear();
319        self.total_bytes = 0;
320    }
321}
322
323// ─────────────────────────────────────────────
324// Sprite Sheet
325// ─────────────────────────────────────────────
326
327/// A sprite sheet splits a texture into a grid of frames.
328#[derive(Debug, Clone)]
329pub struct SpriteSheet {
330    /// Handle to the source image.
331    pub image: AssetHandle<ImageAsset>,
332    /// Number of columns in the sheet.
333    pub columns: u32,
334    /// Number of rows in the sheet.
335    pub rows: u32,
336    /// Individual frame dimensions (computed).
337    pub frame_width: f32,
338    pub frame_height: f32,
339}
340
341impl SpriteSheet {
342    /// Create a sprite sheet from an image handle and grid dimensions.
343    pub fn new(image: AssetHandle<ImageAsset>, columns: u32, rows: u32) -> Self {
344        let frame_width = image.width as f32 / columns as f32;
345        let frame_height = image.height as f32 / rows as f32;
346        Self {
347            image,
348            columns,
349            rows,
350            frame_width,
351            frame_height,
352        }
353    }
354
355    /// Get the source rectangle for a specific frame.
356    pub fn frame_rect(&self, index: u32) -> crate::math::Rect {
357        let col = index % self.columns;
358        let row = index / self.columns;
359        crate::math::Rect::new(
360            col as f32 * self.frame_width,
361            row as f32 * self.frame_height,
362            self.frame_width,
363            self.frame_height,
364        )
365    }
366
367    /// Get the source rectangle for a specific (row, column).
368    pub fn cell_rect(&self, row: u32, col: u32) -> crate::math::Rect {
369        crate::math::Rect::new(
370            col as f32 * self.frame_width,
371            row as f32 * self.frame_height,
372            self.frame_width,
373            self.frame_height,
374        )
375    }
376
377    /// Total number of frames.
378    pub fn frame_count(&self) -> u32 {
379        self.columns * self.rows
380    }
381}