Skip to main content

banished_ui/
resources.rs

1//! The interface's resources from the game's packages, each loaded once: sprite sheets, fonts,
2//! element descriptions and string tables.
3
4use std::collections::HashMap;
5use std::rc::Rc;
6
7use banished_formats::pkg::Archive;
8use banished_formats::types;
9use banished_formats::ui::{self, Element, Font, SpriteSheet};
10
11/// Index of a sprite sheet in [`Resources::sheets`].
12pub type SheetId = usize;
13/// The index of a font in [`Resources::fonts`].
14pub type FontId = usize;
15
16#[derive(Debug, Clone, Copy, PartialEq)]
17/// One sprite: its sheet, its size and its place on the sheet.
18pub struct SpriteRef {
19    /// The sheet.
20    pub sheet: SheetId,
21    /// The width in pixels.
22    pub width: i32,
23    /// The height in pixels.
24    pub height: i32,
25    /// The texture coordinates on the sheet: u0, v0, u1, v1.
26    pub uv: [f32; 4],
27}
28
29/// A loaded sprite sheet.
30pub struct LoadedSheet {
31    /// The sheet's resource reference.
32    pub reference: String,
33    /// The sheet: its sprites by name.
34    pub sheet: SpriteSheet,
35    /// `ImageBuffer` reference holding the sheet image (the material's `diffuse` slot).
36    pub image: Option<String>,
37    /// The material instance's base material (e.g. `Material\UI\Logo.rsc:resource`), which
38    /// selects the shader. Most sheets use the plain UI material.
39    pub shader: Option<String>,
40    /// An image made at run time (`width`, `height`, RGBA) instead of `image`: a save's
41    /// preview, which the Save and Load dialogs wrap in a sheet of their own.
42    pub pixels: Option<Rc<(u32, u32, Vec<u8>)>>,
43}
44
45/// A loaded font.
46pub struct LoadedFont {
47    /// The font: its glyphs and metrics.
48    pub font: Font,
49    /// The sprite sheet of its glyphs.
50    pub sheet: Option<SheetId>,
51    /// Pixels from the line top to the baseline.
52    pub ascent: i32,
53}
54
55/// The resource cache.
56pub struct Resources {
57    /// The game's packages.
58    pub archive: Rc<Archive>,
59    elements: HashMap<String, Option<Rc<Element>>>,
60    sheet_ids: HashMap<String, Option<SheetId>>,
61    /// The loaded sprite sheets.
62    pub sheets: Vec<LoadedSheet>,
63    font_ids: HashMap<String, Option<FontId>>,
64    /// The loaded fonts.
65    pub fonts: Vec<LoadedFont>,
66    tables: HashMap<String, Rc<Option<HashMap<String, String>>>>,
67    /// The same tables in serialized order, for index lookups.
68    ordered: HashMap<String, Rc<Vec<(String, String)>>>,
69}
70
71impl Resources {
72    /// An empty cache over `archive`.
73    pub fn new(archive: Rc<Archive>) -> Self {
74        Resources {
75            archive,
76            elements: HashMap::new(),
77            sheet_ids: HashMap::new(),
78            sheets: Vec::new(),
79            font_ids: HashMap::new(),
80            fonts: Vec::new(),
81            tables: HashMap::new(),
82            ordered: HashMap::new(),
83        }
84    }
85
86    /// An element description by reference.
87    pub fn element(&mut self, reference: &str) -> Option<Rc<Element>> {
88        if let Some(e) = self.elements.get(reference) {
89            return e.clone();
90        }
91        let e = self
92            .archive
93            .read_ref(reference)
94            .and_then(|d| ui::element(&d))
95            .map_err(|e| log::warn!("{reference}: {e}"))
96            .ok()
97            .map(Rc::new);
98        self.elements.insert(reference.to_string(), e.clone());
99        e
100    }
101
102    /// A sprite sheet by reference.
103    pub fn sheet(&mut self, reference: &str) -> Option<SheetId> {
104        if let Some(id) = self.sheet_ids.get(reference) {
105            return *id;
106        }
107        let id =
108            self.archive
109                .read_ref(reference)
110                .and_then(|d| ui::sprite_sheet(&d))
111                .map_err(|e| log::warn!("{reference}: {e}"))
112                .ok()
113                .map(|sheet| {
114                    let mi = sheet.material.as_deref().and_then(|m| {
115                        types::material_instance(&self.archive.read_ref(m).ok()?).ok()
116                    });
117                    let image = mi
118                        .as_ref()
119                        .and_then(|mi| mi.texture("diffuse").map(str::to_owned));
120                    let shader = mi.and_then(|mi| mi.material);
121                    self.sheets.push(LoadedSheet {
122                        reference: reference.to_string(),
123                        sheet,
124                        image,
125                        shader,
126                        pixels: None,
127                    });
128                    self.sheets.len() - 1
129                });
130        self.sheet_ids.insert(reference.to_string(), id);
131        id
132    }
133
134    /// A sheet with one sprite, around an image made at run time, for example a save's preview
135    /// picture. `key` names it: the same key gives the same sheet, with the image it was first
136    /// made with.
137    ///
138    /// Original: `FUN_1400bc1b0` (a `UI::SpriteSheet` with `AddSpriteInfo(0, 0, w, h)`).
139    pub fn image_sheet(&mut self, key: &str, width: u32, height: u32, rgba: Vec<u8>) -> SpriteRef {
140        let reference = format!("image:{key}");
141        let sheet = match self.sheet_ids.get(&reference) {
142            Some(Some(id)) => *id,
143            _ => {
144                self.sheets.push(LoadedSheet {
145                    reference: reference.clone(),
146                    sheet: SpriteSheet {
147                        material_path: String::new(),
148                        source_image: String::new(),
149                        width: width as i32,
150                        height: height as i32,
151                        unknown_flag: false,
152                        sprites: Vec::new(),
153                        material: None,
154                    },
155                    image: None,
156                    shader: None,
157                    pixels: Some(Rc::new((width, height, rgba))),
158                });
159                let id = self.sheets.len() - 1;
160                self.sheet_ids.insert(reference, Some(id));
161                id
162            }
163        };
164        SpriteRef {
165            sheet,
166            width: width as i32,
167            height: height as i32,
168            uv: [0.0, 0.0, 1.0, 1.0],
169        }
170    }
171
172    /// A sprite by its sheet's reference and its name.
173    pub fn sprite(&mut self, sheet_ref: Option<&str>, name: &str) -> Option<SpriteRef> {
174        if name.is_empty() {
175            return None;
176        }
177        let sheet = self.sheet(sheet_ref?)?;
178        let s = self.sheets[sheet].sheet.sprite(name)?;
179        Some(SpriteRef {
180            sheet,
181            width: s.width,
182            height: s.height,
183            uv: s.uv,
184        })
185    }
186
187    /// A font by reference.
188    pub fn font(&mut self, reference: &str) -> Option<FontId> {
189        if let Some(id) = self.font_ids.get(reference) {
190            return *id;
191        }
192        let id = self
193            .archive
194            .read_ref(reference)
195            .and_then(|d| ui::font(&d))
196            .map_err(|e| log::warn!("{reference}: {e}"))
197            .ok()
198            .map(|font| {
199                let sheet = font.sprite_sheet.as_deref().and_then(|s| self.sheet(s));
200                // TextElement::Draw uses the serialized descent, not the tallest bitmap.
201                let ascent = font.baseline_offset();
202                self.fonts.push(LoadedFont {
203                    font,
204                    sheet,
205                    ascent,
206                });
207                self.fonts.len() - 1
208            });
209        self.font_ids.insert(reference.to_string(), id);
210        id
211    }
212
213    /// `UI::String(table, index)`: entry `index` of a string table in serialized order
214    /// (`StringTable::GetIndexFromName` searches that order). Empty when out of range.
215    pub fn string_at(&mut self, table: &str, index: i32) -> String {
216        let list = match self.ordered.get(table) {
217            Some(l) => l.clone(),
218            None => {
219                let l = Rc::new(
220                    self.archive
221                        .read_ref(table)
222                        .and_then(|d| ui::string_table(&d))
223                        .unwrap_or_default(),
224                );
225                self.ordered.insert(table.to_string(), l.clone());
226                l
227            }
228        };
229        usize::try_from(index)
230            .ok()
231            .and_then(|i| list.get(i))
232            .map(|(_, v)| v.clone())
233            .unwrap_or_default()
234    }
235
236    /// Resolve a string-table key; falls back to the key itself.
237    pub fn string(&mut self, table: Option<&str>, key: &str) -> String {
238        let Some(table) = table else {
239            return key.to_string();
240        };
241        let map = match self.tables.get(table) {
242            Some(m) => m.clone(),
243            None => {
244                let m: Option<HashMap<String, String>> = self
245                    .archive
246                    .read_ref(table)
247                    .and_then(|d| ui::string_table(&d))
248                    .map(|v| v.into_iter().collect())
249                    .ok();
250                let m = Rc::new(m);
251                self.tables.insert(table.to_string(), m.clone());
252                m
253            }
254        };
255        // `StringTable::GetText(name)`: a name the table lacks is an empty string
256        // (`GetIndexFromName` −1); without a table (none loaded) the name is the text.
257        match map.as_ref() {
258            Some(m) => m.get(key).cloned().unwrap_or_default(),
259            None => key.to_string(),
260        }
261    }
262}