Skip to main content

siglus_scene_vm/
image_manager.rs

1use std::collections::HashMap;
2use std::hash::{Hash, Hasher};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::assets::{load_image_any, RgbaImage};
7use anyhow::{Context, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct ImageId(pub u32);
11
12impl ImageId {
13    pub fn index(self) -> usize {
14        self.0 as usize
15    }
16}
17
18#[derive(Debug, Clone)]
19struct ImageKey {
20    path: PathBuf,
21    frame_index: usize,
22}
23
24impl PartialEq for ImageKey {
25    fn eq(&self, other: &Self) -> bool {
26        self.path == other.path && self.frame_index == other.frame_index
27    }
28}
29
30impl Eq for ImageKey {}
31
32impl Hash for ImageKey {
33    fn hash<H: Hasher>(&self, state: &mut H) {
34        self.path.hash(state);
35        self.frame_index.hash(state);
36    }
37}
38
39#[derive(Debug)]
40pub struct ImageManager {
41    project_dir: PathBuf,
42    current_append_dir: String,
43    key_to_id: HashMap<ImageKey, ImageId>,
44    solid_to_id: HashMap<(u8, u8, u8, u8), ImageId>,
45    images: Vec<ImageEntry>,
46}
47
48#[derive(Debug, Clone)]
49struct ImageEntry {
50    img: Arc<RgbaImage>,
51    version: u64,
52}
53
54#[derive(Debug, Clone)]
55pub struct DebugImageInfo {
56    pub id: ImageId,
57    pub width: u32,
58    pub height: u32,
59    pub version: u64,
60    pub source_path: Option<PathBuf>,
61    pub frame_index: Option<usize>,
62}
63
64impl ImageManager {
65    pub fn new(project_dir: PathBuf) -> Self {
66        Self {
67            project_dir,
68            current_append_dir: String::new(),
69            key_to_id: HashMap::new(),
70            solid_to_id: HashMap::new(),
71            images: Vec::new(),
72        }
73    }
74
75    pub fn project_dir(&self) -> &Path {
76        &self.project_dir
77    }
78
79    pub fn current_append_dir(&self) -> &str {
80        &self.current_append_dir
81    }
82
83    pub fn set_current_append_dir(&mut self, append_dir: impl Into<String>) {
84        self.current_append_dir = append_dir.into();
85    }
86
87    pub fn get(&self, id: ImageId) -> Option<&Arc<RgbaImage>> {
88        self.images.get(id.index()).map(|e| &e.img)
89    }
90
91    pub fn get_entry(&self, id: ImageId) -> Option<(&Arc<RgbaImage>, u64)> {
92        self.images.get(id.index()).map(|e| (&e.img, e.version))
93    }
94
95    /// Create a 1x1 solid RGBA image and return its image id.
96    ///
97    /// This is used for UI placeholders (e.g. message window background) until
98    /// full UI skinning is implemented.
99    pub fn solid_rgba(&mut self, rgba: (u8, u8, u8, u8)) -> ImageId {
100        if let Some(id) = self.solid_to_id.get(&rgba) {
101            return *id;
102        }
103        let img = RgbaImage {
104            width: 1,
105            height: 1,
106            center_x: 0,
107            center_y: 0,
108            rgba: vec![rgba.0, rgba.1, rgba.2, rgba.3],
109        };
110        let id = ImageId(self.images.len() as u32);
111        self.images.push(ImageEntry {
112            img: Arc::new(img),
113            version: 0,
114        });
115        self.solid_to_id.insert(rgba, id);
116        id
117    }
118
119    /// Load a BG resource by name (Siglus policy: g00/ then bg/, with extension fallback).
120    ///
121    /// BG is not animated in our current bring-up, so frame index is always 0.
122    pub fn load_bg(&mut self, name: &str) -> Result<ImageId> {
123        let (path, _ty) = crate::resource::find_bg_image_with_append_dir(
124            &self.project_dir,
125            &self.current_append_dir,
126            name,
127        )
128        .with_context(|| format!("find bg resource {name}"))?;
129        self.load_file(&path, 0)
130    }
131
132    /// Load a BG resource with an explicit frame index (kept for compatibility).
133    pub fn load_bg_frame(&mut self, name: &str, frame_index: usize) -> Result<ImageId> {
134        let (path, _ty) = crate::resource::find_bg_image_with_append_dir(
135            &self.project_dir,
136            &self.current_append_dir,
137            name,
138        )
139        .with_context(|| format!("find bg resource {name}"))?;
140        self.load_file(&path, frame_index)
141    }
142
143    /// Load an image restricted to the `g00/` directory (with extension fallback).
144    ///
145    /// Used for CHR / sprite image loading.
146    pub fn load_g00(&mut self, name: &str, frame_index: u32) -> Result<ImageId> {
147        let (path, _ty) = crate::resource::find_g00_image_with_append_dir(
148            &self.project_dir,
149            &self.current_append_dir,
150            name,
151        )
152        .with_context(|| format!("find g00 resource {name}"))?;
153        self.load_file(&path, frame_index as usize)
154    }
155
156    /// Load an image from an explicit path (relative to project_dir if not absolute).
157    pub fn load_file(&mut self, path: &Path, frame_index: usize) -> Result<ImageId> {
158        let resolved = if path.is_absolute() {
159            path.to_path_buf()
160        } else if path.is_file() {
161            // Resource lookup helpers return paths rooted at project_dir. When
162            // project_dir itself is relative, those paths are still relative
163            // (for example `testcase/g00/foo.g00`). Do not join project_dir a
164            // second time; the original engine passes the resolved resource
165            // path through unchanged after tnm_find_* succeeds.
166            path.to_path_buf()
167        } else {
168            self.project_dir.join(path)
169        };
170
171        let key = ImageKey {
172            path: resolved.clone(),
173            frame_index,
174        };
175
176        if let Some(id) = self.key_to_id.get(&key) {
177            return Ok(*id);
178        }
179
180        let img = load_image_any(&resolved, frame_index)
181            .with_context(|| format!("load image {:?}", resolved))?;
182        let id = self.insert_image(img);
183        self.key_to_id.insert(key, id);
184        Ok(id)
185    }
186
187    /// Insert an already-decoded image into the manager and return a new ImageId.
188    pub fn insert_image(&mut self, img: RgbaImage) -> ImageId {
189        let id = ImageId(self.images.len() as u32);
190        self.images.push(ImageEntry {
191            img: Arc::new(img),
192            version: 0,
193        });
194        id
195    }
196
197    pub fn insert_image_arc(&mut self, img: Arc<RgbaImage>) -> ImageId {
198        let id = ImageId(self.images.len() as u32);
199        self.images.push(ImageEntry { img, version: 0 });
200        id
201    }
202
203    /// Replace an existing image in-place and bump its version.
204    ///
205    /// This allows the renderer to update the GPU texture without changing the ImageId.
206    pub fn replace_image(&mut self, id: ImageId, img: RgbaImage) -> Result<()> {
207        let Some(entry) = self.images.get_mut(id.index()) else {
208            anyhow::bail!("replace_image: invalid ImageId {}", id.index());
209        };
210        entry.img = Arc::new(img);
211        entry.version = entry.version.wrapping_add(1);
212        Ok(())
213    }
214
215    pub fn replace_image_arc(&mut self, id: ImageId, img: Arc<RgbaImage>) -> Result<()> {
216        let Some(entry) = self.images.get_mut(id.index()) else {
217            anyhow::bail!("replace_image_arc: invalid ImageId {}", id.index());
218        };
219        entry.img = img;
220        entry.version = entry.version.wrapping_add(1);
221        Ok(())
222    }
223
224    pub fn debug_image_info(&self, id: ImageId) -> Option<DebugImageInfo> {
225        let entry = self.images.get(id.index())?;
226        let mut source_path = None;
227        let mut frame_index = None;
228        for (key, key_id) in &self.key_to_id {
229            if *key_id == id {
230                source_path = Some(key.path.clone());
231                frame_index = Some(key.frame_index);
232                break;
233            }
234        }
235        Some(DebugImageInfo {
236            id,
237            width: entry.img.width,
238            height: entry.img.height,
239            version: entry.version,
240            source_path,
241            frame_index,
242        })
243    }
244}