Skip to main content

siglus_scene_vm/assets/
g00.rs

1//! G00 decoder.
2//!
3//! Implemented based on the the original implementation extractor logic provided by the user.
4//!
5//! Output format: RGBA8.
6
7use crate::assets::RgbaImage;
8use anyhow::{bail, Context, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum G00Type {
13    Type24bit = 0,
14    Type8bit = 1,
15    TypeDir = 2,
16    TypeJpeg = 3,
17}
18
19#[derive(Debug, Clone)]
20pub struct DecodedG00 {
21    pub kind: G00Type,
22    pub width: u32,
23    pub height: u32,
24    /// For TypeDir, this contains multiple frames.
25    pub frames: Vec<RgbaImage>,
26}
27
28fn read_u16le(buf: &[u8], off: usize) -> Result<u16> {
29    if off + 2 > buf.len() {
30        bail!("read u16le out of bounds at {off}");
31    }
32    Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
33}
34
35fn read_u32le(buf: &[u8], off: usize) -> Result<u32> {
36    if off + 4 > buf.len() {
37        bail!("read u32le out of bounds at {off}");
38    }
39    Ok(u32::from_le_bytes([
40        buf[off],
41        buf[off + 1],
42        buf[off + 2],
43        buf[off + 3],
44    ]))
45}
46
47fn read_i32le(buf: &[u8], off: usize) -> Result<i32> {
48    if off + 4 > buf.len() {
49        bail!("read i32le out of bounds at {off}");
50    }
51    Ok(i32::from_le_bytes([
52        buf[off],
53        buf[off + 1],
54        buf[off + 2],
55        buf[off + 3],
56    ]))
57}
58
59/// Decode a `.g00` file into RGBA frames.
60pub fn decode_g00(data: &[u8]) -> Result<DecodedG00> {
61    if data.len() < 1 + 2 + 2 {
62        bail!("g00 too small");
63    }
64    let ty = data[0];
65    let width = read_u16le(data, 1)? as u32;
66    let height = read_u16le(data, 3)? as u32;
67    let kind = match ty {
68        0 => G00Type::Type24bit,
69        1 => G00Type::Type8bit,
70        2 => G00Type::TypeDir,
71        3 => G00Type::TypeJpeg,
72        other => bail!("unknown g00 type: {other}"),
73    };
74
75    let mut off = 5;
76
77    match kind {
78        G00Type::Type24bit => {
79            // lzss_compress_head_t { compress_length, decompress_length }
80            let decompress_length = read_u32le(data, off + 4)? as usize;
81            off += 8;
82            if off > data.len() {
83                bail!("g00 type0 header out of bounds");
84            }
85            if decompress_length == 0 {
86                bail!("g00 type0 decompress_length=0");
87            }
88            let mut out = vec![0u8; decompress_length];
89            lzss_decompress_24bit(&data[off..], &mut out).context("lzss_decompress_24bit")?;
90
91            // out is BGRA (alpha already 255). Convert to RGBA.
92            let rgba = bgra_to_rgba_inplace(out);
93            Ok(DecodedG00 {
94                kind,
95                width,
96                height,
97                frames: vec![RgbaImage {
98                    width,
99                    height,
100                    center_x: 0,
101                    center_y: 0,
102                    rgba,
103                }],
104            })
105        }
106        G00Type::Type8bit => {
107            // RealLive_g00_type1_uncompress
108            let (mut out, out_len) =
109                real_live_type1_uncompress(&data[off..]).context("type1 uncompress")?;
110            if out_len == 0 {
111                bail!("type1 produced empty output");
112            }
113            out.truncate(out_len);
114            // output is BGRA
115            let rgba = bgra_to_rgba_inplace(out);
116            Ok(DecodedG00 {
117                kind,
118                width,
119                height,
120                frames: vec![RgbaImage {
121                    width,
122                    height,
123                    center_x: 0,
124                    center_y: 0,
125                    rgba,
126                }],
127            })
128        }
129        G00Type::TypeDir => {
130            // type2: index_entries + g02_info_list + LZSS payload
131            // We do not need g02_info_list for image extraction in this stage, but we must skip it.
132            let index_entries = read_u32le(data, off)? as usize;
133            off += 4;
134            // g02_info_t is 24 bytes in the provided extractor.
135            let g02_info_size = 24usize;
136            let skip = index_entries
137                .checked_mul(g02_info_size)
138                .context("index_entries overflow")?;
139            if off + skip > data.len() {
140                bail!("type2 g02_info_list out of bounds");
141            }
142            off += skip;
143
144            // lzss_compress_head_t
145            let decompress_length = read_u32le(data, off + 4)? as usize;
146            off += 8;
147            if decompress_length == 0 {
148                bail!("type2 decompress_length=0");
149            }
150            if off > data.len() {
151                bail!("type2 payload out of bounds");
152            }
153
154            let mut debuf = vec![0u8; decompress_length];
155            lzss_decompress(&data[off..], &mut debuf).context("lzss_decompress")?;
156
157            // debuf: u32 entries, then entries * {u32 offset,u32 length}
158            if debuf.len() < 4 {
159                bail!("type2 debuf too small");
160            }
161            let debuf_entries = read_u32le(&debuf, 0)? as usize;
162            let pairs_off = 4usize;
163            let pair_size = 8usize;
164            let pairs_bytes = debuf_entries
165                .checked_mul(pair_size)
166                .context("debuf_entries overflow")?;
167            if pairs_off + pairs_bytes > debuf.len() {
168                bail!("type2 pairs out of bounds");
169            }
170
171            let mut frames: Vec<RgbaImage> = Vec::with_capacity(debuf_entries);
172            for i in 0..debuf_entries {
173                let p_off = pairs_off + i * pair_size;
174                let offset = read_u32le(&debuf, p_off)? as usize;
175                let length_raw = read_u32le(&debuf, p_off + 4)? as i32;
176                if offset == 0 || length_raw == 0 || offset >= debuf.len() {
177                    frames.push(transparent_missing_g00_cut());
178                    continue;
179                }
180
181                // Original C_g00::get_cut_data_point() treats negative size as a
182                // linked/reused cut-data marker and still returns g00_data + offset.
183                // C_g00_cut::set_data(type2) parses the cut header/chips from that
184                // pointer and does not need the signed size as a hard bound.
185                let end = if length_raw > 0 {
186                    offset.saturating_add(length_raw as usize).min(debuf.len())
187                } else {
188                    debuf.len()
189                };
190                if end <= offset {
191                    frames.push(transparent_missing_g00_cut());
192                    continue;
193                }
194                let part_bytes = &debuf[offset..end];
195                let img = extract_g02_part(part_bytes)
196                    .with_context(|| format!("extract g02 part idx={i}"))?;
197                frames.push(img);
198            }
199
200            if frames.is_empty() {
201                bail!("type2 produced no frames");
202            }
203
204            Ok(DecodedG00 {
205                kind,
206                width,
207                height,
208                frames,
209            })
210        }
211        G00Type::TypeJpeg => {
212            if off > data.len() {
213                bail!("g00 type3 header out of bounds");
214            }
215            let jpeg = &data[off..];
216            let img = image::load_from_memory_with_format(jpeg, image::ImageFormat::Jpeg)
217                .or_else(|_| image::load_from_memory(jpeg))
218                .context("decode g00 jpeg")?;
219            let rgba = img.to_rgba8();
220            let (w, h) = rgba.dimensions();
221            if w != width || h != height {
222                bail!("g00 jpeg size mismatch: got={w}x{h}, expect={width}x{height}");
223            }
224            Ok(DecodedG00 {
225                kind,
226                width,
227                height,
228                frames: vec![RgbaImage {
229                    width,
230                    height,
231                    center_x: 0,
232                    center_y: 0,
233                    rgba: rgba.into_raw(),
234                }],
235            })
236        }
237    }
238}
239
240
241fn transparent_missing_g00_cut() -> RgbaImage {
242    RgbaImage {
243        width: 1,
244        height: 1,
245        center_x: 0,
246        center_y: 0,
247        rgba: vec![0, 0, 0, 0],
248    }
249}
250
251fn bgra_to_rgba_inplace(mut bgra: Vec<u8>) -> Vec<u8> {
252    for px in bgra.chunks_exact_mut(4) {
253        let b = px[0];
254        let g = px[1];
255        let r = px[2];
256        let a = px[3];
257        px[0] = r;
258        px[1] = g;
259        px[2] = b;
260        px[3] = a;
261    }
262    bgra
263}
264
265fn real_live_type1_uncompress(compr: &[u8]) -> Result<(Vec<u8>, usize)> {
266    if compr.len() < 8 {
267        bail!("type1 data too small");
268    }
269    let total_len = read_u32le(compr, 0)? as usize;
270    let uncomprlen = read_u32le(compr, 4)? as usize;
271    if total_len < 8 {
272        bail!("type1 total_len < 8");
273    }
274    if total_len > compr.len() {
275        // Be strict: extractor uses the length to limit parsing.
276        bail!(
277            "type1 total_len out of bounds: total_len={total_len} buf={}",
278            compr.len()
279        );
280    }
281
282    if uncomprlen != 0 {
283        let mut out = vec![0u8; uncomprlen + 64];
284        let mut curbyte = 8usize;
285        let mut act = 0usize;
286        let mut bit_count = 0u8;
287        let mut flag = 0u8;
288
289        while act < uncomprlen && curbyte < total_len {
290            if bit_count == 0 {
291                flag = compr[curbyte];
292                curbyte += 1;
293                bit_count = 8;
294            }
295
296            if (flag & 1) != 0 {
297                if curbyte >= total_len {
298                    break;
299                }
300                out[act] = compr[curbyte];
301                act += 1;
302                curbyte += 1;
303            } else {
304                if curbyte + 2 > total_len {
305                    break;
306                }
307                let count0 = compr[curbyte] as usize;
308                let b1 = compr[curbyte + 1] as usize;
309                curbyte += 2;
310
311                let offset = (b1 << 4) | (count0 >> 4);
312                let count = (count0 & 0xF) + 2;
313                if offset == 0 {
314                    bail!("type1 invalid offset=0");
315                }
316                if act < offset {
317                    bail!("type1 backref before start: act={act} offset={offset}");
318                }
319                for _ in 0..count {
320                    if act >= uncomprlen {
321                        break;
322                    }
323                    let v = out[act - offset];
324                    out[act] = v;
325                    act += 1;
326                }
327            }
328
329            flag >>= 1;
330            bit_count = bit_count.saturating_sub(1);
331        }
332
333        Ok((out, uncomprlen))
334    } else {
335        let payload_len = total_len - 8;
336        let mut out = vec![0u8; payload_len];
337        out.copy_from_slice(&compr[8..8 + payload_len]);
338        Ok((out, payload_len))
339    }
340}
341
342fn lzss_decompress(src: &[u8], dst: &mut [u8]) -> Result<()> {
343    let mut s = 0usize;
344    let mut d = 0usize;
345    while d < dst.len() {
346        if s >= src.len() {
347            break;
348        }
349        let mut flags = src[s];
350        s += 1;
351        for _ in 0..8 {
352            if d >= dst.len() {
353                break;
354            }
355            if (flags & 1) != 0 {
356                if s >= src.len() {
357                    break;
358                }
359                dst[d] = src[s];
360                d += 1;
361                s += 1;
362            } else {
363                if s + 2 > src.len() {
364                    break;
365                }
366                let w = u16::from_le_bytes([src[s], src[s + 1]]) as usize;
367                s += 2;
368                let offset = w >> 4;
369                let count = (w & 0xF) + 2;
370                if offset == 0 {
371                    bail!("lzss offset=0");
372                }
373                if d < offset {
374                    bail!("lzss backref before start: d={d} offset={offset}");
375                }
376                for _ in 0..count {
377                    if d >= dst.len() {
378                        break;
379                    }
380                    let v = dst[d - offset];
381                    dst[d] = v;
382                    d += 1;
383                }
384            }
385            flags >>= 1;
386        }
387    }
388
389    if d != dst.len() {
390        bail!(
391            "lzss_decompress did not fill output: wrote {d} of {}",
392            dst.len()
393        );
394    }
395    Ok(())
396}
397
398fn lzss_decompress_24bit(src: &[u8], dst: &mut [u8]) -> Result<()> {
399    // the original implementation extractor emits BGRA (alpha byte set to 0xFF).
400    let mut s = 0usize;
401    let mut d = 0usize;
402    while d < dst.len() {
403        if s >= src.len() {
404            break;
405        }
406        let mut flags = src[s];
407        s += 1;
408        for _ in 0..8 {
409            if d >= dst.len() {
410                break;
411            }
412            if (flags & 1) != 0 {
413                if s + 3 > src.len() {
414                    break;
415                }
416                if d + 4 > dst.len() {
417                    bail!("lzss24 literal would overflow dst");
418                }
419                // movsw; movsb; then alpha=0xFF
420                dst[d] = src[s];
421                dst[d + 1] = src[s + 1];
422                dst[d + 2] = src[s + 2];
423                dst[d + 3] = 0xFF;
424                d += 4;
425                s += 3;
426            } else {
427                if s + 2 > src.len() {
428                    break;
429                }
430                let w = u16::from_le_bytes([src[s], src[s + 1]]) as usize;
431                s += 2;
432                let offset_bytes = (w >> 4) << 2; // (word>>4)*4
433                let dword_count = (w & 0xF) + 1;
434                let count_bytes = dword_count * 4;
435                if offset_bytes == 0 {
436                    bail!("lzss24 offset_bytes=0");
437                }
438                if d < offset_bytes {
439                    bail!("lzss24 backref before start: d={d} offset={offset_bytes}");
440                }
441                for _ in 0..count_bytes {
442                    if d >= dst.len() {
443                        break;
444                    }
445                    let v = dst[d - offset_bytes];
446                    dst[d] = v;
447                    d += 1;
448                }
449            }
450            flags >>= 1;
451        }
452    }
453
454    if d != dst.len() {
455        bail!(
456            "lzss_decompress_24bit did not fill output: wrote {d} of {}",
457            dst.len()
458        );
459    }
460    Ok(())
461}
462
463#[derive(Debug, Clone)]
464struct G02PartInfo {
465    part_type: u16,
466    block_count: u16,
467    hs_orig_x: i32,
468    hs_orig_y: i32,
469    width: u32,
470    height: u32,
471    screen_show_x: i32,
472    screen_show_y: i32,
473    full_part_width: u32,
474    full_part_height: u32,
475}
476
477#[derive(Debug, Clone)]
478struct G02BlockInfo {
479    orig_x: u16,
480    orig_y: u16,
481    _info: u16,
482    width: u16,
483    height: u16,
484}
485
486const G02_BLOCK_INFO_SIZE: usize = 92; // sizeof(g02_block_info_t) in the provided extractor
487
488fn parse_g02_block(buf: &[u8]) -> Result<G02BlockInfo> {
489    if buf.len() < G02_BLOCK_INFO_SIZE {
490        bail!("g02_block_info_t truncated");
491    }
492    let orig_x = read_u16le(buf, 0)?;
493    let orig_y = read_u16le(buf, 2)?;
494    let info = read_u16le(buf, 4)?;
495    let width = read_u16le(buf, 6)?;
496    let height = read_u16le(buf, 8)?;
497    Ok(G02BlockInfo {
498        orig_x,
499        orig_y,
500        _info: info,
501        width,
502        height,
503    })
504}
505
506fn parse_g02_part_info_prefix(buf: &[u8]) -> Result<G02PartInfo> {
507    // Original C++ G00_CUT_HEADER_STRUCT layout:
508    //   u16 type, u16 count,
509    //   i32 x, i32 y, i32 disp_xl, i32 disp_yl,
510    //   i32 xc, i32 yc,
511    //   i32 cut_xl, i32 cut_yl,
512    //   i32 keep[20]
513    // C_g00_cut::set_data() uses cut_xl/cut_yl as the actual cut image size.
514    // Chip x/y are already coordinates in that full cut image; they are not
515    // relative to the display rectangle x/y.
516    if buf.len() < 0x24 {
517        bail!("g02_part_info prefix too small");
518    }
519    let part_type = read_u16le(buf, 0)?;
520    let block_count = read_u16le(buf, 2)?;
521    let disp_x = read_i32le(buf, 4)?;
522    let disp_y = read_i32le(buf, 8)?;
523    let disp_width = read_u32le(buf, 0x0C)?;
524    let disp_height = read_u32le(buf, 0x10)?;
525    let center_x = read_i32le(buf, 0x14)?;
526    let center_y = read_i32le(buf, 0x18)?;
527    let cut_width = read_u32le(buf, 0x1C)?;
528    let cut_height = read_u32le(buf, 0x20)?;
529    Ok(G02PartInfo {
530        part_type,
531        block_count,
532        hs_orig_x: disp_x,
533        hs_orig_y: disp_y,
534        width: cut_width,
535        height: cut_height,
536        screen_show_x: center_x,
537        screen_show_y: center_y,
538        full_part_width: disp_width,
539        full_part_height: disp_height,
540    })
541}
542
543fn fix_vertical_flip_bgra(width: u32, height: u32, buf: &mut [u8]) -> Result<()> {
544    let stride = width.checked_mul(4).context("stride overflow")? as usize;
545    let h = height as usize;
546    if buf.len() != stride * h {
547        bail!("fix_vertical_flip_bgra length mismatch");
548    }
549    let mut tmp = vec![0u8; buf.len()];
550    for y in 0..h {
551        let src_off = y * stride;
552        let dst_off = (h - 1 - y) * stride;
553        tmp[dst_off..dst_off + stride].copy_from_slice(&buf[src_off..src_off + stride]);
554    }
555    buf.copy_from_slice(&tmp);
556    Ok(())
557}
558
559fn extract_g02_part(part_bytes: &[u8]) -> Result<RgbaImage> {
560    // Original C_d3d_texture::load_g00_cut() creates the D3D texture from
561    // cut_info.disp_rect, not from the whole cut_xl/cut_yl canvas. Each chip is
562    // copied to chip.x - disp_rect.left, chip.y - disp_rect.top, and texture
563    // center is cut_info.center - disp_rect.left/top.
564    let part = parse_g02_part_info_prefix(part_bytes).context("parse part prefix")?;
565    if part.width == 0 || part.height == 0 {
566        bail!("g02 part has zero full-cut dimensions");
567    }
568    if part.full_part_width == 0 || part.full_part_height == 0 {
569        bail!("g02 part has zero display dimensions");
570    }
571
572    // MSVC layout for the original G00_CUT_HEADER_STRUCT is 0x74 bytes.
573    // Try the original layout first, then keep the older defensive candidates for
574    // unusual extracted assets.
575    let candidates: [usize; 10] = [0x74, 0x24, 0xD0, 0xC0, 0xE0, 0x80, 0x90, 0xA0, 0xB0, 0x100];
576
577    let mut chosen_header: Option<usize> = None;
578    for &hdr in &candidates {
579        if hdr > part_bytes.len() {
580            continue;
581        }
582        if validate_g02_layout(part_bytes, &part, hdr).is_ok() {
583            chosen_header = Some(hdr);
584            break;
585        }
586    }
587
588    let header_size = chosen_header.context("unable to determine g02_part_info header size")?;
589
590    let out_w = part.full_part_width;
591    let out_h = part.full_part_height;
592    let stride = (out_w as usize)
593        .checked_mul(4)
594        .context("stride overflow")?;
595    let mut dib = vec![0u8; stride * (out_h as usize)];
596
597    let mut off = header_size;
598    for _ in 0..part.block_count {
599        let block = parse_g02_block(&part_bytes[off..])?;
600        off += G02_BLOCK_INFO_SIZE;
601
602        let bw = block.width as usize;
603        let bh = block.height as usize;
604        if bw == 0 || bh == 0 {
605            bail!("g02 block zero size");
606        }
607        let px_len = bw
608            .checked_mul(bh)
609            .and_then(|v| v.checked_mul(4))
610            .context("pixel len overflow")?;
611        if off + px_len > part_bytes.len() {
612            bail!("g02 block pixel data out of bounds");
613        }
614        let src = &part_bytes[off..off + px_len];
615        off += px_len;
616
617        let dst_x_i = block.orig_x as i32 - part.hs_orig_x;
618        let dst_y_i = block.orig_y as i32 - part.hs_orig_y;
619        if dst_x_i < 0 || dst_y_i < 0 {
620            bail!(
621                "g02 block outside display rect: chip=({}, {}) disp=({}, {})",
622                block.orig_x,
623                block.orig_y,
624                part.hs_orig_x,
625                part.hs_orig_y
626            );
627        }
628        let dst_x = dst_x_i as usize;
629        let dst_y = dst_y_i as usize;
630        if dst_x.saturating_add(bw) > out_w as usize || dst_y.saturating_add(bh) > out_h as usize {
631            bail!(
632                "g02 block write outside display rect: dst=({}, {}) size={}x{} out={}x{}",
633                dst_x,
634                dst_y,
635                bw,
636                bh,
637                out_w,
638                out_h
639            );
640        }
641
642        for row in 0..bh {
643            let src_row_off = row * bw * 4;
644            let dst_row_off = (dst_y + row) * stride + dst_x * 4;
645            let dst_end = dst_row_off + bw * 4;
646            if dst_end > dib.len() {
647                bail!("g02 block write out of bounds");
648            }
649            dib[dst_row_off..dst_end].copy_from_slice(&src[src_row_off..src_row_off + bw * 4]);
650        }
651    }
652
653    let rgba = bgra_to_rgba_inplace(dib);
654
655    Ok(RgbaImage {
656        width: out_w,
657        height: out_h,
658        center_x: part.screen_show_x - part.hs_orig_x,
659        center_y: part.screen_show_y - part.hs_orig_y,
660        rgba,
661    })
662}
663
664fn validate_g02_layout(part_bytes: &[u8], part: &G02PartInfo, header_size: usize) -> Result<()> {
665    let mut off = header_size;
666    for _ in 0..part.block_count {
667        if off + G02_BLOCK_INFO_SIZE > part_bytes.len() {
668            bail!("block header out of bounds");
669        }
670        let block = parse_g02_block(&part_bytes[off..off + G02_BLOCK_INFO_SIZE])?;
671        off += G02_BLOCK_INFO_SIZE;
672
673        if block.width == 0 || block.height == 0 {
674            bail!("block zero size");
675        }
676        if (block.width as u32) > part.width || (block.height as u32) > part.height {
677            bail!("block larger than full cut");
678        }
679        if (block.orig_x as u32).saturating_add(block.width as u32) > part.width
680            || (block.orig_y as u32).saturating_add(block.height as u32) > part.height
681        {
682            bail!("block outside full cut");
683        }
684        // Many files keep reserved zeros; we don't strictly check reserved bytes here.
685
686        let bw = block.width as usize;
687        let bh = block.height as usize;
688        let px_len = bw
689            .checked_mul(bh)
690            .and_then(|v| v.checked_mul(4))
691            .context("pixel len overflow")?;
692        if off + px_len > part_bytes.len() {
693            bail!("block pixels out of bounds");
694        }
695        off += px_len;
696    }
697    Ok(())
698}