melee_c → from ISO to 2D: write our own extraction

Goal: render all characters, all stages (2D for now), all animations · Start: Fox + Falco, all action states
Extract from the ISO: fighters · fighter colours · animations · stages · stage animations
Direction: no borrowed Zig code — original C extraction, Zig/other code used only as behavioral reference · Plan: 2026-08-15
TL;DR. The end goal is unchanged: a 2D renderer in viewer.c that draws (eventually) every character, costume, and stage, driven by the replay's action_state + anim_frame. What changed is the front half: the first part of this plan is writing a from-scratch C extraction pipeline (tools/extract) that reads the ISO → FST → DAT container → HSD types, and produces cached assets for fighters, fighter colours, animations, stages, and stage animations. We do it in C, as original work: Zig (litewing) is a reference for behavior only — we study what it does and why, then write our own code. Understanding HSD deeply is a prerequisite, not a side quest.

0What exists & what we lean on

~/Dev/fixtures/game.iso 1.46 GB Melee ISO — the source of all assets. melee_c/src/parser.{c,h} Our C .slp parser: per-frame action_state, anim_frame, facing, x/y, character_id, FOD/Whispy/Stadium events. melee_c/src/viewer.c Our C HTTP + software-2D + PNG renderer (browser at melee.zamachnoi.com). litewing (Zig) reference only dat-extractor + melee-viewer prove the pipeline works and reveal the hard parts. We read it for behavior and never copy code; it is not a build dependency. Known-good numbers Validation target (Falco): bones=67, primitive_groups=26, textures=24, actions > 300. Our extractor must reproduce these independently. Toolchain cc 13.3 + existing Makefile. No Zig needed to build melee_c.

Reference sources (behavior, formats, reverse engineering)

1Architecture

ConcernWhere it livesLanguageNote
ISO → cached assets (extraction)tools/extract in melee_cCOriginal implementation. Offline, run on dev box.
Asset loadingsrc/asset.{c,h}CReads the cache produced by extract.
Pose + 2D rastersrc/viewer.c rendererCSame server, PNG pipeline unchanged.
Action-state → animation lookupsrc/asset.c (tables)CCommon states + Fox/Falco specials, name → prefix → raw-index fallback.
House rule: never git subtree, vendor, or transpile litewing. We implement the same formats with our own structure. Shared facts (offsets, byte layouts from the community thread) are fine to reproduce verbatim — that's data, not code.

Data flow

game.iso ──[tools/extract: C]──► /data/cache   (versioned, ISO-fingerprinted)
  │                                  ├─ meta.json                fingerprint, schema ver
  │  FST ──► DAT bytes ──► HSD       ├─ char/<char>-<colour>.model  bones, prims, verts, tex
  │                                  ├─ char/<char>-<colour>.anims  action table + figatree tracks
  │                                  └─ stage/<stage>.stage        sections, scale, camera, stage anims
master.slp ──[parser.c]──► action_state, anim_frame, facing, x, y
     │
     └─[viewer.c render]────────► 4x4 RGBA ──PNG──► browser
         action_state ─lookup→ animation   anim_frame ─sample→ bones
         bones ─skin→ verts                x,y,facing ─project→ 2D ─paint

2Phase A — Understand HSD (the prerequisite)

Goal: a written, precise model of the formats before writing decoder code. This is the milestone that makes extraction possible — do it properly.

  1. ISO/FST layerdocs/DAT.md: GameCube disc structure, FST entries, locating *.dat, reading file bytes.
  2. DAT container → same doc: header, data block, relocation table, root/reference node tables, string table, offset rules (0x20 + dataOffset), what root names mean (ftData*, *figatree*, *_joint, coll_data).
  3. HSD object modeldocs/HSD.md: the scene graph — JOBJ (joints/bones + transforms), DOBJ (draw objects), POBJ (primitive objects: display lists), MOBJ (materials: color, alpha, TEV), TOBJ (textures: TPL image + wrap/scale), COBJ? (lights/cameras). Enumerate every HSD type we must handle for fighters and stages.
  4. Geometry: POBJ display-list encoding for Melee's GX subset (primitive types, positions, normals, colors, UVs, skin weights).
  5. Textures: TPL/CI image formats + TLUT lookup; which fonts/emblem/costume textures live in fighter DATs.
  6. Animationdocs/ANIM.md: figatree — AOBJ-based joint/material/texture tracks, keyframes (constant/step/linear/hermite), track interpolation math; and how the fighter action table (in ftData*, 0x18-byte entries) links to byte slices inside Pl*AJ.dat.
  7. Stagesdocs/STAGES.md: stage DAT layout (GrNLa = FD …), sections, camera/scale/lights, and stage animations (platform movement, Whispy, Stadium transforms).

Exit: reading any of these docs + the community DAT thread should let us explain any byte in a Pl*.dat without peeking at Zig sources.

3Phase B — Build the C extractor (tools/extract)

Goal: original C code, module per format layer, each with a tiny self-check.

ModuleParses / producesSelf-check
iso.cFST index; dat_entry(path) -> bytesKnown DAT paths resolve; sizes sane.
dat.cDAT container → sections, nodes, namesRoot names match the thread's naming.
hsd_jobj.cJOBJ tree → bones, base/inv-bind transformsFalco skeleton: 67 bones.
hsd_dobj.cDOBJ/POBJ/MOBJ → primitive groups, indices, verticesFalco: 26 prim groups.
texture.cTOBJ/TLU + GX formats → RGBAFalco: 24 textures, decodes to correct palette.
colour.ccostume → mesh DAT mapping (e.g. PlFxOr/Bu/Gr/Nr) + material swapEach costume yields a distinct model.
anim.cfigatree → joint/material/texture keyframe tracksTracks interpolate identically at known frames.
action.cftData* action table → linked Pl*AJ.dat slicesFalco: >300 actions, Wait1 resolves.
stage.cstage sections, scale, camera, lights, stage animationsFD: scale ~1.0, sections match.
cache.cserialize all of the above; meta.jsonRe-read round-trips byte-identical.

Exit: tools/extract reproduces every validation number above with zero Zig in the build. Cache never lands in git.

4Phase C — Renderer (the part our viewer already gestures at)

  1. Asset loader (src/asset.{c,h}): mmap cache files into runtime structs; validate schema version + ISO fingerprint; load Fox/Falco on startup (ASSET_DIR=/data/cache).
  2. Pose sampler: action_state → animation; sample at anim_frame (mind the +1 frame quirk); local → world transforms; mirror x when facing < 0.
  3. 2D rasterizer: project with the existing side-view camera; painter's-algorithm triangle fill; flat shading from MOBJ colors first, affine texture mapping next.
  4. /api/pose?char=falco&action=Wait1&frame=30&facing=1 debug endpoint — iterate on one skeleton before touching playback.
  5. Wire into /api/frame: followers/Nana, 4 ports, same character twice, Zelda/Sheik (key models by internal char id).
  6. Stages: real stage meshes + stage animations driven by our existing slp_fod_at / slp_whispy_at / slp_stadium_at events.
  7. Scale: all 26 characters + all costumes; 2.5D perspective + textures; later WebGL 3D is its own project.

5Decisions

#DecisionResolved
1Who writes the extraction?Us, in C, original code. Zig is reference-only. No litewing in the build.
2What counts as "copying"?Facts/offsets from the community thread = fine verbatim. Structure, idioms, and code = ours. No copy-paste from litewing.
3Cache locationDev box /data/cache, versioned + ISO-fingerprinted, never in git.
4First projectionSide view (matches replay x/y; facing → mirror).
5Textures in rendererFlat-shaded first (silhouettes > accuracy); texture decode still built in Phase B for correctness.
6Costume orderDefault (0) for Fox/Falco first, then all colours.

6Risks

7First concrete actions

  1. Write docs/DAT.md + a C ISO/FST → DAT tool in tools/extract; dump a real PlFc.dat header, sections, roots, and names.
  2. Decode the JOBJ tree of PlFc.dat; print the bone hierarchy + base transforms; hit 67 bones.
  3. Decode one DOBJ/POBJ/MOBJ primitive group into vertices/indices; hit 26 prim groups.
  4. Action table + figatree: resolve Falco's Wait1 / a blaster action; hit >300 actions.
  5. First cached Falco asset + /api/pose render of Wait1 in 2D.