QA Ticket Resolution Log 8 / 8 RESOLVED

Ticket submitted by sandmanfan (Discord) · 2026-06-11 · levels 2 / 2a / 2b · fixes authored autonomously by Claude (Fable 5) via Claude Code · live demo: exentt.com/jn-engine · next ticket: 2026-06-11 #2 (12 reports, level 1)

This page documents, line item by line item, how an 8-report QA ticket against the jn-engine JNBG reimplementation was diagnosed and fixed. Each card shows the report as filed, a screenshot from the deprecated engine build that contained the flaw, the evidence trail that located the root cause, the fix, and the same camera shot from the fixed engine. The ticket was filed in-game with the engine's QA annotate tool (B-key picker → markdown/JSON export), which records entity FourCC, class tag, asset reference and world position — those positions were reused verbatim to aim the before/after cameras below.

The Ticket

#levelentityclasscat.report
1level23DINC3DDINOMISmodel is GEP instead of dino. texture is correct. same applies in level2b
2level23SBUC3DBUSTEXtexture is retroland shuttle instead of school bus. same applies in level2b
3level23FISC3DDARWINFISHMISin general, darwins use what seems to be a level editor model instead of the real one
4level23PICkittyMISmissing cat ASE
5level23BALC3DBALLOONTEXmissing balloon sprite for every 3BAL entity. also look into what makes them different colors, as its white by default.
6level2b3CONC3DCONETEXtexture is piggybank instead of cone for all entities. same applies in level2
7level2bSTARTplacementTEXbanner doesn't display "Nick's Finish Line". same applies in level2
8level2b3CHKcheck1aTEXcheckpoint should be crossed flags sprite and not the JNvJN clock. same applies in level2a

MIS = wrong/missing model · TEX = wrong/missing texture. Six of eight items resolved in src/game/entity_visual.c (the entity→visual resolver); item 7 turned out to be a renderer-level D3D7↔OpenGL semantics mismatch. Items 5, 6 and 8 share a single root cause.

1 · 3DIN — wrong dino model MIS

level2 @ (-1299, 30, -1061) · C3DDINO
"model is GEP instead of dino. texture is correct. same applies in level2b"
before: wrong prop mesh
DEPRECATED — resolver bound an unrelated prop mesh from objects.omt
after: real dino
FIXED — the real dinostop.ASE character mesh + dino.png
EVIDENCE TRAIL

The resolver row pointed at assets/ase/omt/dino.ASE — a mesh that happens to be named "dino" inside the objects.omt container (a level prop, not the character). The ground truth was already on disk: the Ghidra-derived decomp spec docs/decomp/C3DDino.md captures the class's InitObject verbatim:

(**(code **)(puVar1 + 0xd8))(s_HIWALK,   s_dinowalk_ase);
(**(code **)(this->vftable + 0xd8))(s_HISHRINK, s_dinoshrink_ASE);
(**(code **)(this->vftable + 0xd8))(s_HISTOP,  s_dinostop_ase);
(**(code **)(this->vftable + 0xf0))(s_dino_png, 0);

The class registers dinostop.ase (+walk/shrink anims) and loads dino.png — both already extracted from the original install. One ls confirmed they exist.

Fix: one-row swap in the resolver's TYPE table → { "3DIN", { "assets/ase/dinostop.ASE", "assets/png/dino.png" } }

2 · 3SBU — shuttle texture instead of school bus TEX

level2 @ (-1275, -1, -2735) · C3DBUS
"texture is retroland shuttle instead of school bus. same applies in level2b"
before: purple Retroland shuttle
DEPRECATED — retrobus.ASE: the purple Retroland shuttle, a different vehicle
after: yellow school bus
FIXED — bus.ASE + bus.png: the yellow school bus
EVIDENCE TRAIL

Both bus meshes ship in the original install; the resolver had picked the wrong one. Their ASE material blocks distinguish them instantly:

bus.ASE:      *BITMAP "D:\neutron\run\png\bus.png"        ← school bus
retrobus.ASE: *BITMAP "D:\Jimmy\busretro.bmp"             ← Retroland shuttle

docs/decomp/C3DBus.md settles it from the binary itself: C3DBus's lazy visual loader "registers HIDEFAULT → bus.ase, loads bus.png into texture slot 0".

Fix: one-row swap → { "3SBU", { "assets/ase/bus.ASE", "assets/png/bus.png" } }

3 · 3FIS — Darwin uses a level-editor model MIS

level2 @ (5874, 12, -364) · C3DDARWINFISH
"in general, darwins use what seems to be a level editor model instead of the real one"
before: flat editor fish
DEPRECATED — fish2.ASE, a flat pickup-item fish
after: Darwin
FIXED — darwinstop.ASE + darwin.png: the actual Darwin character
EVIDENCE TRAIL

The reporter's "level editor model" instinct was exactly right — the bound mesh's own material path betrays its origin as a 3D-items pickup asset, not a character:

fish2.ASE: *BITMAP "D:\Jimmy (ken)\3D Items (pick up)\bmp\fish3.bmp"

docs/decomp/C3DDarwinFish.md shows the class registering darwinwalk.ase / darwinshrink.ASE / darwinstop.ase and loading darwin.png — the same registration pattern as the dino (the two classes share their decompiled InitObject shape).

Fix: one-row swap → { "3FIS", { "assets/ase/darwinstop.ASE", "assets/png/darwin.png" } }

4 · 3PIC "kitty" — missing cat MIS

level2 @ (3616, 904, -1679) · pickup, tag "kitty", authored SpriteIndex 106
"missing cat ASE"
before: question mark sprite
DEPRECATED — a floating "?" placeholder sprite where the cat should sit
after: cat on ledge
FIXED — catsit.ASE + cat.png on the school ledge (note: bus in the background is also fixed)
EVIDENCE TRAIL

This one had two layers. First, the resolver had no per-tag row for "kitty", so it should have fallen to a generic pickup. But the engine logs showed no cat mesh ever loading — because 3PIC entities with an authored SpriteIndex > 0 take a separate billboard shortcut. Decoding what that index actually points at explained the mystery floating "?":

hidden sprite
sprites.omt chunk 106 — a canvas literally named hidden: the original level editor's "hidden object" placeholder

The original game never renders this marker — the kitty pickup (the level's rescue-the-cat objective) shows the cat itself. docs/decomp/C3DKitty.md confirms cat animations + cat.png.

Fix: new tag row { "3PIC", "kitty", { "assets/ase/catsit.ASE", "assets/png/cat.png" } }, plus a new entity_visual_tag_override() check so curated tag visuals take precedence over the SpriteIndex billboard shortcut.

5 · 3BAL — white balloons / missing sprite + colors TEX

level2 @ (14592, 858, 5647) · C3DBALLOON, authored SpriteIndex 50
"missing balloon sprite for every 3BAL entity. also look into what makes them different colors, as its white by default."
before: white square
DEPRECATED — untextured white quad: the curated row referenced a file that doesn't exist
after: tinted balloon
FIXED — real balloon sprite, tinted per-instance from .gam Red/Green/Blue
EVIDENCE TRAIL

Bug A (white quad): the resolver row hardcoded 0050_32x32d32.png — an image-file ordinal from an older interpretation of SpriteIndex. That file doesn't exist (texture load fails → white). The generated sprite-chunk map, built after the SpriteIndex = canvas-chunk-id discovery, already had the right answer:

{ "sprites.omt", 50, ".../0071_128x128d32.png" },  /* balloon */
balloon sprite
chunk 50 — the real balloon canvas

Bug B (colors): a hex dump of a 3BAL record in Level2.gam revealed three unparsed per-instance float properties:

Red   ... 3f000000  = 0.5      (big-endian float)
Green ... 3dcccccd  = 0.1
Blue  ... 3f800000  = 1.0

docs/decomp/C3DBalloon.md (measured from the binary) documents exactly where they go: "Adjusted canvas/material color record set to (Red, Green, Blue, 0.8)". That's what makes each balloon a different color — the engine just wasn't reading them.

Fix: deleted the stale row (the per-instance sprite tier now resolves chunk 50 via the generated map, with the authored SpriteSize), and the resolver now reads Red/Green/Blue from the entity's property bag and applies them as an (R, G, B, 0.8) tint.

6 · 3CON — piggybank instead of cone TEX

level2b @ (-711, 377, 10515) · C3DCONE, authored SpriteIndex 41
"texture is piggybank instead of cone for all entities. same applies in level2"
before: piggybanks
DEPRECATED — piggybank sprites scattered across the dirt course
after: cones
FIXED — orange traffic cones (the now-working START banner is visible top-left)
EVIDENCE TRAIL

Same root cause as the balloons, but this time the wrong file existed — which is why it rendered a piggybank rather than white. The stale row referenced image file 0041_…png; cross-checking the generated chunk map showed file 0041 actually belongs to canvas chunk 160:

{ "sprites.omt", 160, ".../0041_128x128d16.png" },  /* piggybank  ← what the row pointed at */
{ "sprites.omt",  41, ".../0062_64x64d32.png"  },  /* cone       ← what chunk 41 really is  */
piggybank
file 0041 = chunk 160 "piggybank" (what rendered)
cone
chunk 41 = "cone" → file 0062 (what should render)

A file-ordinal ↔ chunk-id collision: for low ids the two numbering schemes coincide (which is why older sprite rows worked), and they diverge exactly where this ticket found bugs. A third stale row (3LEA, leaves) was caught and fixed in the same sweep before anyone reported it.

Fix: deleted the stale 3CON row (and 3LEA) — the per-instance sprite tier resolves the authored chunk id through the generated map.

7 · START — banner doesn't display "Nick's Finish Line" TEX

level2b placement @ (-1175, 0, 10619) · assets/glb/omt/level2b/START.glb
"banner doesn't display 'Nick's Finish Line'. same applies in level2"
before: blank banner
DEPRECATED — the banner renders as perfectly blank cloth
after: banner with text
FIXED — "NICK'S FINISH LINE / Lindbergh Elementary School / Annual Science Fair"
EVIDENCE TRAIL

The hardest item — every layer of the asset pipeline checked out clean, one by one: the banner texture parses correctly from the level container; the exported GLB embeds it on the right faces (verified by decoding the mesh's triangles + materials directly); the GLB loader uploads one GL texture per material group; the placement draw path passes no overrides; culling is off. The data was provably perfect:

finish texture
the 64×64 finish canvas, parsed from level2b.omt — text intact

With the asset exonerated, the suspect had to be runtime state. A raw view/projection camera descriptor was crafted to frame the sign headless — it rendered textured cloth with no text. Dumping the banner primitive's UVs exposed the trick: the banner is two co-planar quads at identical depth — triangles 0–7 sample the blank-cloth band of the texture, triangles 8–15 (drawn second) sample the text band:

tri  0  cloth  v= [-0.55 -0.20 -0.54]      tri  8  TEXT  v= [-0.65 -1.00 -0.99]
tri  1  cloth  v= [-0.20 -0.22 -0.54]      tri  9  TEXT  v= [-0.67 -0.65 -0.99]
...                                         ...      (same positions, same depth)

That's a decal pattern that only works under Direct3D 7's default depth test, D3DCMP_LESSEQUAL — equal-depth fragments pass, so the later text layer wins. OpenGL defaults to GL_LESS — equal-depth fragments fail, so the text layer could never draw a single pixel. The engine had simply never set a depth function.

Fix: glDepthFunc(GL_LEQUAL) at renderer init (matching the D3D7 default the original game ran under), plus adding the symbol to the engine's minimal GL loader. This is a faithfulness fix at the renderer level: any other co-planar decal layering in the original data now resolves the same way it did in 2001.

8 · 3CHK — JNvsJN clock instead of crossed flags TEX

level2b @ (-208, -61, 13484) · checkpoint, authored SpriteIndex 42
"checkpoint should be crossed flags sprite and not the JNvJN clock. same applies in level2a"
before: clock sprite
DEPRECATED — the sequel's (JN vs JN) clock checkpoint sprite
after: crossed flags
FIXED — crossed checkered flags, resolved from the level's own authored sprite
EVIDENCE TRAIL

The engine runs two games (JNBG and its sequel JN vs JN), and the checkpoint visual was a single hard-coded row pointing at the sequel's clock sprite for both. But the QA export itself contained the answer: the JNBG .gam authors SpriteIndex 42 on every checkpoint, and chunk 42 in sprites.omt is a canvas named — literally — flag:

clock
JNvsJN spr_checkpoint (what rendered)
flags
sprites.omt chunk 42 "flag" (what the level authored)
Fix: JNBG checkpoints are now routed to their authored sprite ahead of the hard-coded row; the row remains as the JNvsJN path (that game's checkpoints really are clocks). Done as a narrow special case, since a blanket priority reorder would have broken curated tinted sprites elsewhere.

How These Were Resolved — Process Notes

All eight items were diagnosed and fixed in a single session by Claude (Fable 5) running in Claude Code on the project box, with no human-written code. The general loop:

Efficiency Takeaways