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::{Context, Result, bail};
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    /// Per-cut canvas dimensions before display-rectangle cropping.
27    /// These are C_d3d_texture::get_original_width/height(), used by scripts.
28    pub original_sizes: Vec<(u32, u32)>,
29}
30
31fn read_u16le(buf: &[u8], off: usize) -> Result<u16> {
32    if off + 2 > buf.len() {
33        bail!("read u16le out of bounds at {off}");
34    }
35    Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
36}
37
38fn read_u32le(buf: &[u8], off: usize) -> Result<u32> {
39    if off + 4 > buf.len() {
40        bail!("read u32le out of bounds at {off}");
41    }
42    Ok(u32::from_le_bytes([
43        buf[off],
44        buf[off + 1],
45        buf[off + 2],
46        buf[off + 3],
47    ]))
48}
49
50fn read_i32le(buf: &[u8], off: usize) -> Result<i32> {
51    if off + 4 > buf.len() {
52        bail!("read i32le out of bounds at {off}");
53    }
54    Ok(i32::from_le_bytes([
55        buf[off],
56        buf[off + 1],
57        buf[off + 2],
58        buf[off + 3],
59    ]))
60}
61
62/// Decode a `.g00` file into RGBA frames.
63pub fn decode_g00(data: &[u8]) -> Result<DecodedG00> {
64    if data.len() < 1 + 2 + 2 {
65        bail!("g00 too small");
66    }
67    let ty = data[0];
68    let width = read_u16le(data, 1)? as u32;
69    let height = read_u16le(data, 3)? as u32;
70    let kind = match ty {
71        0 => G00Type::Type24bit,
72        1 => G00Type::Type8bit,
73        2 => G00Type::TypeDir,
74        3 => G00Type::TypeJpeg,
75        other => bail!("unknown g00 type: {other}"),
76    };
77
78    let mut off = 5;
79
80    match kind {
81        G00Type::Type24bit => {
82            // lzss_compress_head_t { compress_length, decompress_length }
83            let decompress_length = read_u32le(data, off + 4)? as usize;
84            off += 8;
85            if off > data.len() {
86                bail!("g00 type0 header out of bounds");
87            }
88            if decompress_length == 0 {
89                bail!("g00 type0 decompress_length=0");
90            }
91            let mut out = vec![0u8; decompress_length];
92            lzss_decompress_24bit(&data[off..], &mut out).context("lzss_decompress_24bit")?;
93
94            // Convert BGR literals to the engine's RGBA representation as
95            // they enter the output. Backreferences then reuse already
96            // converted pixels, avoiding a second full-frame channel pass.
97            let rgba = out;
98            Ok(DecodedG00 {
99                kind,
100                width,
101                height,
102                original_sizes: vec![(width, height)],
103                frames: vec![RgbaImage {
104                    width,
105                    height,
106                    center_x: 0,
107                    center_y: 0,
108                    rgba,
109                }],
110            })
111        }
112        G00Type::Type8bit => {
113            // C_g00_chip::get_data(type 1): the LZSS output is not a BGRA
114            // framebuffer. It is WORD pal_cnt, pal_cnt DWORD BGRA entries,
115            // followed by one palette index byte per pixel.
116            let (out, out_len) =
117                real_live_type1_uncompress(&data[off..]).context("type1 uncompress")?;
118            let raw = &out[..out_len.min(out.len())];
119            if raw.len() < 2 {
120                bail!("type1 palette header truncated");
121            }
122            let pal_cnt = read_u16le(raw, 0)? as usize;
123            let pal_bytes = pal_cnt
124                .checked_mul(4)
125                .context("type1 palette size overflow")?;
126            let indices_off = 2usize
127                .checked_add(pal_bytes)
128                .context("type1 palette offset overflow")?;
129            if indices_off > raw.len() {
130                bail!(
131                    "type1 palette truncated: count={} bytes={} raw={}",
132                    pal_cnt,
133                    pal_bytes,
134                    raw.len()
135                );
136            }
137            let pixel_count = (width as usize)
138                .checked_mul(height as usize)
139                .context("type1 pixel count overflow")?;
140            let indices_end = indices_off
141                .checked_add(pixel_count)
142                .context("type1 index range overflow")?;
143            if indices_end > raw.len() {
144                bail!(
145                    "type1 indices truncated: need={} have={}",
146                    pixel_count,
147                    raw.len().saturating_sub(indices_off)
148                );
149            }
150
151            let palette = &raw[2..indices_off];
152            let indices = &raw[indices_off..indices_end];
153            let rgba_len = pixel_count
154                .checked_mul(4)
155                .context("type1 RGBA size overflow")?;
156            let mut rgba = Vec::with_capacity(rgba_len);
157            for &index in indices {
158                let index = index as usize;
159                if index >= pal_cnt {
160                    bail!(
161                        "type1 palette index out of range: index={} palette_count={}",
162                        index,
163                        pal_cnt
164                    );
165                }
166                let base = index * 4;
167                let b = palette[base];
168                let g = palette[base + 1];
169                let r = palette[base + 2];
170                let a = palette[base + 3];
171                rgba.extend_from_slice(&[r, g, b, a]);
172            }
173
174            Ok(DecodedG00 {
175                kind,
176                width,
177                height,
178                original_sizes: vec![(width, height)],
179                frames: vec![RgbaImage {
180                    width,
181                    height,
182                    center_x: 0,
183                    center_y: 0,
184                    rgba,
185                }],
186            })
187        }
188        G00Type::TypeDir => {
189            // type2: index_entries + g02_info_list + LZSS payload
190            // We do not need g02_info_list for image extraction in this stage, but we must skip it.
191            let index_entries = read_u32le(data, off)? as usize;
192            off += 4;
193            // g02_info_t is 24 bytes in the provided extractor.
194            let g02_info_size = 24usize;
195            let skip = index_entries
196                .checked_mul(g02_info_size)
197                .context("index_entries overflow")?;
198            if off + skip > data.len() {
199                bail!("type2 g02_info_list out of bounds");
200            }
201            off += skip;
202
203            // lzss_compress_head_t
204            let decompress_length = read_u32le(data, off + 4)? as usize;
205            off += 8;
206            if decompress_length == 0 {
207                bail!("type2 decompress_length=0");
208            }
209            if off > data.len() {
210                bail!("type2 payload out of bounds");
211            }
212
213            let mut debuf = vec![0u8; decompress_length];
214            lzss_decompress(&data[off..], &mut debuf).context("lzss_decompress")?;
215
216            // debuf: u32 entries, then entries * {u32 offset,u32 length}
217            if debuf.len() < 4 {
218                bail!("type2 debuf too small");
219            }
220            let debuf_entries = read_u32le(&debuf, 0)? as usize;
221            let pairs_off = 4usize;
222            let pair_size = 8usize;
223            let pairs_bytes = debuf_entries
224                .checked_mul(pair_size)
225                .context("debuf_entries overflow")?;
226            if pairs_off + pairs_bytes > debuf.len() {
227                bail!("type2 pairs out of bounds");
228            }
229
230            // C_g00::set_data() sizes m_cut_list from the outer cut count and
231            // calls get_cut_data_point() for every slot. The decompressed table
232            // can therefore leave later slots empty without changing PATNO numbering.
233            let mut frames: Vec<RgbaImage> = Vec::with_capacity(index_entries);
234            let mut original_sizes = vec![(0, 0); index_entries];
235            for i in 0..index_entries {
236                if i >= debuf_entries {
237                    frames.push(transparent_missing_g00_cut());
238                    continue;
239                }
240                let p_off = pairs_off + i * pair_size;
241                let offset = read_u32le(&debuf, p_off)? as usize;
242                let length_raw = read_u32le(&debuf, p_off + 4)? as i32;
243                if offset == 0 || length_raw == 0 || offset >= debuf.len() {
244                    frames.push(transparent_missing_g00_cut());
245                    continue;
246                }
247
248                // get_cut_data_point() returns g00_data + offset for every
249                // non-zero size (negative means a linked/reused cut). The type-2
250                // C_g00_cut::set_data() implementation does not use data_size at
251                // all; it advances by the fixed cut/chip headers and chip pixels.
252                // Therefore the signed table size must not truncate this parser.
253                let part_bytes = &debuf[offset..];
254                let img = extract_g02_part(part_bytes)
255                    .with_context(|| format!("extract g02 part idx={i}"))?;
256                let part = parse_g02_part_info_prefix(part_bytes)?;
257                original_sizes[i] = (part.width, part.height);
258                frames.push(img);
259            }
260
261            if frames.is_empty() {
262                bail!("type2 produced no frames");
263            }
264
265            Ok(DecodedG00 {
266                kind,
267                width,
268                height,
269                frames,
270                original_sizes,
271            })
272        }
273        G00Type::TypeJpeg => {
274            if off > data.len() {
275                bail!("g00 type3 header out of bounds");
276            }
277            let jpeg = siglus_assets::g00::decode_type3_jpeg_payload(&data[off..]);
278            if !jpeg.starts_with(&[0xFF, 0xD8]) {
279                bail!(
280                    "g00 type3 XOR decode did not produce JPEG SOI: got={:02X?}",
281                    &jpeg[..jpeg.len().min(2)]
282                );
283            }
284            let img = image::load_from_memory_with_format(&jpeg, image::ImageFormat::Jpeg)
285                .or_else(|_| image::load_from_memory(&jpeg))
286                .context("decode g00 type3 jpeg after XOR")?;
287            let rgba = img.to_rgba8();
288            let (w, h) = rgba.dimensions();
289            if w != width || h != height {
290                bail!("g00 jpeg size mismatch: got={w}x{h}, expect={width}x{height}");
291            }
292            Ok(DecodedG00 {
293                kind,
294                width,
295                height,
296                original_sizes: vec![(width, height)],
297                frames: vec![RgbaImage {
298                    width,
299                    height,
300                    center_x: 0,
301                    center_y: 0,
302                    rgba: rgba.into_raw(),
303                }],
304            })
305        }
306    }
307}
308
309fn transparent_missing_g00_cut() -> RgbaImage {
310    RgbaImage {
311        width: 1,
312        height: 1,
313        center_x: 0,
314        center_y: 0,
315        rgba: vec![0, 0, 0, 0],
316    }
317}
318
319fn bgra_to_rgba_inplace(mut bgra: Vec<u8>) -> Vec<u8> {
320    for px in bgra.as_chunks_mut::<4>().0.iter_mut() {
321        let b = px[0];
322        let g = px[1];
323        let r = px[2];
324        let a = px[3];
325        px[0] = r;
326        px[1] = g;
327        px[2] = b;
328        px[3] = a;
329    }
330    bgra
331}
332
333fn real_live_type1_uncompress(compr: &[u8]) -> Result<(Vec<u8>, usize)> {
334    if compr.len() < 8 {
335        bail!("type1 data too small");
336    }
337    // LzssUnPack() reads [arc_size, org_size] but the original decoder does
338    // not use arc_size to terminate decoding; it stops when org_size bytes
339    // have been produced. Do the same, while retaining safe input bounds.
340    let _arc_size = read_u32le(compr, 0)? as usize;
341    let uncomprlen = read_u32le(compr, 4)? as usize;
342    if uncomprlen == 0 {
343        bail!("type1 org_size=0");
344    }
345
346    let mut out = vec![0u8; uncomprlen];
347    let mut curbyte = 8usize;
348    let mut act = 0usize;
349    let mut bit_count = 0u8;
350    let mut flag = 0u8;
351
352    while act < uncomprlen {
353        if bit_count == 0 {
354            if curbyte >= compr.len() {
355                bail!(
356                    "type1 truncated before output complete: wrote={} expected={}",
357                    act,
358                    uncomprlen
359                );
360            }
361            flag = compr[curbyte];
362            curbyte += 1;
363            bit_count = 8;
364        }
365
366        if (flag & 1) != 0 {
367            if curbyte >= compr.len() {
368                bail!("type1 truncated literal");
369            }
370            out[act] = compr[curbyte];
371            act += 1;
372            curbyte += 1;
373        } else {
374            if curbyte + 2 > compr.len() {
375                bail!("type1 truncated backreference");
376            }
377            let count0 = compr[curbyte] as usize;
378            let b1 = compr[curbyte + 1] as usize;
379            curbyte += 2;
380
381            let offset = (b1 << 4) | (count0 >> 4);
382            let count = (count0 & 0xF) + 2;
383            if offset == 0 {
384                bail!("type1 invalid offset=0");
385            }
386            if act < offset {
387                bail!("type1 backref before start: act={act} offset={offset}");
388            }
389            copy_lzss_match(&mut out, &mut act, offset, count);
390        }
391
392        flag >>= 1;
393        bit_count -= 1;
394    }
395
396    Ok((out, uncomprlen))
397}
398
399/// Forward overlapping LZSS copy. The original x86 code uses forward
400/// `rep movsb`/`rep movsd`, so bytes written by the match may immediately
401/// become source bytes for the remainder of the same match.
402fn copy_lzss_match(dst: &mut [u8], position: &mut usize, offset: usize, count: usize) {
403    debug_assert!(offset > 0 && *position >= offset && *position <= dst.len());
404    let source = *position - offset;
405    let end = *position + count.min(dst.len() - *position);
406    while *position < end {
407        let available = *position - source;
408        let size = available.min(end - *position);
409        dst.copy_within(source..source + size, *position);
410        *position += size;
411    }
412}
413
414fn lzss_decompress(src: &[u8], dst: &mut [u8]) -> Result<()> {
415    let mut s = 0usize;
416    let mut d = 0usize;
417    while d < dst.len() {
418        if s >= src.len() {
419            break;
420        }
421        let mut flags = src[s];
422        s += 1;
423        for _ in 0..8 {
424            if d >= dst.len() {
425                break;
426            }
427            if (flags & 1) != 0 {
428                if s >= src.len() {
429                    break;
430                }
431                dst[d] = src[s];
432                d += 1;
433                s += 1;
434            } else {
435                if s + 2 > src.len() {
436                    break;
437                }
438                let w = u16::from_le_bytes([src[s], src[s + 1]]) as usize;
439                s += 2;
440                let offset = w >> 4;
441                let count = (w & 0xF) + 2;
442                if offset == 0 {
443                    bail!("lzss offset=0");
444                }
445                if d < offset {
446                    bail!("lzss backref before start: d={d} offset={offset}");
447                }
448                copy_lzss_match(dst, &mut d, offset, count);
449            }
450            flags >>= 1;
451        }
452    }
453
454    if d != dst.len() {
455        bail!(
456            "lzss_decompress did not fill output: wrote {d} of {}",
457            dst.len()
458        );
459    }
460    Ok(())
461}
462
463fn lzss_decompress_24bit(src: &[u8], dst: &mut [u8]) -> Result<()> {
464    // The original expands BGR pixels to four-byte pixels and then reuses
465    // complete pixels for backreferences. Rust stores decoded images as RGBA,
466    // so perform the BGR->RGB permutation once at literal insertion.
467    let mut s = 0usize;
468    let mut d = 0usize;
469    while d < dst.len() {
470        if s >= src.len() {
471            break;
472        }
473        let mut flags = src[s];
474        s += 1;
475        for _ in 0..8 {
476            if d >= dst.len() {
477                break;
478            }
479            if (flags & 1) != 0 {
480                if s + 3 > src.len() {
481                    break;
482                }
483                if d + 4 > dst.len() {
484                    bail!("lzss24 literal would overflow dst");
485                }
486                dst[d] = src[s + 2];
487                dst[d + 1] = src[s + 1];
488                dst[d + 2] = src[s];
489                dst[d + 3] = 0xFF;
490                d += 4;
491                s += 3;
492            } else {
493                if s + 2 > src.len() {
494                    break;
495                }
496                let w = u16::from_le_bytes([src[s], src[s + 1]]) as usize;
497                s += 2;
498                let offset_bytes = (w >> 4) << 2; // (word>>4)*4
499                let dword_count = (w & 0xF) + 1;
500                let count_bytes = dword_count * 4;
501                if offset_bytes == 0 {
502                    bail!("lzss24 offset_bytes=0");
503                }
504                if d < offset_bytes {
505                    bail!("lzss24 backref before start: d={d} offset={offset_bytes}");
506                }
507                copy_lzss_match(dst, &mut d, offset_bytes, count_bytes);
508            }
509            flags >>= 1;
510        }
511    }
512
513    if d != dst.len() {
514        bail!(
515            "lzss_decompress_24bit did not fill output: wrote {d} of {}",
516            dst.len()
517        );
518    }
519    Ok(())
520}
521
522#[cfg(test)]
523mod lzss_fast_path_tests {
524    use super::*;
525
526    #[test]
527    fn overlapping_match_matches_original_forward_byte_copy() {
528        for offset in 1..=16 {
529            for count in 1..=32 {
530                let mut expected: Vec<u8> = (0..64).map(|i| (i * 37) as u8).collect();
531                let mut actual = expected.clone();
532                let mut expected_pos = 32usize;
533                for _ in 0..count {
534                    if expected_pos >= expected.len() {
535                        break;
536                    }
537                    expected[expected_pos] = expected[expected_pos - offset];
538                    expected_pos += 1;
539                }
540                let mut actual_pos = 32usize;
541                copy_lzss_match(&mut actual, &mut actual_pos, offset, count);
542                assert_eq!(actual_pos, expected_pos);
543                assert_eq!(actual, expected, "offset={offset} count={count}");
544            }
545        }
546    }
547
548    #[test]
549    fn type0_literal_is_emitted_as_rgba_before_backreference_reuse() {
550        // flag=1: one literal BGR=(10,20,30). The helper representation is
551        // tested directly because the file header is unrelated to LZSS.
552        let mut dst = [0u8; 4];
553        lzss_decompress_24bit(&[1, 10, 20, 30], &mut dst).unwrap();
554        assert_eq!(dst, [30, 20, 10, 255]);
555    }
556}
557
558#[derive(Debug, Clone)]
559struct G02PartInfo {
560    _part_type: u8,
561    block_count: u16,
562    hs_orig_x: i32,
563    hs_orig_y: i32,
564    width: u32,
565    height: u32,
566    screen_show_x: i32,
567    screen_show_y: i32,
568    full_part_width: u32,
569    full_part_height: u32,
570}
571
572#[derive(Debug, Clone)]
573struct G02BlockInfo {
574    orig_x: u16,
575    orig_y: u16,
576    _sprite: bool,
577    width: u16,
578    height: u16,
579}
580
581// MSVC default packing used by the original g00 library:
582// G00_CUT_HEADER_STRUCT  = BYTE + pad + WORD + 8*i32 + 20*i32 = 0x74
583// G00_CHIP_HEADER_STRUCT = 2*WORD + BYTE + pad + 2*WORD + pad2 + 20*i32 = 0x5c
584const G02_PART_INFO_SIZE: usize = 0x74;
585const G02_BLOCK_INFO_SIZE: usize = 0x5c;
586
587fn parse_g02_block(buf: &[u8]) -> Result<G02BlockInfo> {
588    if buf.len() < G02_BLOCK_INFO_SIZE {
589        bail!("G00_CHIP_HEADER_STRUCT truncated");
590    }
591    let orig_x = read_u16le(buf, 0)?;
592    let orig_y = read_u16le(buf, 2)?;
593    let sprite = buf[4] == 1;
594    let width = read_u16le(buf, 6)?;
595    let height = read_u16le(buf, 8)?;
596    Ok(G02BlockInfo {
597        orig_x,
598        orig_y,
599        _sprite: sprite,
600        width,
601        height,
602    })
603}
604
605fn parse_g02_part_info_prefix(buf: &[u8]) -> Result<G02PartInfo> {
606    // G00_CUT_HEADER_STRUCT from g00.cpp, using MSVC default packing:
607    //   BYTE type; BYTE padding; WORD count;
608    //   int x,y,disp_xl,disp_yl,xc,yc,cut_xl,cut_yl;
609    //   int keep[20];
610    if buf.len() < G02_PART_INFO_SIZE {
611        bail!("G00_CUT_HEADER_STRUCT truncated");
612    }
613    let part_type = buf[0];
614    let block_count = read_u16le(buf, 2)?;
615    let disp_x = read_i32le(buf, 4)?;
616    let disp_y = read_i32le(buf, 8)?;
617    let disp_width = read_u32le(buf, 0x0C)?;
618    let disp_height = read_u32le(buf, 0x10)?;
619    let center_x = read_i32le(buf, 0x14)?;
620    let center_y = read_i32le(buf, 0x18)?;
621    let cut_width = read_u32le(buf, 0x1C)?;
622    let cut_height = read_u32le(buf, 0x20)?;
623    Ok(G02PartInfo {
624        _part_type: part_type,
625        block_count,
626        hs_orig_x: disp_x,
627        hs_orig_y: disp_y,
628        width: cut_width,
629        height: cut_height,
630        screen_show_x: center_x,
631        screen_show_y: center_y,
632        full_part_width: disp_width,
633        full_part_height: disp_height,
634    })
635}
636
637fn extract_g02_part(part_bytes: &[u8]) -> Result<RgbaImage> {
638    // Original C_d3d_texture::load_g00_cut() creates the D3D texture from
639    // cut_info.disp_rect, not from the whole cut_xl/cut_yl canvas. Each chip is
640    // copied to chip.x - disp_rect.left, chip.y - disp_rect.top, and texture
641    // center is cut_info.center - disp_rect.left/top.
642    let part = parse_g02_part_info_prefix(part_bytes).context("parse part prefix")?;
643    if part.width == 0 || part.height == 0 {
644        bail!("g02 part has zero full-cut dimensions");
645    }
646    if part.full_part_width == 0 || part.full_part_height == 0 {
647        bail!("g02 part has zero display dimensions");
648    }
649
650    // The original does not probe alternative layouts. It advances by
651    // sizeof(G00_CUT_HEADER_STRUCT), which is 0x74 with the engine's MSVC ABI.
652    let header_size = G02_PART_INFO_SIZE;
653
654    let out_w = part.full_part_width;
655    let out_h = part.full_part_height;
656    let stride = (out_w as usize).checked_mul(4).context("stride overflow")?;
657    let mut dib = vec![0u8; stride * (out_h as usize)];
658
659    let mut off = header_size;
660    for _ in 0..part.block_count {
661        let block = parse_g02_block(&part_bytes[off..])?;
662        off += G02_BLOCK_INFO_SIZE;
663
664        let bw = block.width as usize;
665        let bh = block.height as usize;
666        if bw == 0 || bh == 0 {
667            bail!("g02 block zero size");
668        }
669        let px_len = bw
670            .checked_mul(bh)
671            .and_then(|v| v.checked_mul(4))
672            .context("pixel len overflow")?;
673        if off + px_len > part_bytes.len() {
674            bail!("g02 block pixel data out of bounds");
675        }
676        let src = &part_bytes[off..off + px_len];
677        off += px_len;
678
679        let dst_x_i = block.orig_x as i32 - part.hs_orig_x;
680        let dst_y_i = block.orig_y as i32 - part.hs_orig_y;
681        if dst_x_i < 0 || dst_y_i < 0 {
682            bail!(
683                "g02 block outside display rect: chip=({}, {}) disp=({}, {})",
684                block.orig_x,
685                block.orig_y,
686                part.hs_orig_x,
687                part.hs_orig_y
688            );
689        }
690        let dst_x = dst_x_i as usize;
691        let dst_y = dst_y_i as usize;
692        if dst_x.saturating_add(bw) > out_w as usize || dst_y.saturating_add(bh) > out_h as usize {
693            bail!(
694                "g02 block write outside display rect: dst=({}, {}) size={}x{} out={}x{}",
695                dst_x,
696                dst_y,
697                bw,
698                bh,
699                out_w,
700                out_h
701            );
702        }
703
704        for row in 0..bh {
705            let src_row_off = row * bw * 4;
706            let dst_row_off = (dst_y + row) * stride + dst_x * 4;
707            let dst_end = dst_row_off + bw * 4;
708            if dst_end > dib.len() {
709                bail!("g02 block write out of bounds");
710            }
711            dib[dst_row_off..dst_end].copy_from_slice(&src[src_row_off..src_row_off + bw * 4]);
712        }
713    }
714
715    let rgba = bgra_to_rgba_inplace(dib);
716
717    Ok(RgbaImage {
718        width: out_w,
719        height: out_h,
720        center_x: part.screen_show_x - part.hs_orig_x,
721        center_y: part.screen_show_y - part.hs_orig_y,
722        rgba,
723    })
724}
725
726#[cfg(test)]
727mod original_g00_layout_tests {
728    use super::*;
729
730    fn literal_type1_g00(width: u16, height: u16, raw: &[u8]) -> Vec<u8> {
731        let mut payload = Vec::new();
732        for chunk in raw.chunks(8) {
733            payload.push(if chunk.len() == 8 {
734                0xff
735            } else {
736                ((1u16 << chunk.len()) - 1) as u8
737            });
738            payload.extend_from_slice(chunk);
739        }
740        let mut file = Vec::new();
741        file.push(1); // type 1
742        file.extend_from_slice(&width.to_le_bytes());
743        file.extend_from_slice(&height.to_le_bytes());
744        file.extend_from_slice(&(payload.len() as u32).to_le_bytes());
745        file.extend_from_slice(&(raw.len() as u32).to_le_bytes());
746        file.extend_from_slice(&payload);
747        file
748    }
749
750    #[test]
751    fn type1_expands_palette_indices_like_c_g00_chip_get_data() {
752        // Palette entries are DWORD BGRA in the original. Pixels are byte indices.
753        let mut raw = Vec::new();
754        raw.extend_from_slice(&2u16.to_le_bytes());
755        raw.extend_from_slice(&[10, 20, 30, 40]); // BGRA -> RGBA 30,20,10,40
756        raw.extend_from_slice(&[50, 60, 70, 80]); // BGRA -> RGBA 70,60,50,80
757        raw.extend_from_slice(&[1, 0]);
758
759        let decoded = decode_g00(&literal_type1_g00(2, 1, &raw)).unwrap();
760        assert_eq!(decoded.frames.len(), 1);
761        assert_eq!(decoded.frames[0].rgba, vec![70, 60, 50, 80, 30, 20, 10, 40]);
762    }
763
764    #[test]
765    fn type2_cut_uses_fixed_msvc_headers_and_display_rect_coordinates() {
766        let mut cut = vec![0u8; G02_PART_INFO_SIZE];
767        cut[0] = 2;
768        cut[2..4].copy_from_slice(&1u16.to_le_bytes()); // one chip
769        cut[4..8].copy_from_slice(&10i32.to_le_bytes()); // disp left
770        cut[8..12].copy_from_slice(&20i32.to_le_bytes()); // disp top
771        cut[12..16].copy_from_slice(&2i32.to_le_bytes()); // disp width
772        cut[16..20].copy_from_slice(&1i32.to_le_bytes()); // disp height
773        cut[20..24].copy_from_slice(&11i32.to_le_bytes()); // center x
774        cut[24..28].copy_from_slice(&20i32.to_le_bytes()); // center y
775        cut[28..32].copy_from_slice(&32i32.to_le_bytes()); // full cut width
776        cut[32..36].copy_from_slice(&32i32.to_le_bytes()); // full cut height
777
778        let mut chip = vec![0u8; G02_BLOCK_INFO_SIZE];
779        chip[0..2].copy_from_slice(&10u16.to_le_bytes());
780        chip[2..4].copy_from_slice(&20u16.to_le_bytes());
781        chip[4] = 1; // sprite flag; pixels themselves are still copied verbatim
782        chip[6..8].copy_from_slice(&2u16.to_le_bytes());
783        chip[8..10].copy_from_slice(&1u16.to_le_bytes());
784        cut.extend_from_slice(&chip);
785        cut.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); // two BGRA pixels
786
787        let image = extract_g02_part(&cut).unwrap();
788        assert_eq!((image.width, image.height), (2, 1));
789        assert_eq!((image.center_x, image.center_y), (1, 0));
790        assert_eq!(image.rgba, vec![3, 2, 1, 4, 7, 6, 5, 8]);
791    }
792}