Skip to main content

siglus_assets/
g00.rs

1use anyhow::{Result, anyhow, bail};
2use image::ImageFormat;
3
4use crate::lzss::{lzss_unpack, lzss_unpack32};
5
6/// Fixed 256-byte XOR table used by Siglus G00 type 3 payloads.
7///
8/// Type 3 stores a JPEG stream after the 5-byte G00 header, with every
9/// payload byte XORed against this table cyclically from index 0.
10pub const G00_TYPE3_XOR_TABLE: [u8; 256] = [
11    0x45, 0x0C, 0x85, 0xC0, 0x75, 0x14, 0xE5, 0x5D, 0x8B, 0x55, 0xEC, 0xC0, 0x5B, 0x8B, 0xC3, 0x8B,
12    0x81, 0xFF, 0x00, 0x00, 0x04, 0x00, 0x85, 0xFF, 0x6A, 0x00, 0x76, 0xB0, 0x43, 0x00, 0x76, 0x49,
13    0x00, 0x8B, 0x7D, 0xE8, 0x8B, 0x75, 0xA1, 0xE0, 0x0C, 0x85, 0xC0, 0xC0, 0x75, 0x78, 0x30, 0x44,
14    0x00, 0x85, 0xFF, 0x76, 0x37, 0x81, 0x1D, 0xD0, 0xFF, 0x00, 0x00, 0x75, 0x44, 0x8B, 0xB0, 0x43,
15    0x45, 0xF8, 0x8D, 0x55, 0xFC, 0x52, 0x00, 0x76, 0x68, 0x00, 0x00, 0x04, 0x00, 0x6A, 0x43, 0x8B,
16    0xB1, 0x43, 0x00, 0x6A, 0x05, 0xFF, 0x50, 0xFF, 0xD3, 0xA1, 0xE0, 0x04, 0x00, 0x56, 0x15, 0x2C,
17    0x44, 0x00, 0x85, 0xC0, 0x74, 0x09, 0xC3, 0xA1, 0x5F, 0x5E, 0x33, 0x8B, 0xE5, 0x5D, 0xE0, 0x30,
18    0x04, 0x00, 0x81, 0xC6, 0x00, 0x00, 0x81, 0xEF, 0x04, 0x00, 0x85, 0x30, 0x44, 0x00, 0x00, 0x00,
19    0x5D, 0xC3, 0x8B, 0x55, 0xF8, 0x8D, 0x5E, 0x5B, 0x4D, 0xFC, 0x51, 0xC4, 0x04, 0x5F, 0x8B, 0xE5,
20    0x43, 0x00, 0xEB, 0xD8, 0x8B, 0x45, 0xFF, 0x15, 0xE8, 0x83, 0xC0, 0x57, 0x56, 0x52, 0x2C, 0xB1,
21    0x01, 0x00, 0x8B, 0x7D, 0xE8, 0x89, 0x00, 0xE8, 0x45, 0xF4, 0x8B, 0x20, 0x50, 0x6A, 0x47, 0x28,
22    0x00, 0x50, 0x53, 0xFF, 0x15, 0x34, 0xE4, 0x6A, 0xB1, 0x43, 0x00, 0x0C, 0x8B, 0x45, 0x00, 0x6A,
23    0x8B, 0x4D, 0xEC, 0x89, 0x08, 0x8A, 0x85, 0xC0, 0x45, 0xF0, 0x84, 0x8B, 0x45, 0x10, 0x74, 0x05,
24    0xF5, 0x28, 0x01, 0x00, 0x83, 0xC4, 0x52, 0x6A, 0x08, 0x89, 0x45, 0x83, 0xC2, 0x20, 0x00, 0xE8,
25    0xE8, 0xF4, 0xFB, 0xFF, 0xFF, 0x8B, 0x8B, 0x5D, 0x45, 0x0C, 0x83, 0xC0, 0x74, 0xC5, 0xF8, 0x53,
26    0xC4, 0x08, 0x85, 0xC0, 0x75, 0x56, 0x30, 0x44, 0x8B, 0x1D, 0xD0, 0xF0, 0xA1, 0xE0, 0x00, 0x83,
27];
28
29/// Apply/remove the G00 type 3 JPEG XOR layer in place.
30///
31/// XOR is symmetric, so this is used for both decoding and encoding.
32pub fn xor_type3_jpeg_in_place(buf: &mut [u8]) {
33    for (i, byte) in buf.iter_mut().enumerate() {
34        *byte ^= G00_TYPE3_XOR_TABLE[i & 0xFF];
35    }
36}
37
38/// Return a de-obfuscated JPEG payload from a G00 type 3 payload.
39pub fn decode_type3_jpeg_payload(payload: &[u8]) -> Vec<u8> {
40    let mut out = payload.to_vec();
41    xor_type3_jpeg_in_place(&mut out);
42    out
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum G00Type {
47    Type0,
48    Type1,
49    Type2,
50    Type3,
51}
52
53impl G00Type {
54    fn from_u8(v: u8) -> Result<Self> {
55        match v {
56            0 => Ok(Self::Type0),
57            1 => Ok(Self::Type1),
58            2 => Ok(Self::Type2),
59            3 => Ok(Self::Type3),
60            _ => bail!("g00: unknown type {}", v),
61        }
62    }
63}
64
65#[derive(Clone, Debug)]
66pub struct G00 {
67    pub ty: G00Type,
68    pub cuts: Vec<G00Cut>,
69}
70
71#[derive(Clone, Debug)]
72pub struct G00Cut {
73    pub width: u32,
74    pub height: u32,
75    pub center_x: i32,
76    pub center_y: i32,
77    pub disp_left: i32,
78    pub disp_top: i32,
79    pub disp_right: i32,
80    pub disp_bottom: i32,
81    pub chips: Vec<G00Chip>,
82}
83
84#[derive(Clone, Debug)]
85pub struct G00Chip {
86    pub x: u32,
87    pub y: u32,
88    pub width: u32,
89    pub height: u32,
90    pub sprite: bool,
91    pub data: G00ChipData,
92}
93
94#[derive(Clone, Debug)]
95pub enum G00ChipData {
96    /// Type0: LZSS32 compressed 32bpp pixels.
97    Type0Lzss32(Vec<u8>),
98    /// Type1: LZSS compressed palette + indices.
99    Type1LzssIndexed(Vec<u8>),
100    /// Type2: Raw 32bpp pixels in the cut stream.
101    RawBgra(Vec<u8>),
102    /// Type3: JPEG bitstream (not decoded here).
103    Jpeg(Vec<u8>),
104}
105
106struct Cur<'a> {
107    buf: &'a [u8],
108    pos: usize,
109}
110
111impl<'a> Cur<'a> {
112    fn new(buf: &'a [u8]) -> Self {
113        Self { buf, pos: 0 }
114    }
115
116    fn remaining(&self) -> usize {
117        self.buf.len().saturating_sub(self.pos)
118    }
119
120    fn ensure(&self, n: usize) -> Result<()> {
121        if self.pos + n > self.buf.len() {
122            bail!(
123                "g00: unexpected EOF (need {}, have {})",
124                n,
125                self.remaining()
126            );
127        }
128        Ok(())
129    }
130
131    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
132        self.ensure(n)?;
133        let out = &self.buf[self.pos..self.pos + n];
134        self.pos += n;
135        Ok(out)
136    }
137
138    fn skip(&mut self, n: usize) -> Result<()> {
139        self.ensure(n)?;
140        self.pos += n;
141        Ok(())
142    }
143
144    fn read_u8(&mut self) -> Result<u8> {
145        Ok(self.take(1)?[0])
146    }
147
148    fn read_u16_le(&mut self) -> Result<u16> {
149        let b = self.take(2)?;
150        Ok(u16::from_le_bytes([b[0], b[1]]))
151    }
152
153    fn read_u32_le(&mut self) -> Result<u32> {
154        let b = self.take(4)?;
155        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
156    }
157
158    fn read_i32_le(&mut self) -> Result<i32> {
159        Ok(self.read_u32_le()? as i32)
160    }
161}
162
163impl G00 {
164    /// Parse a full `.g00` file.
165    pub fn parse(bytes: &[u8]) -> Result<Self> {
166        let mut cur = Cur::new(bytes);
167        let ty = G00Type::from_u8(cur.read_u8()?)?;
168
169        match ty {
170            G00Type::Type0 | G00Type::Type1 | G00Type::Type3 => {
171                let w = cur.read_u16_le()? as u32;
172                let h = cur.read_u16_le()? as u32;
173                let rest = cur.take(cur.remaining())?.to_vec();
174
175                let chip_data = match ty {
176                    G00Type::Type0 => G00ChipData::Type0Lzss32(rest),
177                    G00Type::Type1 => G00ChipData::Type1LzssIndexed(rest),
178                    G00Type::Type3 => G00ChipData::Jpeg(rest),
179                    _ => unreachable!(),
180                };
181
182                let chip = G00Chip {
183                    x: 0,
184                    y: 0,
185                    width: w,
186                    height: h,
187                    sprite: false,
188                    data: chip_data,
189                };
190
191                let cut = G00Cut {
192                    width: w,
193                    height: h,
194                    center_x: 0,
195                    center_y: 0,
196                    disp_left: 0,
197                    disp_top: 0,
198                    disp_right: w as i32,
199                    disp_bottom: h as i32,
200                    chips: vec![chip],
201                };
202
203                Ok(Self {
204                    ty,
205                    cuts: vec![cut],
206                })
207            }
208            G00Type::Type2 => {
209                // Header:
210                // [u16 width][u16 height][i32 cut_cnt][G00_CUT_DATABASE * cut_cnt][lzss payload...]
211                let _w = cur.read_u16_le()? as u32;
212                let _h = cur.read_u16_le()? as u32;
213                let cut_cnt = cur.read_i32_le()?;
214                if cut_cnt < 0 {
215                    bail!("g00: negative cut_cnt {}", cut_cnt);
216                }
217
218                // G00_CUT_DATABASE is 6 * i32.
219                let db_bytes = (cut_cnt as usize)
220                    .checked_mul(24)
221                    .ok_or_else(|| anyhow!("g00: cut database size overflow"))?;
222                cur.skip(db_bytes)?;
223
224                let compressed = cur.take(cur.remaining())?;
225                let decompressed = lzss_unpack(compressed)?;
226
227                let mut dcur = Cur::new(&decompressed);
228                let table_cut_cnt = dcur.read_u32_le()? as usize;
229
230                // Table entries: (offset, size) pairs.
231                let mut pairs: Vec<(usize, i32)> = Vec::with_capacity(table_cut_cnt);
232                for _ in 0..table_cut_cnt {
233                    let off = dcur.read_u32_le()? as usize;
234                    let size = dcur.read_u32_le()? as i32;
235                    pairs.push((off, size));
236                }
237
238                let mut cuts = Vec::new();
239                for (off, size) in pairs {
240                    if off == 0 || size <= 0 {
241                        continue;
242                    }
243                    let size_u = size as usize;
244                    let end = off
245                        .checked_add(size_u)
246                        .ok_or_else(|| anyhow!("g00: cut slice overflow"))?;
247                    if end > decompressed.len() {
248                        bail!(
249                            "g00: cut slice out of bounds (off={}, size={}, len={})",
250                            off,
251                            size_u,
252                            decompressed.len()
253                        );
254                    }
255                    let cut_data = &decompressed[off..end];
256                    cuts.push(parse_type2_cut(cut_data)?);
257                }
258
259                if cuts.is_empty() {
260                    bail!("g00: type2 has no cuts");
261                }
262
263                Ok(Self { ty, cuts })
264            }
265        }
266    }
267}
268
269fn parse_type2_cut(cut_data: &[u8]) -> Result<G00Cut> {
270    // Cut header layout (MSVC default packing, 32-bit):
271    //  u8 type
272    //  u8 pad
273    //  u16 count
274    //  i32 x, y, disp_xl, disp_yl, xc, yc, cut_xl, cut_yl
275    //  i32 keep[20]
276    // Total: 116 bytes.
277    let mut cur = Cur::new(cut_data);
278
279    let _cut_type = cur.read_u8()?;
280    cur.skip(1)?; // padding
281    let chip_cnt = cur.read_u16_le()? as usize;
282
283    let x = cur.read_i32_le()?;
284    let y = cur.read_i32_le()?;
285    let disp_xl = cur.read_i32_le()?;
286    let disp_yl = cur.read_i32_le()?;
287    let xc = cur.read_i32_le()?;
288    let yc = cur.read_i32_le()?;
289    let cut_xl = cur.read_i32_le()?;
290    let cut_yl = cur.read_i32_le()?;
291
292    cur.skip(20 * 4)?; // keep
293
294    if cut_xl <= 0 || cut_yl <= 0 {
295        bail!("g00: invalid cut size {}x{}", cut_xl, cut_yl);
296    }
297
298    let mut chips = Vec::with_capacity(chip_cnt);
299    for _ in 0..chip_cnt {
300        // Chip header layout (MSVC default packing, 32-bit):
301        //  u16 x
302        //  u16 y
303        //  u8 type
304        //  u8 pad
305        //  u16 xl
306        //  u16 yl
307        //  u16 pad2 (to align i32)
308        //  i32 keep[20]
309        // Total: 92 bytes.
310        let cx = cur.read_u16_le()? as u32;
311        let cy = cur.read_u16_le()? as u32;
312        let ctype = cur.read_u8()?;
313        cur.skip(1)?; // padding
314        let w = cur.read_u16_le()? as u32;
315        let h = cur.read_u16_le()? as u32;
316        cur.skip(2)?; // padding to i32
317        cur.skip(20 * 4)?; // keep
318
319        let pix_len = (w as usize)
320            .checked_mul(h as usize)
321            .and_then(|v| v.checked_mul(4))
322            .ok_or_else(|| anyhow!("g00: chip pixel size overflow"))?;
323        let pix = cur.take(pix_len)?.to_vec();
324
325        chips.push(G00Chip {
326            x: cx,
327            y: cy,
328            width: w,
329            height: h,
330            sprite: ctype == 1,
331            data: G00ChipData::RawBgra(pix),
332        });
333    }
334
335    Ok(G00Cut {
336        width: cut_xl as u32,
337        height: cut_yl as u32,
338        center_x: xc,
339        center_y: yc,
340        disp_left: x,
341        disp_top: y,
342        disp_right: x.saturating_add(disp_xl),
343        disp_bottom: y.saturating_add(disp_yl),
344        chips,
345    })
346}
347
348impl G00Chip {
349    /// Decode chip pixels into a BGRA8 buffer (little-endian bytes).
350    pub fn decode_bgra(&self) -> Result<Vec<u8>> {
351        match &self.data {
352            G00ChipData::Type0Lzss32(blob) => {
353                let out = lzss_unpack32(blob)?;
354                let expected = (self.width as usize)
355                    .checked_mul(self.height as usize)
356                    .and_then(|v| v.checked_mul(4))
357                    .ok_or_else(|| anyhow!("g00: expected size overflow"))?;
358                if out.len() != expected {
359                    bail!(
360                        "g00: type0 size mismatch (got={}, expected={})",
361                        out.len(),
362                        expected
363                    );
364                }
365                Ok(out)
366            }
367            G00ChipData::Type1LzssIndexed(blob) => {
368                let dec = lzss_unpack(blob)?;
369                let mut cur = Cur::new(&dec);
370
371                let pal_cnt = cur.read_u16_le()? as usize;
372                if pal_cnt == 0 {
373                    bail!("g00: type1 pal_cnt=0");
374                }
375
376                let pal_bytes = pal_cnt
377                    .checked_mul(4)
378                    .ok_or_else(|| anyhow!("g00: palette size overflow"))?;
379                let pal_raw = cur.take(pal_bytes)?;
380
381                let expected_px = (self.width as usize)
382                    .checked_mul(self.height as usize)
383                    .ok_or_else(|| anyhow!("g00: index size overflow"))?;
384
385                if cur.remaining() < expected_px {
386                    bail!(
387                        "g00: type1 truncated indices (need={}, have={})",
388                        expected_px,
389                        cur.remaining()
390                    );
391                }
392
393                let indices = cur.take(expected_px)?;
394
395                // Palette entries are stored as u32 and copied directly to the output dwords in the original implementation.
396                // On little-endian, u32::to_le_bytes gives BGRA bytes.
397                let mut out: Vec<u8> = Vec::with_capacity(expected_px * 4);
398                for &ix in indices {
399                    let ix = ix as usize;
400                    if ix >= pal_cnt {
401                        bail!(
402                            "g00: type1 palette index out of range (ix={}, pal_cnt={})",
403                            ix,
404                            pal_cnt
405                        );
406                    }
407                    let base = ix * 4;
408                    out.extend_from_slice(&pal_raw[base..base + 4]);
409                }
410
411                Ok(out)
412            }
413            G00ChipData::RawBgra(pix) => {
414                let expected = (self.width as usize)
415                    .checked_mul(self.height as usize)
416                    .and_then(|v| v.checked_mul(4))
417                    .ok_or_else(|| anyhow!("g00: expected size overflow"))?;
418                if pix.len() != expected {
419                    bail!(
420                        "g00: raw size mismatch (got={}, expected={})",
421                        pix.len(),
422                        expected
423                    );
424                }
425                Ok(pix.clone())
426            }
427            G00ChipData::Jpeg(bytes) => {
428                let jpeg = decode_type3_jpeg_payload(bytes);
429                if !jpeg.starts_with(&[0xFF, 0xD8]) {
430                    bail!(
431                        "g00: type3 XOR decode did not produce JPEG SOI (got={:02X?})",
432                        &jpeg[..jpeg.len().min(2)]
433                    );
434                }
435                let img = image::load_from_memory_with_format(&jpeg, ImageFormat::Jpeg)
436                    .or_else(|_| image::load_from_memory(&jpeg))
437                    .map_err(|e| anyhow!("g00: JPEG decode failed after type3 XOR: {e}"))?;
438                let rgba = img.to_rgba8();
439                let (w, h) = rgba.dimensions();
440                if w != self.width || h != self.height {
441                    bail!(
442                        "g00: JPEG size mismatch (got={}x{}, expected={}x{})",
443                        w,
444                        h,
445                        self.width,
446                        self.height
447                    );
448                }
449                let mut out = rgba.into_raw();
450                bgra_to_rgba_in_place(&mut out);
451                // We swapped RGBA->BGRA in-place above; decode_bgra expects BGRA.
452                Ok(out)
453            }
454        }
455    }
456
457    /// Decode chip pixels into an RGBA8 buffer.
458    pub fn decode_rgba(&self) -> Result<Vec<u8>> {
459        let mut bgra = self.decode_bgra()?;
460        bgra_to_rgba_in_place(&mut bgra);
461        Ok(bgra)
462    }
463
464    /// Return the stored type 3 payload bytes. These bytes are still XOR-obfuscated.
465    pub fn jpeg_bytes(&self) -> Option<&[u8]> {
466        match &self.data {
467            G00ChipData::Jpeg(b) => Some(b),
468            _ => None,
469        }
470    }
471}
472
473fn bgra_to_rgba_in_place(buf: &mut [u8]) {
474    for px in buf.as_chunks_mut::<4>().0 {
475        // BGRA -> RGBA
476        px.swap(0, 2);
477    }
478}