Skip to main content

ds_rom/rom/raw/
banner.rs

1use std::{borrow::Cow, fmt::Display, ops::Range};
2
3use bitfield_struct::bitfield;
4use bytemuck::{Pod, PodCastError, Zeroable};
5use serde::{Deserialize, Serialize};
6use snafu::{Backtrace, Snafu};
7
8use super::RawHeaderError;
9use crate::str::Unicode16Array;
10
11/// Banner for displaying an icon and title on the home menu. This is the raw struct, see the plain one [here](super::super::Banner).
12pub struct Banner<'a> {
13    version: BannerVersion,
14    data: Cow<'a, [u8]>,
15}
16
17/// Errors related to [`Banner`].
18#[derive(Debug, Snafu)]
19pub enum RawBannerError {
20    /// See [`RawHeaderError`].
21    #[snafu(transparent)]
22    RawHeader {
23        /// Source error.
24        source: RawHeaderError,
25    },
26    /// Occurs when the input banner has an unknown version. Should not occur unless there's an undocumented banner version
27    /// we're unaware of.
28    #[snafu(display("unknown banner version {version}:\n{backtrace}"))]
29    UnknownVersion {
30        /// Input banner version.
31        version: u16,
32        /// Backtrace to the source of the error.
33        backtrace: Backtrace,
34    },
35    /// Occurs when the input is not the right size according to its version number.
36    #[snafu(display("banner version {version:x} must be {expected} bytes but got {actual} bytes"))]
37    InvalidSize {
38        /// Version number.
39        version: u16,
40        /// Expected size for this version.
41        expected: usize,
42        /// Actual input size.
43        actual: usize,
44        /// Backtrace to the source of the error.
45        backtrace: Backtrace,
46    },
47    /// Occurs when the input is less aligned than the banner
48    #[snafu(display("expected {expected}-alignment but got {actual}-alignment:\n{backtrace}"))]
49    Misaligned {
50        /// Expected alignment.
51        expected: usize,
52        /// Actual alignment.
53        actual: usize,
54        /// Backtrace to the source of the error.
55        backtrace: Backtrace,
56    },
57}
58
59impl<'a> Banner<'a> {
60    /// Creates a new [`Banner`].
61    pub fn new(version: BannerVersion) -> Self {
62        let size = version.banner_size();
63        let mut data = vec![0u8; size];
64        data[0..2].copy_from_slice(&(version as u16).to_le_bytes());
65        Self { version, data: data.into() }
66    }
67
68    fn handle_pod_cast<T>(result: Result<T, PodCastError>) -> T {
69        match result {
70            Ok(build_info) => build_info,
71            Err(PodCastError::TargetAlignmentGreaterAndInputNotAligned) => unreachable!(),
72            Err(PodCastError::AlignmentMismatch) => panic!(),
73            Err(PodCastError::OutputSliceWouldHaveSlop) => panic!(),
74            Err(PodCastError::SizeMismatch) => unreachable!(),
75        }
76    }
77
78    /// Reinterprets a `&[u8]` as a reference to [`Banner`].
79    ///
80    /// # Errors
81    ///
82    /// This function will return an error if the input has an unknown banner version, or has the wrong size for its version,
83    /// or is not aligned enough.
84    pub fn borrow_from_slice(data: &'a [u8]) -> Result<Self, RawBannerError> {
85        let addr = data as *const [u8] as *const () as usize;
86        if !addr.is_multiple_of(2) {
87            return MisalignedSnafu { expected: 2usize, actual: 1usize << addr.trailing_zeros() as usize }.fail();
88        }
89
90        let version_value = u16::from_le_bytes([data[0], data[1]]);
91        let Some(version) = BannerVersion::from_u16(version_value) else {
92            return UnknownVersionSnafu { version: version_value }.fail();
93        };
94        let size = version.banner_size();
95        if data.len() < size {
96            return InvalidSizeSnafu { version: version_value, expected: size, actual: data.len() }.fail();
97        }
98        let data = &data[..size];
99
100        let mut bitmap = [0u8; 0x200];
101        bitmap.copy_from_slice(&data[0x20..0x220]);
102
103        let mut palette = [0u16; 16];
104        for i in 0..16 {
105            palette[i] = u16::from_le_bytes([data[0x220 + i * 2], data[0x221 + i * 2]]);
106        }
107
108        Ok(Self { version, data: Cow::Borrowed(data) })
109    }
110
111    /// Returns the version of this [`Banner`].
112    pub fn version(&self) -> BannerVersion {
113        self.version
114    }
115
116    /// Returns the CRC checksum at the given index.
117    pub fn crc(&self, index: usize) -> u16 {
118        u16::from_le_bytes([self.data[2 + index * 2], self.data[3 + index * 2]])
119    }
120
121    /// Returns a mutable CRC checksum at the given index.
122    pub fn crc_mut(&mut self, index: usize) -> &mut u16 {
123        let start = 2 + index * 2;
124        let end = start + 2;
125        let data = &mut self.data.to_mut()[start..end];
126        Self::handle_pod_cast(bytemuck::try_from_bytes_mut(data))
127    }
128
129    /// Returns a reference to the bitmap of this [`Banner`].
130    pub fn bitmap(&self) -> &BannerBitmap {
131        let data = &self.data[0x20..0x220];
132        Self::handle_pod_cast(bytemuck::try_from_bytes(data))
133    }
134
135    /// Returns a mutable reference to the bitmap of this [`Banner`].
136    pub fn bitmap_mut(&mut self) -> &mut BannerBitmap {
137        let data = &mut self.data.to_mut()[0x20..0x220];
138        Self::handle_pod_cast(bytemuck::try_from_bytes_mut(data))
139    }
140
141    /// Returns a reference to the palette of this [`Banner`].
142    pub fn palette(&self) -> &BannerPalette {
143        let data = &self.data[0x220..0x240];
144        Self::handle_pod_cast(bytemuck::try_from_bytes(data))
145    }
146
147    /// Returns a mutable reference to the palette of this [`Banner`].
148    pub fn palette_mut(&mut self) -> &mut BannerPalette {
149        let data = &mut self.data.to_mut()[0x220..0x240];
150        Self::handle_pod_cast(bytemuck::try_from_bytes_mut(data))
151    }
152
153    /// Returns a title for the given language, or `None` the language is not supported by this banner version.
154    pub fn title(&self, language: Language) -> Option<&Unicode16Array<0x80>> {
155        if !self.version.supports_language(language) {
156            None
157        } else {
158            let start = 0x240 + language as usize * 0x100;
159            let end = start + 0x100;
160            let data = &self.data[start..end];
161            Some(Self::handle_pod_cast(bytemuck::try_from_bytes(data)))
162        }
163    }
164
165    /// Returns a mutable title for the given language, or `None` the language is not supported by this banner version.
166    pub fn title_mut(&mut self, language: Language) -> Option<&mut Unicode16Array<0x80>> {
167        if !self.version.supports_language(language) {
168            None
169        } else {
170            let start = 0x240 + language as usize * 0x100;
171            let end = start + 0x100;
172            let data = &mut self.data.to_mut()[start..end];
173            Some(Self::handle_pod_cast(bytemuck::try_from_bytes_mut(data)))
174        }
175    }
176
177    /// Returns a reference to the animation of this [`Banner`], if it exists in this banner version.
178    pub fn animation(&self) -> Option<&BannerAnimation> {
179        if !self.version.has_animation() {
180            None
181        } else {
182            let data = &self.data[0x1240..0x23c0];
183            Some(Self::handle_pod_cast(bytemuck::try_from_bytes(data)))
184        }
185    }
186
187    /// Returns a mutable reference to the animation of this [`Banner`], if it exists in this banner version.
188    pub fn animation_mut(&mut self) -> Option<&mut BannerAnimation> {
189        if !self.version.has_animation() {
190            None
191        } else {
192            let data = &mut self.data.to_mut()[0x1240..0x23c0];
193            Some(Self::handle_pod_cast(bytemuck::try_from_bytes_mut(data)))
194        }
195    }
196
197    /// Returns a reference to the full data of this [`Banner`].
198    pub fn full_data(&self) -> &[u8] {
199        &self.data
200    }
201
202    /// Creates a [`DisplayBanner`] which implements [`Display`].
203    pub fn display(&self, indent: usize) -> DisplayBanner<'_> {
204        DisplayBanner { banner: self, indent }
205    }
206}
207
208/// Can be used to display values inside [`Banner`].
209pub struct DisplayBanner<'a> {
210    banner: &'a Banner<'a>,
211    indent: usize,
212}
213
214macro_rules! write_title {
215    ($f:ident, $fmt:literal, $banner:ident, $language:expr) => {
216        if let Some(title) = $banner.title($language) {
217            writeln!($f, $fmt, '\n', title, '\n')
218        } else {
219            Ok(())
220        }
221    };
222}
223
224impl Display for DisplayBanner<'_> {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        let i = " ".repeat(self.indent);
227        let banner = &self.banner;
228        writeln!(f, "{i}Version ......... : {}", banner.version)?;
229        writeln!(f, "{i}Original CRC .... : {:#x}", banner.crc(BannerVersion::Original.crc_index()))?;
230        write_title!(f, "{i}Japanese Title .. : {}{}{}", banner, Language::Japanese)?;
231        write_title!(f, "{i}English Title ... : {}{}{}", banner, Language::English)?;
232        write_title!(f, "{i}French Title .... : {}{}{}", banner, Language::French)?;
233        write_title!(f, "{i}German Title .... : {}{}{}", banner, Language::German)?;
234        write_title!(f, "{i}Italian Title ... : {}{}{}", banner, Language::Italian)?;
235        write_title!(f, "{i}Spanish Title ... : {}{}{}", banner, Language::Spanish)?;
236        if banner.version >= BannerVersion::China {
237            writeln!(f, "{i}China CRC ....... : {:#x}", banner.crc(BannerVersion::China.crc_index()))?;
238            write_title!(f, "{i}Chinese Title ... : {}{}{}", banner, Language::Chinese)?;
239        }
240        if banner.version >= BannerVersion::Korea {
241            writeln!(f, "{i}Korea CRC ....... : {:#x}", banner.crc(BannerVersion::Korea.crc_index()))?;
242            write_title!(f, "{i}Korean Title .... : {}{}{}", banner, Language::Korean)?;
243        }
244        if banner.version >= BannerVersion::Animated {
245            writeln!(f, "{i}Animation CRC ... : {:#x}", banner.crc(BannerVersion::Animated.crc_index()))?;
246        }
247        writeln!(f, "{i}Palette ......... :\n{}", banner.palette())?;
248        writeln!(f, "{i}Bitmap .......... :\n{}", banner.bitmap().display(banner.palette()))?;
249        Ok(())
250    }
251}
252
253/// Known banner versions.
254#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize, Default)]
255pub enum BannerVersion {
256    /// Original version with titles in Japanese, English, French, German, Italian and Spanish.
257    #[default]
258    Original = 1,
259    /// Inherits from [`BannerVersion::Original`] and adds Chinese.
260    China = 2,
261    /// Inherits from [`BannerVersion::China`] and adds Korean.
262    Korea = 3,
263    /// Inherits from [`BannerVersion::Korea`] and adds an animated icon.
264    Animated = 0x103,
265}
266
267impl BannerVersion {
268    fn from_u16(value: u16) -> Option<Self> {
269        match value {
270            1 => Some(Self::Original),
271            2 => Some(Self::China),
272            3 => Some(Self::Korea),
273            0x103 => Some(Self::Animated),
274            _ => None,
275        }
276    }
277
278    /// Returns whether this version has a Chinese title.
279    pub fn has_chinese(self) -> bool {
280        self >= Self::China
281    }
282
283    /// Returns whether this version has a Korean title.
284    pub fn has_korean(self) -> bool {
285        self >= Self::Korea
286    }
287
288    /// Returns whether this version has an animated icon.
289    pub fn has_animation(self) -> bool {
290        self >= Self::Animated
291    }
292
293    /// Returns whether this version supports the given language.
294    pub fn supports_language(self, language: Language) -> bool {
295        match language {
296            Language::Japanese => true,
297            Language::English => true,
298            Language::French => true,
299            Language::German => true,
300            Language::Italian => true,
301            Language::Spanish => true,
302            Language::Chinese => self.has_chinese(),
303            Language::Korean => self.has_korean(),
304        }
305    }
306
307    /// Returns the CRC index of this version.
308    pub fn crc_index(self) -> usize {
309        match self {
310            BannerVersion::Original => 0,
311            BannerVersion::China => 1,
312            BannerVersion::Korea => 2,
313            BannerVersion::Animated => 3,
314        }
315    }
316
317    /// Returns the CRC checksum range of this version.
318    pub fn crc_range(self) -> Range<usize> {
319        match self {
320            BannerVersion::Original => 0x20..0x840,
321            BannerVersion::China => 0x20..0x940,
322            BannerVersion::Korea => 0x20..0xa40,
323            BannerVersion::Animated => 0x1240..0x23c0,
324        }
325    }
326
327    /// Returns the banner size of this version.
328    pub fn banner_size(self) -> usize {
329        match self {
330            BannerVersion::Original => 0x840,
331            BannerVersion::China => 0x940,
332            BannerVersion::Korea => 0x1240,
333            BannerVersion::Animated => 0x23c0,
334        }
335    }
336}
337
338impl Display for BannerVersion {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        match self {
341            BannerVersion::Original => write!(f, "Original"),
342            BannerVersion::China => write!(f, "China"),
343            BannerVersion::Korea => write!(f, "Korea"),
344            BannerVersion::Animated => write!(f, "DSi Animated"),
345        }
346    }
347}
348
349/// Languages present in the banner.
350#[derive(Clone, Copy, Debug)]
351pub enum Language {
352    /// Japanese.
353    Japanese = 0,
354    /// English.
355    English = 1,
356    /// French.
357    French = 2,
358    /// German.
359    German = 3,
360    /// Italian.
361    Italian = 4,
362    /// Spanish.
363    Spanish = 5,
364    /// Chinese.
365    Chinese = 6,
366    /// Korean.
367    Korean = 7,
368}
369
370impl Display for Language {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        match self {
373            Language::Japanese => write!(f, "Japanese"),
374            Language::English => write!(f, "English"),
375            Language::French => write!(f, "French"),
376            Language::German => write!(f, "German"),
377            Language::Italian => write!(f, "Italian"),
378            Language::Spanish => write!(f, "Spanish"),
379            Language::Chinese => write!(f, "Chinese"),
380            Language::Korean => write!(f, "Korean"),
381        }
382    }
383}
384
385/// Contains a palette for a banner bitmap, where each color is 15-bit BGR.
386#[repr(C)]
387#[derive(Clone, Copy, Zeroable, Pod, Default)]
388pub struct BannerPalette(pub [u16; 16]);
389
390impl BannerPalette {
391    /// Returns the color from 32-bit `[r, g, b, a]` at the given index.
392    pub fn get_color(&self, index: usize) -> [u8; 4] {
393        if index < self.0.len() {
394            let color = self.0[index];
395            let b = (((color >> 10) & 31) << 3) as u8;
396            let g = (((color >> 5) & 31) << 3) as u8;
397            let r = ((color & 31) << 3) as u8;
398            let a = if index > 0 { u8::MAX } else { 0 };
399            [r, g, b, a]
400        } else {
401            [0, 0, 0, 0]
402        }
403    }
404
405    /// Sets the color from 24-bit `(r, g, b)` at the given index.
406    pub fn set_color(&mut self, index: usize, r: u8, g: u8, b: u8) {
407        let r = r as u16 >> 3;
408        let g = g as u16 >> 3;
409        let b = b as u16 >> 3;
410        let color = r | (g << 5) | (b << 10);
411        self.0[index] = color;
412    }
413}
414
415impl Display for BannerPalette {
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        for i in 0..16 {
418            let [r, g, b, a] = self.get_color(i);
419            if a == 0 {
420                write!(f, " ")?;
421            } else {
422                write!(f, "\x1b[38;2;{r};{g};{b}m█")?;
423            }
424        }
425        write!(f, "\x1b[0m")
426    }
427}
428
429/// A bitmap in the banner.
430#[repr(C)]
431#[derive(Clone, Copy, Zeroable, Pod)]
432pub struct BannerBitmap(pub [u8; 0x200]);
433
434impl BannerBitmap {
435    /// Creates a [`DisplayBannerBitmap`] which implements [`Display`].
436    pub fn display<'a>(&'a self, palette: &'a BannerPalette) -> DisplayBannerBitmap<'a> {
437        DisplayBannerBitmap { bitmap: self, palette }
438    }
439
440    fn get_index(x: usize, y: usize) -> (usize, usize) {
441        // 8x8 pixel tiles in a 4x4 grid
442        let index = (y / 8 * 0x80) + (x / 8 * 0x20) + (y % 8 * 4) + (x / 2 % 4);
443        // 4 bits per pixel
444        let offset = (x % 2) * 4;
445        (index, offset)
446    }
447
448    /// Gets a palette index at the given coordinates.
449    pub fn get_pixel(&self, x: usize, y: usize) -> usize {
450        let (index, offset) = Self::get_index(x, y);
451        if index < self.0.len() {
452            (self.0[index] as usize >> offset) & 0xf
453        } else {
454            0
455        }
456    }
457
458    /// Sets a palette index at the given coordinates.
459    pub fn set_pixel(&mut self, x: usize, y: usize, value: u8) {
460        let (index, offset) = Self::get_index(x, y);
461        if index < self.0.len() {
462            self.0[index] = (self.0[index] & !(0xf << offset)) | (value << offset);
463        }
464    }
465}
466
467impl Default for BannerBitmap {
468    fn default() -> Self {
469        Self([0; 0x200])
470    }
471}
472
473/// Can be used to display a [`BannerBitmap`].
474pub struct DisplayBannerBitmap<'a> {
475    bitmap: &'a BannerBitmap,
476    palette: &'a BannerPalette,
477}
478
479impl Display for DisplayBannerBitmap<'_> {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        for y in (0..32).step_by(2) {
482            for x in 0..32 {
483                let [tr, tg, tb, ta] = self.palette.get_color(self.bitmap.get_pixel(x, y));
484                let [br, bg, bb, ba] = self.palette.get_color(self.bitmap.get_pixel(x, y + 1));
485
486                match (ta, ba) {
487                    (0, 0) => write!(f, " ")?,
488                    (_, 0) => write!(f, "\x1b[38;2;{tr};{tg};{tb}m▀")?,
489                    (0, _) => write!(f, "\x1b[38;2;{tr};{tg};{tb}m▄")?,
490                    (_, _) => write!(f, "\x1b[38;2;{tr};{tg};{tb}m\x1b[48;2;{br};{bg};{bb}m▀")?,
491                }
492            }
493            writeln!(f, "\x1b[0m")?;
494        }
495        Ok(())
496    }
497}
498
499/// An animated banner icon.
500#[repr(C)]
501#[derive(Clone, Copy, Zeroable, Pod)]
502pub struct BannerAnimation {
503    /// Up to 8 bitmaps.
504    pub bitmaps: [BannerBitmap; 8],
505    /// Up to 8 palettes.
506    pub palettes: [BannerPalette; 8],
507    /// Up to 64 keyframes.
508    pub keyframes: [BannerKeyframe; 64],
509}
510
511/// A keyframe for [`BannerAnimation`].
512#[bitfield(u16)]
513pub struct BannerKeyframe {
514    /// How long to show this keyframe for, in frames.
515    pub frame_duration: u8,
516    /// Which of the 8 bitmaps to show.
517    #[bits(3)]
518    pub bitmap_index: u8,
519    /// Which of the 8 palettes to use.
520    #[bits(3)]
521    pub palette_index: u8,
522    /// Flips the bitmap horizontally.
523    pub flip_horizontally: bool,
524    /// Flips the bitmap vertically.
525    pub flip_vertically: bool,
526}
527
528unsafe impl Zeroable for BannerKeyframe {}
529unsafe impl Pod for BannerKeyframe {}