diff --git a/.gitignore b/.gitignore index cd9b04b7c..45cd9db96 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,11 @@ assets/generated/ *.gba *.sav *.z64 +# .nds was missing until Gen 4 arrived: this block was written for the +# Game Boy and GBA carts and never extended when Platinum support began, +# so a Platinum dump sat in the working tree UNIGNORED -- untracked, but one +# `git add -A` away from putting a copyrighted cartridge in a public repo. +*.nds # Inverted Mew sprites the example mod ships: tools/generate_example_mod_sprite.py # derives them from an imported cache, so they are ROM content. mod.card already diff --git a/assets/sky/gen4_sky.png b/assets/sky/gen4_sky.png new file mode 100644 index 000000000..4fa58aa3e Binary files /dev/null and b/assets/sky/gen4_sky.png differ diff --git a/assets/sky/gen4_sky_day.png b/assets/sky/gen4_sky_day.png new file mode 100644 index 000000000..94bc60b04 Binary files /dev/null and b/assets/sky/gen4_sky_day.png differ diff --git a/assets/sky/gen4_sky_night.png b/assets/sky/gen4_sky_night.png new file mode 100644 index 000000000..cfafc9309 Binary files /dev/null and b/assets/sky/gen4_sky_night.png differ diff --git a/data/scripts/init.lua b/data/scripts/init.lua index 9007ac4d3..cdc029a4e 100644 --- a/data/scripts/init.lua +++ b/data/scripts/init.lua @@ -35,6 +35,20 @@ local gen3VM = gen2Data.map_scripts and require("src.script.Gen3ScriptVM") or ni if gen3VM and not gen3VM.store(gen2Data) then gen3VM = nil end if gen3VM then gen3VM.register(gen2Data, "talk") end +-- Gen4 lands in the same module and is gated the same way, and it was the +-- arm that was missing: nothing here ever asked Gen4ScriptVM for anything, so +-- Platinum's 4,079 decoded scripts sat in the cache unlowered and unattached. +-- Pressing A on an NPC in Sinnoh found no talk entry, fell through to the +-- text tables, and warned "no text for Twinleaf Town/nil" -- reported as +-- "talking to npcs does nothing", which is exactly what it was. +-- +-- `register` stamps each object's script label onto its map record first, +-- because a Gen 4 object carries a script id rather than a TEXT constant and +-- the engine looks for the constant. +local gen4VM = gen2Data.map_scripts and require("src.script.Gen4ScriptVM") or nil +if gen4VM and not gen4VM.store(gen2Data) then gen4VM = nil end +if gen4VM then gen4VM.register(gen2Data, "talk") end + -- Maps that Gen2 map_scripts already owns. Gen1 story hand-ports for the -- same map ids (CERULEAN_CITY, ROUTE24→ROUTE_24, etc.) must not attach: -- they override Gen2 talk with pokered rival/Nugget Bridge scripts and @@ -43,7 +57,7 @@ if gen3VM then gen3VM.register(gen2Data, "talk") end -- A Gen 3 cache owns its maps for the same reason a Gen 2 one does: the -- pokered hand-ports are Kanto stories and must not attach over Hoenn. local gen2OwnedMaps = {} -if (gen2VM or gen3VM) and gen2Data.map_scripts and gen2Data.map_scripts.maps then +if (gen2VM or gen3VM or gen4VM) and gen2Data.map_scripts and gen2Data.map_scripts.maps then for mapId in pairs(gen2Data.map_scripts.maps) do gen2OwnedMaps[mapId] = true -- MapScripts.normalizeMapId inserts _ before trailing digits (ROUTE24 @@ -66,7 +80,7 @@ end -- OaksLab is a full Yellow rewrite (one Eevee ball + forced Pikachu); -- Red/Blue keep the three-starter choose flow. Skip on Gen2. -if not (gen2VM or gen3VM) then +if not (gen2VM or gen3VM or gen4VM) then local oaksLab = GameVersion.isYellow() and "data.scripts.oaks_lab_yellow" or "data.scripts.oaks_lab" @@ -86,7 +100,7 @@ end -- end, and mixing the two left the player stuck -- the hand-written Elm -- dialogue never advanced the scene the ROM's "you can't leave yet" script -- checks. They only load when the disassembly is unavailable. -if not (gen2VM or gen3VM) then +if not (gen2VM or gen3VM or gen4VM) then for _, mapEntry in ipairs({ { "PLAYERS_HOUSE1_F", "data.scripts.players_house1f" }, { "MAP_G18_N06", "data.scripts.players_house1f" }, @@ -118,7 +132,7 @@ end -- Yellow-only content on top of the shared tables (talk keys merge per -- TEXT constant): the Kanto-starter gift quests and Jessie & James. -if GameVersion.isYellow() and not (gen2VM or gen3VM) then +if GameVersion.isYellow() and not (gen2VM or gen3VM or gen4VM) then for _, file in ipairs({ "data.scripts.yellow_gifts", "data.scripts.yellow_jessie_james", "data.scripts.yellow_beach_house", @@ -131,6 +145,7 @@ end if gen2VM then gen2VM.register(gen2Data, "scenes") end if gen3VM then gen3VM.register(gen2Data, "scenes") end +if gen4VM then gen4VM.register(gen2Data, "scenes") end local M = {} diff --git a/docs/gen3-window-contact-sheets.zip b/docs/gen3-window-contact-sheets.zip new file mode 100644 index 000000000..2f4b660bd Binary files /dev/null and b/docs/gen3-window-contact-sheets.zip differ diff --git a/docs/gen3-window-metatiles.md b/docs/gen3-window-metatiles.md new file mode 100644 index 000000000..bd1b5c791 --- /dev/null +++ b/docs/gen3-window-metatiles.md @@ -0,0 +1,293 @@ +# Window glass metatiles — Pokémon Emerald and FireRed + +Every metatile in both cartridges that shows **window glass**, by tileset. +Companion to `src/world/Gen3WindowMetatiles.lua`, which carries the same data +as a lookup table. + +## Where this came from + +**Neither pret disassembly labels window metatiles.** pokeemerald's +`include/constants/metatile_labels.h` has no `Window` macro at all; +pokefirered's only ones are Silph Co.'s scripted elevator window +(`METATILE_SilphCo_ElevatorWindow_Top0/Mid0/Bottom0` … `0x2E8`, `0x2F0`, +`0x2F8` and neighbours), which is an animation, not a building window. There is +no list in the ROM either — nothing in the metatile attributes, the behaviour +byte or the collision bits distinguishes glass from any other wall. + +So this was read off the art. Every metatile of all 136 tilesets in the two +cartridges was composited exactly as the game draws it — both layers, +per-quadrant H/V flips, the pair's own palette banks (6 primary palettes in +Emerald, 7 in FireRed) — rendered at 4× onto labelled contact sheets, and +inspected. Secondary tilesets were paired with a primary they are actually +used with, so their palettes resolve the way the game resolves them. + +## What counts + +**Included** — a metatile you can see glass in. Windows are normally two or +four cells and panes straddle cell edges, so a cell holding part of a pane +counts even when most of it is wall. Interior windows count as well as +exterior ones. + +**Excluded** — frame, sill, shutter and curtain cells with no glass showing; +water of every kind (sea, ponds, puddles, waterfalls, fountains); blue roofs, +floors and carpet; TV and PC screens, the Pokémon Center healing machine's +display, Game Corner machines; signs, banners and framed pictures; ice and +crystal; and glass-fronted furniture — a display case is glass, but it is not +a window. + +**Doors are listed separately.** A shop's glass double door is glass and is +not a window, so it sits in its own column and in `M.doors`. + +## Checks that were run + +- Every id falls inside its own tileset's metatile range. **0 failures.** +- Window glass is drawn from a small set of 8×8 tiles, so any metatile *not* + on the list that uses a tile appearing **only** in listed windows would be a + miss. **0 found**, across all 136 tilesets. +- Every id was checked against the shipped blockdata of every map. 953 of the + 1,069 appear on at least one real map; the other 116 are marked ⚠ below and + listed in `M.unused`. They are real window art in the tileset that no Hoenn + or Kanto map happens to place — a map editor wants them, a renderer walking + the shipped world will never meet one. + +## Totals + +| | Emerald | FireRed | Both | +|---|---:|---:|---:| +| Tilesets examined | 73 | 63 | 136 | +| Tilesets with glass | 33 | 43 | 76 | +| Window metatiles | 570 | 499 | 1069 | +| Glass doors | 66 | 33 | 99 | + +Metatile ids are in the pair's numbering: below `metatilesInPrimary` +(512 Emerald, 640 FireRed) they belong to the primary tileset, at or above it +to the secondary. ⚠ marks an id no shipped map places. + +## Emerald + +### Tilesets with window glass + +| Tileset | What it is | Window glass | Glass doors | +|---|---|---|---| +| `TILESET_03DF704` | EMERALD PRIMARY outdoor: grass/trees/cliffs/sea plus Center/Mart/Gym fronts | `0x00B`, `0x013`, `0x1C0`, `0x1C1`, `0x1C2`, `0x1C3`, `0x1C4` | `0x021`, `0x1CD` | +| `TILESET_03DF71C` | small Emerald town exterior | `0x20F`, `0x217`, `0x232`, `0x23A`, `0x262` ⚠, `0x263` ⚠, `0x26A` ⚠, `0x26B` ⚠, `0x27D`, `0x287`, `0x28F` | `0x238`, `0x248` | +| `TILESET_03DF734` | port/market town exterior (Slateport-like) | `0x23A`, `0x242`, `0x258`, `0x25C`, `0x25D`, `0x260`, `0x264`, `0x265` | `0x262` | +| `TILESET_03DF74C` | large coastal city exterior, window-grid buildings | `0x20B`, `0x20C`, `0x20D`, `0x20E`, `0x20F`, `0x211`, `0x212`, `0x213`, `0x214`, `0x215`, `0x216`, `0x217`, `0x218`, `0x21D`, `0x21E`, `0x21F`, `0x226`, `0x234`, `0x235`, `0x237`, `0x290` ⚠, `0x291` ⚠, `0x2A0` ⚠, `0x2A1` ⚠, `0x2A3` ⚠, `0x2A4` ⚠, `0x2AC` ⚠, `0x2B2` ⚠, `0x2B5` ⚠, `0x2B6` ⚠, `0x2CB` ⚠, `0x2CC` ⚠, `0x2CD` ⚠, `0x2D3` ⚠, `0x2D4` ⚠, `0x2D5` ⚠, `0x2E0` ⚠, `0x2E1` ⚠, `0x2E2` ⚠, `0x2E3` ⚠, `0x2E8` ⚠, `0x2E9` ⚠, `0x2EA` ⚠, `0x2EB` ⚠, `0x2F0` ⚠, `0x2F1` ⚠, `0x2F2` ⚠, `0x2F3` ⚠, `0x2F8` ⚠, `0x2F9` ⚠, `0x2FA` ⚠ | `0x2AA`, `0x2FB` | +| `TILESET_03DF764` | Battle Frontier outdoor resort | `0x205`, `0x206` ⚠, `0x207`, `0x20D`, `0x20E`, `0x20F`, `0x225`, `0x226` ⚠, `0x227`, `0x268`, `0x269`, `0x280` ⚠, `0x281` ⚠, `0x282` ⚠, `0x283` ⚠, `0x284` ⚠, `0x2BD`, `0x2BF`, `0x2D1`, `0x2D9` ⚠ | `0x38B`, `0x38C`, `0x393`, `0x394` | +| `TILESET_03DF77C` | large outdoor town/plaza exterior | `0x264` ⚠, `0x265`, `0x26B`, `0x26C`, `0x26D`, `0x26F`, `0x27E`, `0x27F`, `0x286`, `0x287`, `0x2A0`, `0x2A1`, `0x2A2`, `0x2A3`, `0x2A6`, `0x2A7`, `0x2A8`, `0x2A9`, `0x2AA`, `0x2D8`, `0x2D9`, `0x2DC`, `0x2E0`, `0x2E1`, `0x2E4`, `0x330`, `0x332`, `0x333`, `0x338`, `0x33A`, `0x33B`, `0x340`, `0x341`, `0x342`, `0x343`, `0x354`, `0x355`, `0x357`, `0x359`, `0x35B`, `0x360`, `0x361`, `0x362`, `0x363`, `0x364`, `0x365` ⚠, `0x367`, `0x38B`, `0x393`, `0x394`, `0x39B` ⚠, `0x3A0`, `0x3A1`, `0x3A2` ⚠, `0x3A3` ⚠, `0x3A8`, `0x3AA`, `0x3AB`, `0x3AC`, `0x3E7`, `0x3EF`, `0x3FB`, `0x3FC` | `0x289`, `0x2AB`, `0x2AC`, `0x2B8`, `0x315`, `0x31D`, `0x348`, `0x34A`, `0x35D`, `0x3CC`, `0x3CD`, `0x3D4`, `0x3D5` | +| `TILESET_03DF794` | lava/hideout cave with ship rooms; two panes above the counter | `0x239`, `0x23A` | — | +| `TILESET_03DF7AC` | seaside town exterior, green shophouses with cyan window bands | `0x29E`, `0x2A6`, `0x2A8`, `0x2FF`, `0x307` | `0x364`, `0x36C` | +| `TILESET_03DF7C4` | forest/route exterior plus domed building with curved cyan glass walls | `0x2B8`, `0x2B9`, `0x2BA`, `0x2BD`, `0x2BE`, `0x2C5`, `0x2C6`, `0x2C8`, `0x2C9`, `0x2CD`, `0x2CE`, `0x2F0`, `0x2F1`, `0x2F8`, `0x2F9`, `0x300`, `0x301` | `0x2C2` | +| `TILESET_03DF7DC` | coastal city exterior, banked facade windows | `0x20D`, `0x20E`, `0x20F`, `0x215`, `0x216`, `0x217`, `0x225`, `0x226`, `0x22D`, `0x22E`, `0x22F`, `0x235`, `0x236`, `0x237`, `0x23D`, `0x23E`, `0x23F`, `0x243`, `0x244`, `0x247`, `0x24A`, `0x24B`, `0x24C`, `0x24D`, `0x24E` ⚠, `0x252`, `0x253`, `0x254`, `0x255`, `0x256` ⚠, `0x25B`, `0x25C`, `0x25D`, `0x25E`, `0x25F`, `0x265`, `0x266`, `0x267`, `0x271`, `0x272`, `0x2D6`, `0x2D7`, `0x2E6` ⚠, `0x2ED`, `0x2FB`, `0x306`, `0x307`, `0x30B`, `0x30C`, `0x30D`, `0x310`, `0x311` ⚠, `0x324`, `0x326`, `0x32C` ⚠, `0x32D`, `0x32E` ⚠, `0x32F`, `0x333`, `0x335`, `0x336`, `0x337`, `0x344`, `0x345`, `0x34B`, `0x34C`, `0x34E`, `0x34F`, `0x357` ⚠ | `0x246`, `0x249`, `0x2FD` | +| `TILESET_03DF7F4` | seaside/undersea exterior, facility walls with small windows | `0x2DD`, `0x2EC`, `0x2ED`, `0x2EE`, `0x3B8`, `0x3B9`, `0x3BC`, `0x3BD`, `0x3C0`, `0x3C1`, `0x3C4`, `0x3C5` | — | +| `TILESET_03DF80C` | Emerald city building fronts, blue glass storefront and Contest-Hall facade | `0x201`, `0x202`, `0x213`, `0x214`, `0x215`, `0x216`, `0x217`, `0x21B`, `0x21C`, `0x21D`, `0x21E`, `0x21F`, `0x228` | `0x212` | +| `TILESET_03DF83C` | Sootopolis City exterior | `0x23F`, `0x250` ⚠ | `0x20C`, `0x214`, `0x216`, `0x21C`, `0x21E`, `0x224`, `0x22C`, `0x248` | +| `TILESET_03DF854` | Battle Frontier plaza, long blue glazed wall | `0x282`, `0x28A`, `0x294`, `0x29C`, `0x2A4`, `0x2A5`, `0x2AC`, `0x2C0`, `0x2C1`, `0x2C2`, `0x2C3`, `0x2C4`, `0x2C5`, `0x2C6`, `0x2C7`, `0x2C9`, `0x2CA`, `0x2CB`, `0x2CC`, `0x2CD`, `0x2CE`, `0x2CF`, `0x2D0`, `0x2D1`, `0x2D2`, `0x2D6`, `0x2D7`, `0x2D8`, `0x2D9`, `0x2DA`, `0x2DB`, `0x2DD`, `0x2DE`, `0x2DF`, `0x38A`, `0x38C`, `0x38E`, `0x38F`, `0x392`, `0x394`, `0x395`, `0x396`, `0x397`, `0x3A0`, `0x3A1`, `0x3A2`, `0x3A3`, `0x3A4`, `0x3A5` | — | +| `TILESET_03DF86C` | Battle Frontier outdoor, glass-curtain-wall towers | `0x289`, `0x292` ⚠, `0x293` ⚠, `0x294` ⚠, `0x2A1`, `0x2A2`, `0x2A3`, `0x2A4`, `0x2B7`, `0x2BF`, `0x2C5`, `0x2C7`, `0x2CF`, `0x2D3`, `0x2D4`, `0x2D6`, `0x2D8`, `0x2DB`, `0x2DC`, `0x2DE`, `0x2E0`, `0x2E8`, `0x2EB`, `0x2EC`, `0x2EE`, `0x2F0`, `0x2F3`, `0x2F8`, `0x2F9`, `0x2FA`, `0x2FB`, `0x2FD`, `0x2FE`, `0x300`, `0x301`, `0x302`, `0x303`, `0x305`, `0x306`, `0x313`, `0x315`, `0x316`, `0x31E`, `0x348`, `0x34A`, `0x34B`, `0x34E`, `0x34F`, `0x350`, `0x352`, `0x353`, `0x355`, `0x356`, `0x357`, `0x35B`, `0x35D`, `0x36A`, `0x370`, `0x371`, `0x372`, `0x378`, `0x379` ⚠, `0x380`, `0x381`, `0x388` ⚠, `0x389`, `0x38A`, `0x38C`, `0x38D`, `0x38E`, `0x38F`, `0x392`, `0x394`, `0x395`, `0x396`, `0x397`, `0x3AB`, `0x3AC`, `0x3D9` ⚠, `0x3DB` ⚠, `0x3E1` ⚠, `0x3E2` ⚠, `0x3E3` ⚠, `0x3EC` | `0x235`, `0x252`, `0x354`, `0x39B` | +| `TILESET_03DF89C` | Lilycove-style department store interior | `0x232`, `0x23A`, `0x240` ⚠, `0x241`, `0x242`, `0x2A2`, `0x2A3` | `0x219` | +| `TILESET_03DF8E4` | school/lab classroom interior | `0x211`, `0x212`, `0x218`, `0x219`, `0x21A`, `0x21B` ⚠ | — | +| `TILESET_03DF8FC` | indoor shopping arcade with a large multi-pane glazed frontage | `0x254` ⚠, `0x255` ⚠, `0x256` ⚠, `0x257` ⚠, `0x25C`, `0x25D`, `0x25E`, `0x25F`, `0x260`, `0x261`, `0x262` | — | +| `TILESET_03DF944` | passenger-ship interior (S.S. Tidal) | `0x210`, `0x211`, `0x212`, `0x213`, `0x216`, `0x217`, `0x218`, `0x219`, `0x21A`, `0x21B`, `0x21E`, `0x21F`, `0x221`, `0x226`, `0x229`, `0x22E` | — | +| `TILESET_03DF9A4` | interior, pale-blue walls, framed strip of sky behind counters | `0x210`, `0x211`, `0x213`, `0x217`, `0x21C`, `0x23D`, `0x23F` | `0x23E` | +| `TILESET_03DF9BC` | large ship set | `0x2F8`, `0x300`, `0x35A` ⚠, `0x35E` ⚠, `0x35F` ⚠, `0x389`, `0x3B6`, `0x3B7` | — | +| `TILESET_03DF9D4` | pale-green lab / research-centre interior | `0x229`, `0x22C`, `0x22D`, `0x231`, `0x234`, `0x235`, `0x26F`, `0x277` | — | +| `TILESET_03DFA34` | outdoor fairground/market on sand | `0x23D` ⚠ | `0x23F` | +| `TILESET_03DFAF4` | Hoenn house-interior secondary, small two-pane windows | `0x21E`, `0x24A`, `0x25D`, `0x25E` ⚠, `0x2B3` | — | +| `TILESET_03DFB6C` | indoor house/apartment set | `0x20F`, `0x238`, `0x28E`, `0x28F`, `0x2C6`, `0x2C7`, `0x306`, `0x307`, `0x30E`, `0x30F`, `0x339`, `0x33A`, `0x341`, `0x342`, `0x36E`, `0x36F`, `0x3E9` ⚠, `0x3EA` ⚠, `0x3F9` | — | +| `TILESET_03DFB84` | contest-hall / theatre interior with wall windows | `0x206`, `0x207`, `0x209`, `0x20A`, `0x20B`, `0x211`, `0x212`, `0x213`, `0x219`, `0x21A`, `0x21B`, `0x23C`, `0x23D`, `0x23E`, `0x23F` | — | +| `TILESET_03DFBFC` | wood-and-tatami house interior, two 2x2 windows | `0x238`, `0x239`, `0x23B` ⚠, `0x23C` ⚠, `0x240`, `0x241`, `0x243` ⚠, `0x244` ⚠ | — | +| `TILESET_03DFC44` | abandoned-ship interior, intact and shattered portholes | `0x209`, `0x211`, `0x2A8`, `0x2B0`, `0x2B8`, `0x2C0`, `0x2CC`, `0x2CD`, `0x2D4`, `0x2D5` | — | +| `TILESET_03DFC7C` | battle/contest arena interiors plus a glazed corridor | `0x271`, `0x27D`, `0x27E`, `0x27F`, `0x285`, `0x286`, `0x287`, `0x28D`, `0x28E`, `0x28F`, `0x29F` ⚠ | — | +| `TILESET_03DFCC4` | Battle Frontier outdoor plaza | `0x209`, `0x20A`, `0x256`, `0x257` | `0x25E` | +| `TILESET_03DFCDC` | Pokemon Center / Mart interior; front glazing is yellow-tinted with a shine streak | `0x23D`, `0x23E`, `0x23F`, `0x2D5`, `0x2D6`, `0x2D7`, `0x2DD`, `0x2DE`, `0x2DF` | `0x274`, `0x275` | +| `TILESET_03DFDB4` | white marble public-building interior, large sea-view windows | `0x218` ⚠, `0x219` ⚠, `0x253`, `0x254`, `0x255` | — | +| `TILESET_03DFDE4` | indoor hall/lobby, arched and rectangular teal-glass windows | `0x206`, `0x207`, `0x20E`, `0x20F`, `0x21C`, `0x21D`, `0x22D` | — | + +### Tilesets with no window glass (40) + +- `TILESET_03DF824` — Pacifidlog: reed huts on rafts, no glazing — glass doors: `0x21A` +- `TILESET_03DF884` — EMERALD PRIMARY tile-bank: 8 metatiles, a PC unit and a mat +- `TILESET_03DF8B4` — department-store / mall interior +- `TILESET_03DF8CC` — cave/desert secondary +- `TILESET_03DF92C` — purple-grey cave interior +- `TILESET_03DF95C` — space-centre / submarine interior +- `TILESET_03DF974` — small Japanese-style shop interior; 20E,20F are display cases +- `TILESET_03DF98C` — garden/lounge decor; 214-216 are paintings +- `TILESET_03DF9EC` — mossy green cave/canyon +- `TILESET_03DFA04` — Secret Base decoration set +- `TILESET_03DFA1C` — Secret Base decoration set +- `TILESET_03DFA4C` — Secret Base decoration set +- `TILESET_03DFA64` — Secret Base decoration set +- `TILESET_03DFA7C` — indoor decoration set (Secret Base style) +- `TILESET_03DFA94` — small dark ship/submarine bunk room +- `TILESET_03DFAC4` — contest-hall interior; palette has no cyan glass colour +- `TILESET_03DFADC` — museum/mansion interior, framed paintings — glass doors: `0x206`, `0x26B` +- `TILESET_03DFB0C` — lab/hospital/office interior, no exterior glazing +- `TILESET_03DFB24` — purple crystal cave interior +- `TILESET_03DFB3C` — house-interior furniture set +- `TILESET_03DFB54` — ice/crystal themed interior +- `TILESET_03DFB9C` — ornate cream/pink indoor hall, no glazing +- `TILESET_03DFBB4` — dark hideout/facility interior +- `TILESET_03DFBCC` — ornate indoor hall (Mauville Gym family) +- `TILESET_03DFBE4` — brick-walled indoor room +- `TILESET_03DFC14` — lab/greenhouse interior +- `TILESET_03DFC2C` — decoration/furniture set +- `TILESET_03DFC5C` — EMERALD PRIMARY: two metatiles, Secret Base base layer +- `TILESET_03DFC94` — very large mixed indoor tileset; blue square panels read as wall panelling +- `TILESET_03DFCAC` — ornate red-and-gold arena interior +- `TILESET_03DFCF4` — Contest Hall; blue items are monitor screens +- `TILESET_03DFD0C` — large indoor hall with a green pitch +- `TILESET_03DFD24` — volcanic hideout interior +- `TILESET_03DFD3C` — large cave set; blue items are gems/ice/water +- `TILESET_03DFD54` — interior, tan wood-slat walls with machinery; 218-221 is a screen +- `TILESET_03DFD6C` — indoor water-garden/atrium +- `TILESET_03DFD84` — multi-room facility interior +- `TILESET_03DFD9C` — desert/sand and rocky mountain terrain +- `TILESET_03DFDCC` — indoor battle/contest venue; only glass is the sliding entrance doors — glass doors: `0x252`, `0x254`, `0x255`, `0x257`, `0x25A`, `0x25C`, `0x25D`, `0x25F`, `0x262`, `0x263`, `0x264`, `0x26A`, `0x26B`, `0x26C` +- `TILESET_03DFDFC` — battle-arena interior + +## FireRed + +### Tilesets with window glass + +| Tileset | What it is | Window glass | Glass doors | +|---|---|---|---| +| `TILESET_02D4A94` | FireRed primary outdoor: town/route plus house/Mart/Gym/Center facades | `0x006`, `0x007`, `0x018`, `0x019`, `0x020`, `0x021`, `0x042`, `0x043`, `0x04F` ⚠, `0x057`, `0x058`, `0x059`, `0x05B`, `0x062`, `0x150`, `0x151`, `0x154`, `0x155`, `0x156`, `0x180`, `0x181`, `0x182` ⚠, `0x18B` ⚠, `0x18C` ⚠, `0x197`, `0x19B` ⚠, `0x19C` ⚠, `0x1B5`, `0x1B6`, `0x1B7` | `0x03D`, `0x047`, `0x15B` | +| `TILESET_02D4AAC` | small-town building exteriors, porthole windows | `0x299`, `0x29A`, `0x29B`, `0x2A1`, `0x2A2`, `0x2B0`, `0x2B1`, `0x2B8`, `0x2B9`, `0x2C0`, `0x2C1`, `0x2C2`, `0x2C3`, `0x2C4`, `0x2C5`, `0x2C9`, `0x2CA` ⚠, `0x2CB`, `0x2CC`, `0x2D0`, `0x2D8` | `0x2A3`, `0x2B3`, `0x2B4` | +| `TILESET_02D4AC4` | town exterior: houses, Center/Mart with tall pale-blue windows | `0x29A`, `0x29B`, `0x2A5`, `0x2A6`, `0x2B5`, `0x2B7`, `0x2B8`, `0x2BB`, `0x2BD`, `0x2BF`, `0x2C3`, `0x2C8`, `0x2C9` ⚠, `0x2CA`, `0x2CB`, `0x2D0`, `0x2D1` ⚠, `0x2D2`, `0x2D4`, `0x2D6`, `0x2DC` ⚠, `0x2DE` ⚠ | `0x2C6` | +| `TILESET_02D4ADC` | city exterior, six-pane blue windows | `0x29C`, `0x2A9`, `0x2AD`, `0x2B8`, `0x2B9` | — | +| `TILESET_02D4AF4` | town exterior, cream houses with blue-pane windows | `0x28A`, `0x28B`, `0x28C`, `0x29A`, `0x29B`, `0x29C`, `0x2B6`, `0x2B7`, `0x2BD`, `0x2BE`, `0x2BF`, `0x2C0`, `0x2F4`, `0x2F5`, `0x2F6`, `0x2F7` | — | +| `TILESET_02D4B0C` | pale-green domed civic building exterior | `0x2D4`, `0x2D5`, `0x2DC`, `0x2DD`, `0x2E4`, `0x2E5`, `0x320`, `0x321`, `0x323`, `0x328`, `0x329`, `0x32A`, `0x32B`, `0x330`, `0x331`, `0x332`, `0x333` | `0x322`, `0x325` | +| `TILESET_02D4B24` | Vermilion-style port-town exterior | `0x29E`, `0x2A3`, `0x2A4`, `0x2A5`, `0x2A6`, `0x2AB`, `0x2AC`, `0x2AD`, `0x2AE`, `0x2E2`, `0x2E3`, `0x2F3` ⚠, `0x2F4`, `0x2F9`, `0x2FB`, `0x2FC`, `0x302`, `0x303`, `0x304` ⚠, `0x305` ⚠, `0x30B`, `0x30D`, `0x30E` ⚠, `0x30F` ⚠, `0x31B`, `0x31C` ⚠, `0x31D`, `0x31E`, `0x325` | — | +| `TILESET_02D4B3C` | Celadon-style city exterior | `0x284`, `0x295`, `0x296`, `0x297`, `0x29D`, `0x29E`, `0x29F`, `0x2A1`, `0x2A2`, `0x2A3`, `0x2A4`, `0x2A9`, `0x2AA`, `0x2AB`, `0x2AC`, `0x2B3`, `0x2B4`, `0x2B8`, `0x2BA`, `0x2BB`, `0x2BC`, `0x2C0`, `0x2C2`, `0x2E7`, `0x2E8`, `0x2E9`, `0x2EA`, `0x2EB`, `0x2EC`, `0x2F0`, `0x2F1`, `0x2F2`, `0x2F3`, `0x2F4`, `0x2F8`, `0x2F9`, `0x2FA`, `0x2FB`, `0x2FC`, `0x2FD`, `0x2FE`, `0x2FF`, `0x300`, `0x303`, `0x308`, `0x30B`, `0x310`, `0x311`, `0x312`, `0x313`, `0x314`, `0x315`, `0x316` | `0x294` | +| `TILESET_02D4B54` | city: brick apartment blocks with blue windows | `0x2B2`, `0x2B3`, `0x2B9`, `0x2BA`, `0x2BB`, `0x2D8` ⚠, `0x2D9` ⚠ | `0x2D2` | +| `TILESET_02D4B6C` | wooden-facade set, three-bay windows | `0x28C`, `0x28D`, `0x28E`, `0x294`, `0x295`, `0x296`, `0x29B`, `0x29C`, `0x29D`, `0x2A3`, `0x2A4`, `0x2A5` | — | +| `TILESET_02D4B84` | ruins/resort exterior plus plank lodge wall with blue-glass windows | `0x2F3`, `0x2F4`, `0x2F5`, `0x309`, `0x30B`, `0x30D` | — | +| `TILESET_02D4B9C` | Celadon-style city: dept store, Game Corner, cyan-glass tower | `0x287`, `0x28B`, `0x28C`, `0x28F`, `0x290`, `0x291`, `0x292`, `0x293`, `0x294`, `0x298`, `0x299`, `0x29A`, `0x29B`, `0x29C`, `0x2A0`, `0x2A1`, `0x2A2`, `0x2A3`, `0x2A4`, `0x2A5`, `0x2A6`, `0x2A7`, `0x2AB`, `0x2AC`, `0x2AD`, `0x2AF`, `0x2BB`, `0x2BD`, `0x2BE`, `0x2BF`, `0x2C1`, `0x2C2`, `0x2C3`, `0x2C5`, `0x2C6`, `0x2C7`, `0x2C9`, `0x2CA`, `0x2CB`, `0x2CD`, `0x2CE`, `0x2CF`, `0x2D1`, `0x2D2`, `0x2D5`, `0x2D6`, `0x2D7`, `0x2E6`, `0x2E7`, `0x2EE`, `0x2EF`, `0x305`, `0x336`, `0x337` | `0x284`, `0x2BC` | +| `TILESET_02D4BB4` | FireRed generic building-interior PRIMARY | `0x021`, `0x022`, `0x029`, `0x02A`, `0x036`, `0x037`, `0x098`, `0x0A0`, `0x0B4`, `0x0B5`, `0x114`, `0x115`, `0x118`, `0x120`, `0x160`, `0x161` | — | +| `TILESET_02D4BE4` | building interiors: Center, Mart, dept-store escalators, teal office wing | `0x29C`, `0x29D`, `0x2A4`, `0x2A5`, `0x2DE` | — | +| `TILESET_02D4C2C` | museum exhibit hall interior | `0x2BF`, `0x2C7` | — | +| `TILESET_02D4C5C` | bike-shop / workshop interior | `0x293`, `0x298` | — | +| `TILESET_02D4C8C` | indoor general-purpose set, small windows with flower boxes | `0x29E` ⚠, `0x29F`, `0x301`, `0x302`, `0x312` | — | +| `TILESET_02D4CA4` | pink-brick interior, one grey-framed blue window | `0x290`, `0x291`, `0x292`, `0x29A`, `0x29B`, `0x29C` | — | +| `TILESET_02D4CD4` | very large interior set plus pale building walls with teal windows | `0x2D0` ⚠, `0x2D1` ⚠, `0x2EA`, `0x2EB` ⚠, `0x2F2`, `0x322` ⚠, `0x323` ⚠, `0x32A`, `0x3CA`, `0x3CB`, `0x3D2` | — | +| `TILESET_02D4CEC` | ship/liner interior (S.S. Anne style) | `0x295`, `0x296`, `0x2C4`, `0x2C6`, `0x2C7`, `0x2DB`, `0x2DC`, `0x313`, `0x314` | `0x2A8`, `0x2A9`, `0x2B0`, `0x2B1`, `0x2B8`, `0x2B9`, `0x2C0`, `0x2C1`, `0x2D8`, `0x2D9` | +| `TILESET_02D4D04` | wooden cabin/house interior | `0x283`, `0x284`, `0x285` | — | +| `TILESET_02D4D34` | Pokemon Center interior, one framed blue panel = back-wall window | `0x2A2`, `0x2A3`, `0x2A4` | — | +| `TILESET_02D4D4C` | indoor greenhouse/gym rooms | `0x284`, `0x28C`, `0x2B0`, `0x2B1`, `0x2B2` | — | +| `TILESET_02D4D64` | purple-brick indoor room, one 3x2 blue window | `0x285`, `0x286`, `0x287`, `0x28D`, `0x28E`, `0x28F` | — | +| `TILESET_02D4D7C` | Pokemon Center interior, one blue window pane | `0x2AC`, `0x2AD`, `0x2AE` | — | +| `TILESET_02D4E6C` | Celadon Department Store interior | `0x2F4`, `0x2F5`, `0x2F6`, `0x2FC`, `0x2FD`, `0x2FE`, `0x300`, `0x301`, `0x302`, `0x31F`, `0x327` | — | +| `TILESET_02D4E84` | interior with teal glass curtain walls (dept store / hotel lobby) | `0x298`, `0x299`, `0x2A0`, `0x2A1`, `0x2A8`, `0x2A9`, `0x2B0`, `0x2B1`, `0x2C0`, `0x2C1`, `0x2FE`, `0x2FF`, `0x306`, `0x307` | — | +| `TILESET_02D4EB4` | laboratory/machine-room interior, windows with flower boxes | `0x28D`, `0x2B0`, `0x2B3` | — | +| `TILESET_02D4ECC` | ship interior (S.S. Anne style) | `0x345`, `0x38C`, `0x38D`, `0x390` | `0x2C4`, `0x374`, `0x391` | +| `TILESET_02D4F14` | house interior, curtained windows | `0x2A8`, `0x2A9`, `0x2B0`, `0x2B1`, `0x2C8`, `0x2C9`, `0x2CA`, `0x2CB` | — | +| `TILESET_02D4F2C` | mansion/house interior, one large arched 2x2 window | `0x378`, `0x379`, `0x380`, `0x381` | — | +| `TILESET_02D4F44` | hotel/restaurant interior | `0x2D0` | — | +| `TILESET_02D4F5C` | wooden schoolroom/lodge interior, four-pane window band | `0x28D`, `0x28E`, `0x28F` | — | +| `TILESET_02D4F74` | hotel/inn interior | `0x28A`, `0x28D`, `0x2C5`, `0x2C6`, `0x2DD` ⚠, `0x2DE` ⚠ | — | +| `TILESET_02D4F8C` | office/lab building interior | `0x2C2`, `0x2C3`, `0x2F8`, `0x2F9` | — | +| `TILESET_02D4FA4` | ransacked building interior, one two-pane curtained window | `0x289`, `0x28A` | — | +| `TILESET_02D504C` | Fuchsia-style town exterior, glass-fronted shops | `0x281`, `0x282`, `0x283`, `0x284`, `0x285`, `0x289`, `0x28A`, `0x28B`, `0x28C`, `0x28D`, `0x291`, `0x292`, `0x293`, `0x294`, `0x295`, `0x2AD`, `0x2AE`, `0x2B4`, `0x2B5`, `0x2B6`, `0x2E1`, `0x2E2`, `0x2E4`, `0x2E5` | `0x29B`, `0x2EB` | +| `TILESET_02D5064` | island-town exterior | `0x29B`, `0x29C`, `0x2BA`, `0x2BB`, `0x30E`, `0x30F` | `0x2B9` | +| `TILESET_02D507C` | city exterior, tall blue dept-store block with rows of windows | `0x29B`, `0x29C`, `0x2A1`, `0x2A2`, `0x2A3`, `0x2A4`, `0x2A5`, `0x2A9`, `0x2AA`, `0x2AB`, `0x2AC`, `0x2AD`, `0x2B1`, `0x2B2`, `0x2B3`, `0x2B4`, `0x2B5`, `0x318`, `0x319`, `0x31A`, `0x31B` | `0x369`, `0x36A` | +| `TILESET_02D5094` | Center / dept-store / Silph-style interior | `0x2E3`, `0x2EB`, `0x2EC` ⚠, `0x2ED` | — | +| `TILESET_02D50AC` | harbour / ferry terminal exterior | `0x292` | — | +| `TILESET_02D50C4` | striped-wallpaper interiors with arched glass windows plus teal-walled exterior | `0x341`, `0x342`, `0x343`, `0x344`, `0x349`, `0x34A`, `0x34D`, `0x398`, `0x399`, `0x39A`, `0x3A8`, `0x3A9`, `0x3AA`, `0x3AB`, `0x3AC` ⚠, `0x3B0`, `0x3B1`, `0x3B2`, `0x3B3`, `0x3B4` ⚠, `0x3B8`, `0x3B9`, `0x3BA`, `0x3BB`, `0x3C0`, `0x3C1`, `0x3C2`, `0x3C3`, `0x3C4`, `0x3C6`, `0x3F8`, `0x3FA` | `0x3F9` | +| `TILESET_02D50DC` | stadium/battle-arena interior | `0x2AB`, `0x2AC`, `0x2B3` | — | + +### Tilesets with no window glass (20) + +- `TILESET_02D4BCC` — shop / Center style interior +- `TILESET_02D4BFC` — rocky cave and desert exterior +- `TILESET_02D4C14` — two blank cream filler metatiles +- `TILESET_02D4C44` — large facility interior, helipad, consoles +- `TILESET_02D4C74` — small house interior extras +- `TILESET_02D4CBC` — Rocket-Hideout style facility interior +- `TILESET_02D4D1C` — blue-tiled swimming pool / bath hall +- `TILESET_02D4D94` — wood-panelled multi-storey interior, sea-view deck — glass doors: `0x281` +- `TILESET_02D4DC4` — park/garden exterior, no buildings +- `TILESET_02D4DF4` — grey rock cave interior +- `TILESET_02D4E0C` — rocky cave/mountain interior +- `TILESET_02D4E24` — ice-cave set +- `TILESET_02D4E54` — rocky mountainside/cave exterior +- `TILESET_02D4E9C` — pale stone hall/shrine interior, no glazing +- `TILESET_02D4EE4` — dark grey/olive facility interior +- `TILESET_02D4EFC` — villain-base / office interior +- `TILESET_02D4FEC` — cave/rock with sand floors and water pools +- `TILESET_02D5004` — outdoor sandy/desert area, no buildings +- `TILESET_02D501C` — snow and ice terrain +- `TILESET_02D5034` — indoor facility, yellow brick walls, colour-coded doors + +## Judgement calls left open + +Cells where glass could not be told from something else are **not** in the +list. They are recorded here so the calls can be revisited rather than +rediscovered — the recurring hard cases are bathroom mirrors, glass-fronted +cabinets, unlit dark panes, and pale wall panelling that shares the glass +palette. + +### Emerald + +- `TILESET_03DF704` — 039,059,1C5,1D3 1-2px blue sliver, likely top of glass door +- `TILESET_03DF71C` — 249 narrow teal strip; 250,251,258,259,285 blue slatted band (picket fence) +- `TILESET_03DF734` — 267 blue vertical bars; 299,29A,29B,2A8,2A9,2AA,2AB dark navy arched openings +- `TILESET_03DF74C` — 281,29C,2C2,2CA narrow pale-blue columns (fountain jets?); 2BA,2C4,2C5,2C6 arch panels; 2D0,2D1,2D8,2D9 white panels with pale-blue dots +- `TILESET_03DF764` — 362,363 dark-navy rectangles on the distant ferry +- `TILESET_03DF77C` — 2E9,2EC teal/white panels under the lit gate arch; 2AF,2B7 pale grey dither panel +- `TILESET_03DF7AC` — 2B0,2B3,2B4,300,301,302,308,309,30A pale circles on a boat hull (portholes or rivets) +- `TILESET_03DF7C4` — 2D1 a few glass-coloured pixels in a grass tile +- `TILESET_03DF7DC` — 200,201,204,205,206 frieze band; 2E8,2E9,2EA,2F8,2F9,2FA white-framed patterned panel +- `TILESET_03DF7F4` — 2E4,2E5,2E6 windshield of a machine; 393,394,39B,39C banding on a rocket body +- `TILESET_03DF854` — 2DC corner filler; 38D curtain only; 306 small blue square +- `TILESET_03DF86C` — 2B1,2B2,2BB white panel in wooden frame; 2E3,2E4,2E6,30B,30D,30E thin blue band along wall base +- `TILESET_03DF89C` — 213,21B,27F,320 small framed panes (window or display cabinet) +- `TILESET_03DF8B4` — 264 flat light-blue panel among counters +- `TILESET_03DF8E4` — 20B small dark-framed pale-cyan panel +- `TILESET_03DF944` — 214,215,21C,21D barred porthole or vent; 220,228 tall clear cylinder +- `TILESET_03DF974` — 216,217 green-framed pale-blue panel with a grid +- `TILESET_03DF9A4` — 208,209 flat pale-blue upper wall, may be upper pane +- `TILESET_03DF9BC` — 2DD framed panel (mirror?); 2E5,3EA,3EB glass above a counter +- `TILESET_03DFA34` — 2BD,2BE,2BF,2C5,2C6,2C7,2CD,2CE,2CF free-standing pale-blue framed panel +- `TILESET_03DFA64` — 237,23F gold-framed panel with light-blue and grey bars +- `TILESET_03DFA7C` — 2BD,2BE,2BF,2C5,2C6,2C7 framed pale-blue glass panel (sliding glass door?) +- `TILESET_03DFAF4` — 24C dresser with lavender panes (glass-fronted furniture) +- `TILESET_03DFB3C` — 210,211,26B,26C,273,274 slate-grey panel in pale wood frame +- `TILESET_03DFB6C` — 35B pale-blue panel over a washbasin (mirror?) +- `TILESET_03DFB84` — 20E,20F grey cabinet with blue sparkle panel +- `TILESET_03DFC7C` — 295,29D window head/trim band +- `TILESET_03DFC94` — 20A,20B,20C,20D cyan panels with an etched Poke Ball; 282,2DE,2E6 blue pane above a counter +- `TILESET_03DFCAC` — 220,221,228,229,22A,288,289,28A large pure-white panels in grey frames +- `TILESET_03DFD0C` — 250,256 white lattice panes in a brown frame +- `TILESET_03DFD6C` — 215,21D framed cyan panel in a counter-unit +- `TILESET_03DFD84` — 217,21F dark grey with white diagonal streak; 218,21A tall pale-blue panes in navy wall + +### FireRed + +- `TILESET_02D4AAC` — 2A8,2A9 dark grey recess above the multi-pane grid +- `TILESET_02D4ADC` — 2AA,2AB,2AC pale blue-grey panel above the store entrance; 2CE vending machine +- `TILESET_02D4AF4` — 2CC white-framed dark panel; 2FC,2FD,2FF,304 flat blue band at wall base +- `TILESET_02D4B0C` — 2CB,2CC,2CD dark navy recesses in the green facade +- `TILESET_02D4B24` — 2DA,2DB market-stall counter panes; 306,307 tiny pale sliver +- `TILESET_02D4B54` — 28E,28F,296,297,338,339 distant background buildings; 2A8,2A9,2AA mullioned facade grid +- `TILESET_02D4B6C` — 297 small grey-framed pane; 2AD blue panel with a catch +- `TILESET_02D4B84` — 313,315 pale panels flanking the door, likely gable/awning +- `TILESET_02D4B9C` — 2B3,2B4,2B5 grey-blue sparkle panel; 343 small green pane; 2F8-2FB blue grid basin +- `TILESET_02D4BB4` — 083,084 white-panelled grid; 0B8,0C0 framed blue/white panel; 183 uniform pale-blue filler +- `TILESET_02D4BCC` — 287,28F,2B0,2B1,2B2,2C0,2C1,2C2 display-cabinet glass fronts +- `TILESET_02D4C2C` — 2F9,2FA,2FB,2FC glass display case (furniture) +- `TILESET_02D4C74` — 281,28E two pale-blue panes over a counter (cupboard or window) +- `TILESET_02D4CD4` — 2F3,32B,3D3 small teal patch beside the window wall +- `TILESET_02D4CEC` — 281-284,289-28C wall-wide sea-and-island scene; 2BC,2BD,2BE display-case glass +- `TILESET_02D4D1C` — 2C8,2C9,2CA grey-framed blue rectangle on tiled wall +- `TILESET_02D4D94` — 282,2D4,2F2,33A,33B,39B round ring-framed wall fixture +- `TILESET_02D4ECC` — 343,344 white-rimmed oval, porthole or bathtub +- `TILESET_02D4F44` — 2C6,2C7 furniture glass; 288,29F hung pictures +- `TILESET_02D4F74` — 318,319,31A,320,321 likely mirrors; 2DC probably a bath/sink +- `TILESET_02D5034` — 2A0-2AB,2B0,2B1,2B2,2C8-2CB,2D0,2D1,2D8-2DB large flat pale-teal framed panels; 285,295,29D colour-coded plaques +- `TILESET_02D507C` — 2A0,2A6,2A8,2AE,2B0,2B6 tall pale-blue strips with foliage +- `TILESET_02D5094` — 31C,31E,31F pale blue with white sweeps, probably carpet +- `TILESET_02D50AC` — 2C0,2C1,2C2 unglazed window or ticket hatch; 2C3,2C5,2CB,2CD near-white framed panels +- `TILESET_02D50C4` — 2D4,2D5,2D9,2DA,2F4,2F5,2F9,2FA white-framed wall square, reads as a picture frame +- `TILESET_02D50DC` — 292-297,2A8,2AA periwinkle upper-wall band with white blocks + diff --git a/docs/gen4-platinum-1.md b/docs/gen4-platinum-1.md new file mode 100644 index 000000000..3e221d8f0 --- /dev/null +++ b/docs/gen4-platinum-1.md @@ -0,0 +1,906 @@ +# Pokemon Platinum / Gen 4 — what is built, what is not + +Tracking document for Gen 4 support. Updated as work lands; the "Status" table +is the short answer and everything below it is the reasoning. + +--- + +## Status + +| Stage | State | +|---|---| +| Cartridge recognised and hashed | **Done** | +| Registered in the launcher (tab, panel, accent) | **Done** | +| NDS filesystem reader (`NdsRom`) | **Done** | +| NARC archive reader (`NarcArchive`) | **Done** | +| Script command table, 840 opcodes (`Gen4ScriptOps`) | **Done** | +| Nine variable-length script commands | **Deliberately unresolved** | +| Text decode (`pl_msg.narc`, `Gen4Text`) | **Done** — 46,053/46,053 strings | +| Species records (`Gen4Species`) | **Done** — 508 records | +| Graphics: containers, LZ77, palettes, tiles, tilemaps (`Gen4Graphics`) | **Done** | +| Battle sprites — cipher **solved**, sprites decode | **Done** | +| Moves (`Gen4Moves`) — 471 records | **Done** | +| Items (`Gen4Items`) — 446 records | **Done** | +| Learnsets and evolutions (`Gen4Species`) | **Done** | +| Wild encounters (`Gen4Encounters`) — 183 areas | **Done** | +| Trainers and parties (`Gen4Trainers`) — 928 | **Done** | +| Packed trainer names (`Gen4Text`) | **Done** | +| Map matrix + land chunks + permissions (`Gen4Maps`) | **Done** | +| Map events: NPCs, warps, signs, triggers (`Gen4Events`) | **Done** | +| Map names (`Gen4Maps.mapNames`) — 593 | **Done** | +| Map 3D meshes (NSBMD) and BDHC height | Not started | +| Map header table (`Gen4MapHeaders`) — 593 in the ARM9 | **Done** | +| Script decoding (`Gen4Script`) — bytes to instructions | **Done** | +| Script lowering (`Gen4ScriptVM`) — 97.8% of instructions | **Done** | +| Extractor: cartridge to cache (`RomExtractorGen4`) | **Done** — 12 tables | +| Wiring the extractor into `RomImporter` | **Done** | +| Registering scripts per map through `MapScripts` | Not started | +| Script lowering to engine commands | Not started | +| Dual-screen + Poketch presentation | Designed, not built | +| Start-menu style switch | Designed, not built | +| `importable` flipped to `true` | **No** — see "What has to be true" | + +The cartridge is **recognised but withheld**. A player who owns it can reach +Platinum's panel in the launcher and read exactly what is and is not ready, +rather than meeting "Not supported yet", which reads as "your dump is wrong" +and sends people hunting for another one. + +--- + +## The cartridge, measured + +Every figure here was read from the project's own dump, not copied from a +format note. + +``` +title POKEMON PL game code CPUE maker 01 +unit 0 version 1 (Rev 1) size 134,217,728 bytes (128 MiB) +used 104,607,804 bytes +sha1 0862ec35b24de5c7e2dcb88c9eea0873110d755c +md5 ab828b0d13f09469a71460a34d0de51b + +ARM9 rom 0x00004000 size 1,057,784 ram 0x02000000 entry 0x02000800 +ARM7 rom 0x00409800 size 161,788 +FNT 0x00431000 (7,092) FAT 0x00432C00 (3,696) -> 462 entries +overlays 122 on the ARM9, 0 on the ARM7 +filesystem 340 files in 89 directories +``` + +462 FAT entries = 122 overlays + 340 files, and the last byte any file +occupies is 104,607,804 — the used-size the header itself reports. The +filesystem walk accounts for the cartridge exactly. + +Rev 0 (`ce81046eda7d232513069519cb2085349896dec7`) is registered as an +alternate hash. pokeplatinum builds both and the filesystem layout is the same +in each, which is what the extractor reads. + +### Files that will matter + +| Path | Bytes | What | +|---|---|---| +| `/fielddata/land_data/land_data.narc` | 16,125,840 | map terrain | +| `/poketool/pokegra/pl_pokegra.narc` | 11,778,676 | species sprites | +| `/msgdata/pl_msg.narc` | 4,351,320 | all text — 724 banks, 46,053 strings | +| `/data/mmodel/mmodel.narc` | 1,465,988 | overworld models | +| `/itemtool/itemdata/pl_item_data.narc` | 440,460 | items | +| `/fielddata/script/scr_seq.narc` | 304,476 | 1,124 script files | +| `/fielddata/encountdata/` | 237,324 | wild encounters | +| `/poketool/personal/pl_personal.narc` | 105,920 | 508 species records, 44 B each | + +Message banks located by looking rather than assumed: **412** is species names +(entry 1 BULBASAUR, entry 25 PIKACHU), **706** is Pokedex entries. + +Platinum keeps Diamond/Pearl's un-prefixed files beside its own `pl_`-prefixed +ones. The `pl_` file is the one to read; the other is the older game's. + +--- + +## Staying legal + +Identical to every other version here, and the reason the two new readers came +first. + +* **No cartridge data is committed.** Not a table, not a tile, not a string. +* The ROM stays where the player keeps it. The importer reads it, writes a + cache under `platinum/` (`cachePrefix`), and that cache is what the game + loads through `CacheFs.mountVersion`. +* What lives in this repository is **scaffolding**: readers, tables of + *offsets and shapes*, and code that knows how to ask the cartridge a + question. Gen 4 does not change the rule; it only changes the question from + "what is at address X" to "what is inside file Y". +* `Gen4ScriptOps.lua` is opcode numbers, command names and operand widths + derived from pret/pokeplatinum — a description of the format, the same thing + `Gen3ScriptOps.lua` is for Emerald. No script bytes from the cartridge. + +--- + +## What is built + +### `src/core/GameVersion.lua` + +`platinum` registered: generation 4, both revision hashes, `cachePrefix +"platinum/"`, `saveSuffix "_platinum"`, `importable = false`, `experimental`, +and a new `dualScreen = true`. Two new predicates, `GameVersion.isGen4(id)` +and `GameVersion.isDualScreen(id)` — the second asked of the *version* rather +than computed from the generation, because "is Gen 4" and "has two screens" +are not the same claim and field code should say which one it means. + +`GameVersion.ORDER` puts platinum after emerald and before the hacks: +cartridges in generation order, then romhacks. + +### `src/import/RomImporter.lua` + +Platinum has a chip in the tab row. **This is the only navigation into a +version's panel** — Prism was once registered without one and was hashed, +counted and completely unreachable, and the code comment saying so is still +there. Also: + +* 128 MiB added to the accepted ROM sizes, `.nds` to the accepted extensions, + and to all five native file pickers (macOS, Windows, zenity, kdialog, and + the multi-select variants) plus the drag-and-drop hint. +* A `pokemon_platinum.nds` name for the panel, via a generation-4 branch. +* Chip colour `#a6b0d8 → #4a5286`: the metal with the cool cast the box art + has, kept clear of Silver's near-white, Crystal's cyan and Polished + Crystal's purple, because all four sit in one row. +* **A bug fixed on the way.** The "recognised, not playable yet" panel had one + hardcoded sentence — *"Prism imports its data, but its scripts still + misbehave"* — shown for **every** withheld game, under a comment reading + "ONE SENTENCE PER GAME". Prism is importable now, so Platinum was about to + become the only withheld game and its panel would have explained Prism. + Replaced with `RomImporter.WITHHELD_REASON`, keyed by version. + +### `src/import/NdsRom.lua` + +The DS filesystem. `open` / `header` / `list` / `stat` / `read(path)` / +`readId` / `arm9` / `overlay(n)`. + +A DS cartridge is a **filesystem, not an address space**, which is the single +biggest structural break from every generation above. `rom:u16(0x3DF884)` has +no meaning here; `rom:read("/poketool/personal/pl_personal.narc")` does. + +It never loads the cartridge into memory. 128 MiB as one Lua string is a +number this engine has to run alongside on a phone, and nearly every stage +wants one file. The handle stays open and ranges are read on demand. + +Two traps it avoids on purpose: the FNT is walked **by directory id**, not +linearly, because the subtables are not stored in tree order and a forward +walk builds a plausible tree with the wrong parents; and an overlay's bytes +come from the **file id at +0x18** of its table entry, not from the overlay +index, which are different numbers that look interchangeable. + +### `src/import/NarcArchive.lua` + +`parse(data)` / `count` / `range(i)` / `get(i)` / `all()`. Chunks are located +by walking, not by fixed offset — the header size is a field precisely because +it varies. Verified on two archives of very different shape: `scr_seq.narc` +(1,124 members) and `pl_personal.narc` (508 members of 44 bytes, which is +Platinum's species count and its personal-record size). + +### `src/import/Gen4ScriptOps.lua` + +All 840 opcodes, `$000`–`$347`, with names and operand specs. + +Gen 4 reads its opcode as a **halfword**, not a byte — 840 commands do not fit +in one — so the smallest instruction is two bytes and a Gen 3 decoder fed Gen +4 bytes desyncs on the first command. + +**Where the widths come from, and why that is the whole story.** +`Gen3ScriptOps.lua`'s header records what guessing cost: Emerald's `$E0` was +reasoned out from handler call shapes, came out two bytes short, and produced +not garbage but the Sootopolis cutscene playing twice with no warp home — +because a two-byte slip lands on a plausible opcode and the walk sails on. So: + +* opcode numbers are the **order of the `ScriptCommand()` lines** in + pokeplatinum's `include/data/scripts/scrcmd.h`. Position *is* the opcode, + so all 840 are listed including dummies. Two of those lines are spelled in + mixed case (`SCRCMD_GetExchangeServiceCornerItemAndCost` at `$2A2`, + `ScrCmd_GETRANDOMBATTLEGROUNDTRAINERS` at `$2FB`); a reader matching only + `[A-Z_]` silently drops them and shifts **every opcode after `$2A2` down by + one** — the same class of failure, one level up. This was hit and fixed. +* operand widths are the stream reads in each handler body, in source order. + The five primitives consume fixed widths (`ReadByte` 1, `ReadHalfWord` 2, + `ReadWord` 4, and the inlines `GetVar`/`GetVarPointer` 2 each, both + `ReadHalfWord` underneath). + +**The two ways that could still be wrong, both measured:** + +1. A handler reading inside an `if` has no fixed width. Nine do. They are + marked `*` and listed in `VARIABLE_LENGTH` — **not guessed**. +2. A handler delegating its read to a helper would hide operands. Every + function in pokeplatinum taking a `ScriptContext *` and transitively + reaching a stream read was collected, and every handler body checked + against that set: **zero** handlers delegate. + +**Corroboration, which is not proof.** Walking all 1,124 members of +`scr_seq.narc` from all 4,079 entry points decodes 50,835 instructions across +381 distinct opcodes and reaches a clean `end` **4,070 times with no unknown +opcode**; 8 stop at a variable-length command and 1 runs past its member's end +(one member's header parse, listed under open questions). Gen3ScriptOps +explains precisely why this is corroboration and not proof. The proof is the +handler source; the corpus only shows nothing contradicts it. + +### `src/import/Gen4Text.lua` + +All of Platinum's text: 724 message banks, 46,053 strings. Each bank is +encrypted **twice** — once on its table of offsets and lengths, again with a +different key on the characters: + +``` +entry table: k = (seed * 765 * (i+1)) & 0xFFFF; k |= k << 16 + offset ^= k; length ^= k (length counts CHARACTERS) +characters: key = ((i+1) * 596947) & 0xFFFF + each u16 ^= key; key = (key + 18749) & 0xFFFF +``` + +Both keys stay in 16 bits. In pokeplatinum's C the character key is a **u16 +parameter** immediately overwritten by a 32-bit product, so the truncation is +the declaration doing it — and a 32-bit key decodes the first character +correctly and then drifts, which reads as a charmap problem rather than a +cipher one. + +**Why this one is known to be right, unlike a script table.** All 46,053 +strings decrypt and all 46,053 end in the `0xFFFF` terminator. A wrong key +does not land on a terminator by accident forty-six thousand times, and text +either reads or it does not — there is no plausible-but-wrong here. A second, +independent Python implementation was run over the whole cartridge and +compared line for line against the Lua: **zero differences**. + +Escapes: `0xFFFE` introduces `command, argc, args...`; if the command's high +byte is a STRVAR base its low byte is the variable index. 68 distinct escape +commands occur in the cartridge and **every one is covered** — no unknown +control codes anywhere. `0xF100` starts a bit-packed trainer name terminating +on `0x01FF` (885 strings); not decoded yet, marked and stopped rather than +printed as characters. + +### `src/import/Gen4Graphics.lua` + +Nitro containers and the LZ77 the cartridge wraps most of them in. Magics are +stored reversed, like NARC's chunk tags: `RLCN` = NCLR (palette, section +`TTLP`), `RGCN` = NCGR (tiles, `RAHC`), `RCSN` = NSCR (tilemap, `NRCS`), +`RECN` = NCER (cells). Sections are found by walking from `headerSize` for +`sectionCount` chunks — both fields exist because they vary. + +Two things believed from arithmetic rather than from a field: + +* **Bit depth**, because `tilesX * tilesY * 32 == dataSize` settles 4bpp + outright and cannot be misread. +* **Colour count**, as `dataSize / 2`. The palette depth field is the one + field here whose meaning did not survive inspection — a 16-colour Platinum + palette carries `4` where the published tables say `3` — and since the byte + count is unambiguous, nothing needs the enum to be right. + +`tilesX`/`tilesY` are `0xFFFF` on an **unsized** sheet (every party icon is +one); the tile count always comes from the byte count instead. + +**Validated by looking, not counting.** All 540 party icons in +`pl_poke_icon.narc` render as recognisable Pokémon in the right colours. The +LZ was run over every compressed member of four graphics archives: **125 of +125** decompress to exactly their declared size and each one turns out to be a +valid Nitro container (48 NCGR, 27 NSCR, 23 NCER, 23 NANR). And a second, +independent Python implementation was compared pixel for pixel across all 540 +icons — 1,105,920 pixels, **zero differences**. + +That cross-check earned its keep immediately: it caught a real bug in the Lua, +where the tile `dataOffset` (which is relative to the start of the section's +*fields*) had the 8-byte tag/size header added a second time. That does not +crash — it produces recognisable shapes displaced by a quarter tile, which is +exactly the kind of wrong that survives a glance. + +### `src/import/Gen4Species.lua` + +508 records of 44 bytes from `pl_personal.narc`, with names from bank 412. +The 44 summing exactly is the check that the layout is read right, and the +values agree with the published tables (Pikachu 35/55/30/90/50/40 with +Static; Giratina 150/100/120/90/100/120; Arceus 120 across). + +**One trap worth naming:** the stat order is hp, attack, defense, **speed**, +spAttack, spDefense. Speed is fourth, not last. Reading it in the display +order every stat screen uses swaps Speed with Special Attack and produces a +table that looks entirely plausible and is wrong for every species. + +This stage also proves the floor: cartridge → `NdsRom` → `NarcArchive` → +record + `Gen4Text`, four modules and no special cases. + +### `src/import/Gen4Moves.lua` and `src/import/Gen4Items.lua` + +471 moves of 16 bytes from `pl_waza_tbl.narc`; 446 items of 34 bytes from +`pl_item_data.narc`. Names come from message banks 647 and 392, both found by +looking rather than assumed. + +**Priority is signed.** Quick Attack is +1 and Roar is −6, and reading that +byte unsigned turns every negative-priority move into one that goes first — +no crash, no visual glitch, nothing a battle looks obviously wrong for. It +just quietly plays a different game. Verified: Quick Attack +1, Roar −6. + +Item record checks against the published tables: Master Ball 0, Ultra Ball +1200, Poké Ball 200, Potion 300, HP Up 9800. Moves: Pound 40/100/35 normal +physical, Thunderbolt 95/100/15 electric special, Hyper Beam 150/90/5, +Struggle 50 power with 0 accuracy and 1 PP. + +### Learnsets and evolutions (in `Gen4Species`) + +`wotbl.narc` holds one **variable-length** learnset per species — 24 to 36 +bytes, terminated by `0xFFFF`. Each entry is a u16 packing the move in the low +**9** bits and the level in the high **7**. That split is forced: 467 moves +need 9 bits and level 100 needs 7. Reading it the other way round gives move +ids under 128 at levels in the hundreds — a table that still looks plausible. + +The structural check is what confirms it: across all 493 species, **6,598 +learnset entries, zero move ids out of range and zero levels outside 1–100**. + +`evo.narc` holds seven `{method, param, target}` slots per species in 44 bytes. +**Every method number was derived from the cartridge and checked against a +species that can be named**, because the enum lives in a generated header +pokeplatinum builds and does not ship — there was nothing to copy. Grouping +all 246 evolutions by method identified each one: 1 friendship (Golbat, +Chansey, Pichu), 4 level (162 of them), 8/9/10 Tyrogue's three branches, +11/12 Wurmple's personality split, 15 Feebas at beauty 170, 18 Happiny by day +and 19 Gligar by night, 20 knows-move (Aipom/Double Hit), 21 species-in-party +(Mantyke/Remoraid), 24 magnetic field, 25/26 the mossy and icy rocks. + +`param` means a different thing per method — a level, an item, a move, a +species, or nothing — so `paramKind` says which. A caller that treats it as +one thing gets Pikachu evolving at level 83. + +### `src/import/Gen4Encounters.lua` + +183 areas of exactly 424 bytes — and that 424 is the check, because +pokeplatinum's `WildEncounters` struct sums to 424 to the byte. + +Two things that look like mistakes and are not. **Species ids are four bytes +here**, not the two they are everywhere else in the cartridge, with three +bytes of padding after each one-byte grass level; reading them as u16 halves +the stride and walks the table into itself. And **a water slot stores maximum +level first, then minimum** — the cartridge really does put the larger number +first, and reversing it prints "Lv55-30", which reads as a display bug rather +than a parse one. + +Twelve of the 183 areas have a grass rate of 0 with the whole grass table +zeroed: water-only routes, where level-0 slots are correct data. Validated on +that basis — 2,196 grass slots, zero species out of range, and **zero +out-of-range levels in any area whose rate is non-zero**. The first land area +comes out Geodude, Zubat and Onix at levels 4–8, which is Oreburgh Gate. + +### `src/import/Gen4Trainers.lua` + +`trdata.narc` (928 × 20-byte headers) and `trpoke.narc` (928, variable) share +an index and are meaningless apart. + +**The header's `monDataType` sets the party stride, and it must be read rather +than inferred by dividing the party file by the party size.** Dividing works +for 887 of the 928 and then quietly does not: 39 party files carry trailing +padding, so the division lands on 12 or 20 where the real stride is 10 or 18, +and every mon after the first comes out shifted. Reading `partySize` entries +at the type's own stride and ignoring the remainder is correct for all 928. + +`species` also carries the form in its high bits, so the id is the low 10 — +which matters for Wormadam cloaks and Rotom appliances. + +Validated across the whole archive: **1,878 party members, zero species, +level, move or item values out of range.** Roark comes out Geodude 12 / Onix +12 / Cranidos 14 with Stealth Rock; Cynthia's six are right down to Garchomp +at 62 holding a Sitrus Berry. + +### Packed trainer names (closing the last text gap) + +Bank 618 has exactly 928 entries — which is how it was identified as the +trainer-name bank — and 885 of them use the `0xF100` sub-format that was +deferred earlier. + +A packed name is **nine bits per character inside fifteen usable bits** of +each halfword, not sixteen, so characters straddle halfword boundaries and one +bit per halfword is skipped. It terminates on `0x01FF`, not `0xFFFF`. Both +oddities matter: packing into 16 bits decodes the first character correctly +and then drifts (which reads as a charmap problem), and watching for `0xFFFF` +means never stopping, so the name runs on into whatever follows. + +All 885 decode with no unmapped character, every gym leader resolves, and the +full 46,053-string corpus was re-diffed against the independent Python +implementation afterwards — still zero differences. + +### `src/import/Gen4Maps.lua` + +**This is where Gen 4 stops resembling every generation above it.** Gen 1–3 +give you a map: a grid of metatile ids plus a tileset, and the renderer draws +it. Gen 4 gives you a *matrix* of fixed 32×32 chunks, and each chunk carries a +movement-permission grid, a list of placed building models, a **3D mesh**, and +a separate height structure. The picture is a mesh, not a tilemap — but the +permission grid is a plain 32×32 array of u16, and that alone is enough to +know where the player may walk. + +**The matrix** (`map_matrix.narc`, 289 members): + +``` +u8 width, u8 height, u8 hasHeaders, u8 hasAltitude, u8 nameLength, char name[] +u16 headers[w*h] -- only when hasHeaders +u8 altitudes[w*h] -- only when hasAltitude +u16 mapIds[w*h] -- always; indexes land_data.narc +``` + +The two optional blocks are why this has to be parsed rather than indexed: a +reader that assumes they are present takes the map ids out of the header table +and builds the world from the wrong chunks. **All 289 members account for +their length exactly** under this layout — that is the check. Matrix 0 is +30×30 and named `map`: the Sinnoh overworld. + +**The chunks** (`land_data.narc`, 666 members): four u32 sizes, then +permissions, objects, an `BMD0` NSBMD mesh, and a `BDHC` height block. All 666 +satisfy `16 + the four sizes == file length`, all 666 have a 2048-byte +permission block, all 666 carry a BMD0 model. + +**Permissions**: one u16 per tile. Bit 15 marks a tile that is not part of the +map — the void around the land — and the low byte is the terrain behaviour. +Across the whole overworld only 54 distinct values occur and only **two** +distinct high bytes, which is what says bit 15 is a flag and not part of a +number. + +**Validated by looking.** The 30×30 matrix assembled into a 960×960 permission +image is recognisably Sinnoh: Mt. Coronet running north–south through the +middle, the three lakes as enclosed pockets, the Great Marsh's grid at +Pastoria, Route 223's water column up the east coast to the Battle Zone. A +layout error anywhere in the matrix or the chunk header would have scrambled +that beyond recognition. The Lua render was then compared pixel-for-pixel with +an independent Python one: the only differences are the 432 empty matrix cells +where one drew black and the other drew the void colour — **every real +permission value agrees**. + +**Objects** are 48 bytes each, and the offsets were not read off one sample: +every field of all 3,476 placed objects was tabulated. That is what pins the +scale at +28/+32/+36 rather than the +24/+28/+32 a hand-read hex dump +suggested — an error that silently produced `scale 0.0`. Fields +16/+20/+24 +are **always 0** and are left unnamed on purpose: they are almost certainly a +rotation, but with every object in the game at zero there is nothing to tell a +rotation from a reserved field, and naming one would be a guess dressed as a +fact. All 360 distinct model ids fall inside `build_model.narc`. + +### `src/import/Gen4Events.lua` + +`zone_event.narc`, 534 members — who stands on a map, where its exits go, and +what watches the player. + +**The counts are interleaved**, not four counts up front: each block is a u32 +count immediately followed by its own records. + +``` +u32 n; Sign[n] 20 bytes u32 n; Warp[n] 12 bytes +u32 n; Npc[n] 32 bytes u32 n; Trigger[n] 16 bytes +``` + +Reading four counts first looks right on member 0 — sixteen zero bytes, which +is an empty map either way — and then falls apart on every populated map. The +interleaved layout accounts for **all 534 members exactly**, and it is the +only combination in a search over every stride from 8 to 40 that does; the +next best fits fewer than 15. + +**Which block is which was settled by measurement, not by position:** + +* **NPCs** — all 3,555 records have a model id below 470, exactly the member + count of `mmodel.narc`, the overworld model archive. +* **Warps** — 1,207 of 1,213 have a destination below 593, the number of names + in `mapname.bin`; the other six are 4095, a "nowhere" sentinel. The fields + at +0/+2 reach 909, so they cannot be map ids and are coordinates. +* **Triggers** — **all 186** carry a value at +14 of `0x4000` or above, which + is Gen 4's variable space. A trigger is a variable, a value to match, an + area and a script. +* **Signs** — the remaining block, and the one identification here that is + inference rather than proof. Its records carry a script id, a position (+4 + and +8 are u32: the halves at +6 and +10 are zero in all 682) and a small + type field, which is the shape of an interactable. Labelled as such, with + that caveat kept in the source. + +Validated across every zone: 534 parsed exactly, zero failures, zero NPC +models outside `mmodel`, zero warp destinations outside the name table, zero +triggers below the variable base. Spot-checking one city's warps resolves them +to its two routes and its Pokémon Center by name. + +`Gen4Maps.mapNames` reads `mapname.bin` — 593 sixteen-byte zero-padded ASCII +names, which is the count every warp destination stays below. + +### `src/import/Gen4MapHeaders.lua` + +Every other Gen 4 table lives in the filesystem and opens by name. This one +does not — it is compiled into the **ARM9 binary**, and it is the record that +ties a map to its matrix, area data, script file, text bank, wild encounters, +events and music. Without it the other modules each read correctly and none of +them knows which map it belongs to. + +593 records of 24 bytes, matching pokeplatinum's `MapHeader`. + +**Found by searching, not by a hardcoded offset.** In this cartridge it sits +at ARM9 offset `0x0E601C` (RAM `0x020E601C`), but that is a fact about one +build — Rev 0 is a different binary, and a fixed offset there would read +whatever happens to live at that address and hand back 593 confident, wrong +records. `find` scans for a run of 24-byte records whose every field indexes +*inside* the archive it names: matrix < 289, events < 534, scripts < 1124, +messages < 724, encounters < 183 or the 0xFFFF sentinel, area < 75. + +**Why 593 and not 594.** A loose filter finds 594 consecutive plausible +records; the 594th is not a map. Tightened against the area-data archive it +fails immediately, while all of 0..592 pass **every** cross-check against six +separate archives. 593 is also exactly the number of entries in +`mapname.bin`. Taking the loose answer would have added one phantom map — the +kind of off-by-one that only surfaces when something walks the whole table. + +The strongest confirmation is coverage rather than shape: the 593 headers +between them reference **all 534** members of `zone_event.narc`, highest index +533, none left over and none out of range. + +#### A join that succeeded and was wrong + +`mapLabelTextID` indexes **message bank 433** — 126 display names, "Jubilife +City", "Old Chateau", "Rock Peak Ruins". It does *not* index `mapname.bin`, +which is a separate table of 593 **internal** identifiers keyed by the header +id itself: `C01`, `C05GYM0113`, `D25R0106`. + +Both tables have an entry for every map, so joining `labelText` to +`mapname.bin` never errors and never looks broken — it just quietly reports +that Jubilife City is called "C01PC0101". The bound that made it look correct, +`labelText < 593`, holds trivially: `labelText` never exceeds 125. This was +caught by checking a name against the game rather than against a range. + +With both joins right the chain reads: header 3 is **Jubilife City** (`C01`) +on the 30×30 overworld matrix with 33 NPCs and 14 warps, bike, run and fly all +allowed; header 100 is **Hearthome City**'s gym (`C05GYM0113`), 1×1, none of +the three allowed; header 300 is the **Old Chateau** (`D25R0106`) with +encounter table 130. + +#### And the event structs corrected an axis mistake + +pokeplatinum's `MapHeaderData` names its four event arrays `bgEvents`, +`objectEvents`, `warpEvents`, `coordEvents` — in exactly the order the file +stores them, which confirmed the block identification including the one that +had only been inferred. The structs also showed that measuring alone had +produced the right fields with the **wrong axes**: Gen 4 is 3D, so the two +horizontal axes are X and **Z**, and **Y is height**. The byte census found +the height fields sitting at zero on most events and filed them as padding; +they are not, and an object event's height is a 20.12 fixed-point value at +28 +rather than the u16 at +30 a census suggested. A census tells you which bytes +vary. It cannot tell you what they mean. + +### `src/import/Gen4Script.lua` + +`Gen4ScriptOps` says how wide every command is; this walks a real script file +with it and produces instructions — opcode, name, operands, and for a jump the +absolute target rather than the relative offset the cartridge stores. Lowering +sits on top of this; nothing here decides what a command *means*. + +**A member is not a script.** Each of `scr_seq.narc`'s 1,124 members holds +several scripts behind a header of u32 offsets that are relative *to the +position after the offset word*. The header ends either at the halfword +`0xFD13` or, more often, simply where the first script begins — there is no +count. So the walk stops when the cursor reaches the lowest target seen, the +only rule that works for both shapes. Reading a count that isn't there takes +the first script's opcodes as more offsets. + +**Jumps are signed and relative to the end of the instruction.** `goto` and +`call` measure from the byte *after* the operand, and the offset is negative +for every backward jump, which is most loops. Reading it unsigned sends a loop +several gigabytes forward and the decode just stops — which looks like a short +script, not a misread operand. + +**How the jump formula was checked**, since a decoder that reads its own output +will agree with itself all day: starting at every entry point and following +every jump **transitively** reaches 8,567 basic blocks and 78,093 instructions +— twice what a linear walk sees — and of the **15,002 jump operands in them, +zero point outside their own member**. 8,549 of those blocks then decode to a +clean `end`. + +That tests two things at once. A wrong sign or base would scatter targets to +negative numbers and gigabyte offsets; a wrong operand width anywhere earlier +in an instruction would shift the jump operand itself and produce the same +mess. Neither happens. Four blocks of 8,567 stop unexpectedly — two run off the +end, two reach an opcode not in the table — which is 0.05%, recorded rather +than smoothed over. + +**Coverage, which is what makes lowering plannable:** 4,079 scripts from the +entry points, 50,835 instructions, 381 distinct opcodes. + +| commonest N opcodes | share of instructions | scripts fully covered | +|---|---|---| +| 20 | 87.3% | 58.3% | +| 40 | 95.9% | 78.4% | +| 80 | 98.3% | 88.7% | +| 150 | 99.3% | 94.5% | + +The top of that list is the shape of a conversation — `lockall`, `faceplayer`, +`message`, `closemessage`, `releaseall` — so a lowering that starts there gets +NPCs talking before anything else works. Decoding a real one gives a Poké Mart +clerk: `playse / lockall / faceplayer / callcommonscript / closemessage / +pokemartcommon / releaseall / end`. + +### `src/script/Gen4ScriptVM.lua` + +A sibling of `Gen3ScriptVM` by design: `ScriptRunner` and `Commands.resolve` +are generation-agnostic, so a fourth generation needs a lowering and a verb +set, not a fourth script subsystem. Shared verbs (`jump`, `label`, +`show_text`, `ask`, `set_flag`, `play_sound`) are emitted as-is; anything Gen 4 +does that no earlier generation has gets a `g4_` verb, exactly as Gen 3 uses +`g3_`. + +**Measured against the whole cartridge** — 4,079 scripts, 50,843 instructions — +it lowers **97.8% of instructions** and leaves **85.7% of scripts** with +nothing unimplemented in them at all. + +It got there in three passes, and their shape is the useful part: + +| pass | instructions | whole scripts | +|---|---|---| +| the conversation set | 87.8% | 52.7% | +| + 8 (trainer preamble, signposts) | 97.3% | 82.4% | +| + 10 load-bearing | 97.8% | 85.7% | + +The conversation set alone reached 87.8% of instructions but only 52.7% of +whole scripts, because **a script is only as lowered as its worst command**. +The eight added next were the four generated trainer-battle commands — each +occurring exactly 928 times, once per trainer in `trdata.narc` — and the four +that drive a signpost, which have to lower together or a sign opens and never +closes. + +The last ten occur twenty-odd times each and are load bearing anyway: `warp`, +without which the player cannot leave a map; `starttrainerbattle`, without +which the trainer preamble runs and nothing happens; `pokemartcommon`, without +which the clerk says hello and sells nothing. **Frequency is a good guide to +what to lower first and a poor guide to what to stop at.** + +What remains is a flat tail of side systems — TV interviews, the journal, the +Battle Tower, Turnback Cave, the Poketch — at a few dozen occurrences each, +left as explicit `g4_unimplemented` rows rather than dropped. A silently +dropped command is a script that runs and quietly does the wrong thing, which +is much harder to find than one that reports what it could not do. + +The Poké Mart clerk now lowers completely: + +``` +{ play_sound, 1500 } { g4_lock_all } { g4_face_player } { g4_common, 2019 } +{ g4_close_message } { g4_pokemart, 1 } { g4_release_all } { jump, end } +``` + +**What is not here:** this is the lowering half. The extractor half — writing +a script pool into `data/generated` and registering a contribution per map +through `MapScripts` — is not built, so nothing calls this in a running game +yet. + +### `src/import/RomExtractorGen4.lua` + +Eleven modules read Platinum correctly and none of them wrote anything. This +is the stage runner that puts them in order and lands their output in +`data/generated`, which is what `CacheFs.mountVersion` serves to a running +game. + +**It takes a path, not the ROM's bytes** — the one place it deliberately does +not look like `RomExtractorGen2` and `RomExtractorGen3`. Those are handed the +whole cartridge as a Lua string, which is fine at 32 MiB. Platinum is 128 MiB, +the engine has to run alongside it on a phone, and almost every stage wants +*one file* out of a filesystem. `NdsRom` reads ranges on demand; handing it a +128 MiB string would undo that before the first stage ran. + +**Measured end to end against the cartridge: 4.2 seconds, 12 tables.** + +| table | entries | | table | entries | +|---|---|---|---|---| +| `gen4_text` | 724 banks | | `gen4_map_headers` | 593 | +| `gen4_species` | 508 | | `gen4_map_matrices` | 289 | +| `gen4_moves` | 471 | | `gen4_map_permissions` | 666 | +| `gen4_items` | 446 | | `gen4_map_objects` | 387 | +| `gen4_encounters` | 183 | | `gen4_events` | 534 | +| `gen4_trainers` | 928 | | `gen4_scripts` | 873 / 8,567 blocks | + +Every count matches what the individual modules measured independently, and +the spot checks land: Pikachu with 90 base speed, 13 learnset moves and one +evolution; Thunderbolt at 95; Potion at 300; Roark with three Pokémon; +map header 3 as Jubilife City (`C01`) on matrix 0. + +Two shape decisions worth naming. Permissions are stored as **one binary +string per chunk** rather than 1,024 numbers — the same choice Gen 3's map +grids make, because a 666-entry table of thousand-element arrays is slow to +load and enormous on disk while a string is neither. And **lowering does not +happen here**: the pool holds decoded instructions and `Gen4ScriptVM` lowers +at load time, so a lowering fix does not require re-importing the cartridge. + +The script pool follows jumps as well as entry points — a block reached only +by a `goto` is still a block the runner needs — which is why it holds 8,567 +blocks rather than the 4,079 a linear walk finds. + +### Wiring it into `RomImporter` + +The extractor dispatch already anticipated this. Its comment reads *"Adding a +fourth generation should be a line here, not a bug"* — and it very nearly was +one line: + +```lua +[4] = { module = "src.import.RomExtractorGen4", takesVersion = true, + takesPath = true }, +``` + +`takesPath` is the part that made it more than a line. `startData` now takes an +optional `sourcePath`, and `startPath` passes the whole path rather than +reducing it to a basename. Verification is unchanged — hashing a cartridge +means hashing its bytes and there is no way around that — but the moment the +SHA-1 matches, a Gen 4 import releases the 128 MiB string and hands the +extractor a path instead. Dropping the bytes *there* rather than after the run +is the entire point; keeping them live through extraction would have gained +nothing. + +**A dropped file may have no path.** `love.filedropped` gives a File whose +`getFilename` is a real path on desktop and need not be anywhere else, and the +Android save-directory scan reads through `love.filesystem` rather than the +disk. A Gen 4 import that gets that far without a path now fails with a +sentence telling the player to use the Import button, instead of passing `nil` +into `new` and failing inside `NdsRom` with something about a missing ROM +path. + +--- + +## What remains, in dependency order + +1. **NCER cell banks** — how an unsized sheet is assembled into a sprite. + Needed for overworld and battle sprites; the container already parses. +2. **Map meshes** — the NSBMD (`BMD0`) in each chunk and the `BDHC` height + block. The 3D half of the map problem; the permission grid means a walkable + world does not wait on it. +3. **Registering scripts per map** through `MapScripts`, which is what makes + `Gen4ScriptVM`'s lowering reachable from a running game. This is the last + piece before a Platinum import produces something playable. +4. **Overworld models** — `mmodel.narc`, NSBMD. Deferred: a 2D stand-in gets + the game walkable long before the model pipeline is worth building. + +### What has to be true before `importable = true` + +Text decodes, graphics decode, species/items/moves load, at least one map +builds and is walkable, and the common scripts lower well enough to talk to an +NPC. Anything less produces a cache the engine mounts and then fails inside, +which is the outcome the withheld flag exists to prevent. + +--- + +## Dual screen and the Poketch + +Design; not built. The requirement is that the second screen is **never a +second window** — the game runs in one window like every other version here, +and the player chooses how the bottom screen reaches them. + +### Second-screen modes (switchable at runtime, and in Options) + +**1. Swap (default).** One screen is drawn at full size; a key toggles which. +Simple, works at any window size, works on a phone. The DS's own split is +approximated by the fact that the bottom screen is almost never needed +*during* an action — the Poketch and the bag are things you stop to look at. + +**2. Inset.** The bottom screen is drawn as a panel in the **top-right +corner** over the main screen, shown and hidden with a hotkey. It is +**interactive in place** — clicks and taps inside the panel go to the bottom +screen's own coordinate space, so the Poketch's buttons, the bag and the touch +controls work without swapping away. Its **size is adjustable** (a scale +setting, and a drag handle on its corner), and it remembers where and how big +it was per version. + +**3. Both.** Stacked as the hardware had them, for players who want the +authentic layout and have the window height for it. Offered but not the +default, because at typical window sizes it makes both screens small. + +Notes for implementation: the inset panel needs its own input routing (a hit +test before the field's, and pointer coordinates transformed into +bottom-screen space) and its own render target, so field rendering does not +have to know it exists. `GameVersion.isDualScreen(id)` is the gate; nothing +about this should be reachable for Gen 1–3, and no Gen 1–3 draw path should +gain a branch. + +### The Poketch + +The Poketch is one of the things the bottom screen *shows*, not a third +screen. It gets a slot in the second-screen surface alongside the bag, the +map and the touch controls, and the same hotkey that raises the bottom screen +raises whatever that surface is currently showing. A separate direct hotkey to +raise the Poketch specifically is worth having, since it is the thing players +check most often. + +### Start menu + +Two styles, player's choice, default to the first: + +**1. Main-screen menu (default).** The start menu appears on the main screen +exactly as it does in Red through Emerald — same position, same behaviour, +same keys. This makes Platinum feel continuous with the rest of the launcher's +library and means muscle memory carries over. + +**2. Bottom-screen menu.** The authentic DS arrangement, for players who want +it. Uses whichever second-screen mode is active, so on Inset the menu appears +in the corner panel and is clickable there. + +The setting belongs with the other per-version options and should be +switchable mid-game, not only at boot. + +--- + +## Mod compatibility + +Gen 4 must not disturb the four generations already working. Where things +stand: + +* Everything added so far is **new files plus additive registration**. The + only edits to shared code are in `RomImporter` (one chip, the accepted + sizes and extensions, one bug fix) and `GameVersion` (one entry, two new + predicates). No Gen 1–3 branch changed behaviour. +* Mods reach content through the same registries regardless of version, so a + mod that adds text or sprites should work once Gen 4 populates those + registries. Mods that hard-code Gen 3 asset ids will not, and should not — + they are version-specific by construction. +* **A known, pre-existing hazard to keep in mind:** `DRAMATIC_SHAPE` asks for + `TILESET_03DF704` (Emerald's primary) during FireRed sessions — a mod + carrying state across a version switch. Gen 4 will make that class of bug + more visible, not less. Worth fixing before Platinum is playable. + +--- + +## The battle sprites (solved) + +`pokegra` and `otherpoke` needed **two** things, and each one disguises the +other. + +**1. They are encrypted.** The keystream is the series' usual LCG: + +``` +key = the FIRST halfword of the tile data +each halfword: plain = cipher ~ key; key = (key * 0x41C64E6D + 0x6073) mod 2^16 +``` + +Only the low 16 bits of the state ever matter, so no 32-bit arithmetic is +needed. The first halfword being the key is the same statement as "the picture +starts with transparent pixels", since anything XORed with itself is zero — +the stream is self-seeding and there is no key table anywhere. + +**2. They are linear bitmaps, not tiles.** The NCGR layout flag (low byte of +the field at section+20) is `1`, meaning rows of pixels rather than 8×8 tiles. +Measured across every NCGR in the cartridge: 3,872 tiled against 4,160 linear, +and the split is not arbitrary — every sprite archive (pokegra, otherpoke and +the trainer sheets `trfgra`/`trbgra`) is linear; fonts, icons and backgrounds +are tiled. The trainer sheets are linear but **not** encrypted (raw entropy +4.8–5.3), which is how the two properties were separated. + +Decrypt without un-tiling and you get real Pokémon colours smeared into +horizontal bands, which reads as "the cipher is nearly right". Un-tile without +decrypting and you get noise, which reads as "the layout is fine, the cipher is +wrong". Neither is a clue about the other. + +**How it was found, because the wrong way round wasted real time.** The +published constants were tried and appeared to fail; then all eight +combinations of seed source, direction and key half; then a brute force over +all 65,536 seeds. Every one of those was scored against **member 0 of +pl_pokegra — a placeholder that decrypts to noise no matter what you do to +it.** The algorithm had been right from the first attempt and the test subject +was the problem. + +What settled it was not another guess. Assume the leading plaintext is zero, +read the leading ciphertext *as* the keystream, and solve +`k[i+1] = (k[i]*A + C) mod 2^16` for `A` and `C` from the values themselves. +34 of 38 sprites gave `A = 0x4E6D`, `C = 0x6073` — exactly the published LCG — +and the four that disagreed are the ones whose first tile is not blank, so the +"keystream" read from them included picture. **Solving beats searching when +the unknown is small, and a failing test needs its subject checked before its +hypothesis.** + +Result: 194 of 194 front sprites decode, none blank, mean transparency 77.3%, +and the rendered sheet is correct Kanto through the legendary birds. The icon +path was re-diffed after the change — zero pixel differences, so the tiled +path did not regress. + +## Open questions and known gaps + +* **Nine variable-length script commands** (`DOSTRENGTHFUNC`, `DOFLASHFUNC`, + `DODEFOGFUNC`, `DOGROUPCONNECTIONACTION`, `CALLTVBROADCAST`, + `CALLTVINTERVIEW`, `MYSTERYGIFTGIVE`, `$27C`, `GIVEPOFFIN`). Each needs its + handler read individually. The decoder should **refuse** a command it cannot + size rather than walk past it — a guessed width here is the Sootopolis bug + again. +* **One script entry point of 4,079** runs past the end of its member. Almost + certainly one member whose header is parsed slightly wrong rather than a + table error, since no opcode desynced. Needs a look. +* **`RomImporter:startData` takes the whole ROM as a Lua string** and hashes + it. At 128 MiB that is tolerable while the import is refused anyway, but the + real Gen 4 extractor must not go through it — it needs a path-based route + into `NdsRom`, which already reads on demand. **Design this before writing + the extractor, not after.** +* **Rev 0 is registered but untested** — no dump on hand. The layout should be + identical; it has not been confirmed. +* Gen 4's save format is untouched. `saveSuffix "_platinum"` reserves the + slot and nothing else. diff --git a/docs/gen4-platinum-2.md b/docs/gen4-platinum-2.md new file mode 100644 index 000000000..b67dea27c --- /dev/null +++ b/docs/gen4-platinum-2.md @@ -0,0 +1,10376 @@ +# Pokemon Platinum / Gen 4 — what is built, what is not + +Tracking document for Gen 4 support. Updated as work lands; the "Status" table +is the short answer and everything below it is the reasoning. + +--- + +## Status + +| Stage | State | +|---|---| +| Cartridge recognised and hashed | **Done** | +| Registered in the launcher (tab, panel, accent) | **Done** | +| NDS filesystem reader (`NdsRom`) | **Done** | +| NARC archive reader (`NarcArchive`) | **Done** | +| Script command table, 840 opcodes (`Gen4ScriptOps`) | **Done** | +| Nine variable-length script commands | **Deliberately unresolved** | +| Text decode (`pl_msg.narc`, `Gen4Text`) | **Done** — 46,053/46,053 strings | +| Species records (`Gen4Species`) | **Done** — 508 records | +| Graphics: containers, LZ77, palettes, tiles, tilemaps (`Gen4Graphics`) | **Done** | +| Battle sprites — cipher **solved**, sprites decode | **Done** | +| Moves (`Gen4Moves`) — 471 records | **Done** | +| Items (`Gen4Items`) — 446 records | **Done** | +| Learnsets and evolutions (`Gen4Species`) | **Done** | +| Wild encounters (`Gen4Encounters`) — 183 areas | **Done** | +| Trainers and parties (`Gen4Trainers`) — 928 | **Done** | +| Packed trainer names (`Gen4Text`) | **Done** | +| Map matrix + land chunks + permissions (`Gen4Maps`) | **Done** | +| Map events: NPCs, warps, signs, triggers (`Gen4Events`) | **Done** | +| Map names (`Gen4Maps.mapNames`) — 593 | **Done** | +| Map 3D meshes (NSBMD) and BDHC height | Not started | +| Map header table (`Gen4MapHeaders`) — 593 in the ARM9 | **Done** | +| Script decoding (`Gen4Script`) — bytes to instructions | **Done** | +| Script lowering (`Gen4ScriptVM`) — 97.8% of instructions | **Done** | +| Archive member names, 68 archives (`Gen4Archives`) | **Done** — 9,511 names | +| Screen composition: NSCR+NCGR+NCLR (`Gen4Graphics.compose`) | **Done** | +| Battle presentation tables (`Gen4Battle`) | **Done** — 23 backgrounds, 24 terrains | +| UI screen recipes (`Gen4Screens`) | **Done** — 16 archives | +| NCER cell banks (`Gen4Cells`) | **Done** — 135 banks, 1,184 cells | +| NFGR fonts (`Gen4Font`) | **Done** — 4 fonts, 509 glyphs each | +| Extractor: cartridge to cache (`RomExtractorGen4`) | **Done** — 13 tables | +| Wiring the extractor into `RomImporter` | **Done** | +| Registering scripts per map (`Gen4ScriptVM.register`) | **Done** — 374 maps | +| Script lowering to engine commands | Not started | +| Graphics extraction stage (`extractGraphics`) | **Done** — 1,405 PNGs | +| Font extraction stage (`extractFonts`) | **Done** — sheets + width tables | +| Fonts published as `data.font` (engine shape) | **Done** — 1 page, 3 faces, 484 charmap entries | +| Per-map regions cut from shared grids (`Gen4Maps.extents`/`crop`) | **Done** — 72 regions | +| Map defs with events attached (`extractRegions`) | **Done** — 593 defs, 5,636 events | +| Tile behaviours, named (`Gen4Behaviors`) | **Done** — 256 values, all 75 in use named | +| Stand-in tileset (`Gen4Tileset`) | **Done** — 15 terrain classes, no renderer changes | +| Type chart from the battle overlay (`Gen4TypeChart`) | **Done** — 110 rows, 2 sections | +| `constants`, `type_chart`, flat `text` | **Done** — all 11 SHARED modules present | +| Gen 4 branch in `Data.lua` module lists | **Done** — Gen 1/2/3 proven unchanged | +| A Gen 4 cache satisfies every required module | **Done** — 15 of 15, 0 missing | +| Overworld NPC sprites (`Gen4Models`) | **Done** — 421 sets, 3,567 frames | +| NPC sprites published as `data.sprites` | **Done** — 421 entries, keyed by member | +| `graphicsId` → sprite, through the overlay-5 table | **Done** — 3,128 resolve, 427 have no billboard by design, 0 broken | +| Script ids classified into their bands | **Done** — 30 bands from the cartridge's own dispatcher | +| Every pickup resolved to its item (`Gen4Pickups`) | **Done** — 329 balls + 262 hidden = 591 of 591, across 140 maps | +| Every map trainer resolved to its trainer record | **Done** — 417 of 417, class names from the ROM's own bank | +| Map height field (`Gen4Bdhc`) | **Done** — 666 of 666 chunks, 8,974 plates, 0 failures | +| Map meshes (NSBMD) | **Read and seen** — 666 terrain + 590 building meshes exact, rendered as recognisable Sinnoh; per-map texture sets unfinished; the ENGINE draws none of it | +| Species battle pictures (`Gen4Pokegra`) | **Done** — 493/493, front/back/shiny/shiny-back + 2-frame strips | +| Species pictures stamped onto `data.pokemon` | **Done** — the Gen 3 field names, no Gen 4 branch | +| Form sprites (`Gen4Otherpoke`) | **Done** — 78 forms, 463 images, Substitute + shadows | +| Form *switching* (weather, plate, appliance, letter) | Not started — engine logic, not extraction | +| Battle *engine* running on Gen 4 data | Not started | +| Summary/party screens running on Gen 4 data | Not started | +| Title sequence running | Not started | +| Dual-screen + Poketch presentation | Designed, not built | +| Start-menu style switch | Designed, not built | +| `importable` flipped to `true` | **Yes** — the launcher imports Platinum; the pill still says WIP | + +The cartridge is **recognised but withheld**. A player who owns it can reach +Platinum's panel in the launcher and read exactly what is and is not ready, +rather than meeting "Not supported yet", which reads as "your dump is wrong" +and sends people hunting for another one. + +--- + +## The cartridge, measured + +Every figure here was read from the project's own dump, not copied from a +format note. + +``` +title POKEMON PL game code CPUE maker 01 +unit 0 version 1 (Rev 1) size 134,217,728 bytes (128 MiB) +used 104,607,804 bytes +sha1 0862ec35b24de5c7e2dcb88c9eea0873110d755c +md5 ab828b0d13f09469a71460a34d0de51b + +ARM9 rom 0x00004000 size 1,057,784 ram 0x02000000 entry 0x02000800 +ARM7 rom 0x00409800 size 161,788 +FNT 0x00431000 (7,092) FAT 0x00432C00 (3,696) -> 462 entries +overlays 122 on the ARM9, 0 on the ARM7 +filesystem 340 files in 89 directories +``` + +462 FAT entries = 122 overlays + 340 files, and the last byte any file +occupies is 104,607,804 — the used-size the header itself reports. The +filesystem walk accounts for the cartridge exactly. + +Rev 0 (`ce81046eda7d232513069519cb2085349896dec7`) is registered as an +alternate hash. pokeplatinum builds both and the filesystem layout is the same +in each, which is what the extractor reads. + +### Files that will matter + +| Path | Bytes | What | +|---|---|---| +| `/fielddata/land_data/land_data.narc` | 16,125,840 | map terrain | +| `/poketool/pokegra/pl_pokegra.narc` | 11,778,676 | species sprites | +| `/msgdata/pl_msg.narc` | 4,351,320 | all text — 724 banks, 46,053 strings | +| `/data/mmodel/mmodel.narc` | 1,465,988 | overworld models | +| `/itemtool/itemdata/pl_item_data.narc` | 440,460 | items | +| `/fielddata/script/scr_seq.narc` | 304,476 | 1,124 script files | +| `/fielddata/encountdata/` | 237,324 | wild encounters | +| `/poketool/personal/pl_personal.narc` | 105,920 | 508 species records, 44 B each | + +Message banks located by looking rather than assumed: **412** is species names +(entry 1 BULBASAUR, entry 25 PIKACHU), **706** is Pokedex entries. + +Platinum keeps Diamond/Pearl's un-prefixed files beside its own `pl_`-prefixed +ones. The `pl_` file is the one to read; the other is the older game's. + +--- + +## Staying legal + +Identical to every other version here, and the reason the two new readers came +first. + +* **No cartridge data is committed.** Not a table, not a tile, not a string. +* The ROM stays where the player keeps it. The importer reads it, writes a + cache under `platinum/` (`cachePrefix`), and that cache is what the game + loads through `CacheFs.mountVersion`. +* What lives in this repository is **scaffolding**: readers, tables of + *offsets and shapes*, and code that knows how to ask the cartridge a + question. Gen 4 does not change the rule; it only changes the question from + "what is at address X" to "what is inside file Y". +* `Gen4ScriptOps.lua` is opcode numbers, command names and operand widths + derived from pret/pokeplatinum — a description of the format, the same thing + `Gen3ScriptOps.lua` is for Emerald. No script bytes from the cartridge. + +--- + +## What is built + +### `src/core/GameVersion.lua` + +`platinum` registered: generation 4, both revision hashes, `cachePrefix +"platinum/"`, `saveSuffix "_platinum"`, `importable = true` (see *Turning it +on*), `experimental`, +and a new `dualScreen = true`. Two new predicates, `GameVersion.isGen4(id)` +and `GameVersion.isDualScreen(id)` — the second asked of the *version* rather +than computed from the generation, because "is Gen 4" and "has two screens" +are not the same claim and field code should say which one it means. + +`GameVersion.ORDER` puts platinum after emerald and before the hacks: +cartridges in generation order, then romhacks. + +### `src/import/RomImporter.lua` + +Platinum has a chip in the tab row. **This is the only navigation into a +version's panel** — Prism was once registered without one and was hashed, +counted and completely unreachable, and the code comment saying so is still +there. Also: + +* 128 MiB added to the accepted ROM sizes, `.nds` to the accepted extensions, + and to all five native file pickers (macOS, Windows, zenity, kdialog, and + the multi-select variants) plus the drag-and-drop hint. +* A `pokemon_platinum.nds` name for the panel, via a generation-4 branch. +* Chip colour `#a6b0d8 → #4a5286`: the metal with the cool cast the box art + has, kept clear of Silver's near-white, Crystal's cyan and Polished + Crystal's purple, because all four sit in one row. +* **A bug fixed on the way.** The "recognised, not playable yet" panel had one + hardcoded sentence — *"Prism imports its data, but its scripts still + misbehave"* — shown for **every** withheld game, under a comment reading + "ONE SENTENCE PER GAME". Prism is importable now, so Platinum was about to + become the only withheld game and its panel would have explained Prism. + Replaced with `RomImporter.WITHHELD_REASON`, keyed by version. + +### `src/import/NdsRom.lua` + +The DS filesystem. `open` / `header` / `list` / `stat` / `read(path)` / +`readId` / `arm9` / `overlay(n)`. + +A DS cartridge is a **filesystem, not an address space**, which is the single +biggest structural break from every generation above. `rom:u16(0x3DF884)` has +no meaning here; `rom:read("/poketool/personal/pl_personal.narc")` does. + +It never loads the cartridge into memory. 128 MiB as one Lua string is a +number this engine has to run alongside on a phone, and nearly every stage +wants one file. The handle stays open and ranges are read on demand. + +Two traps it avoids on purpose: the FNT is walked **by directory id**, not +linearly, because the subtables are not stored in tree order and a forward +walk builds a plausible tree with the wrong parents; and an overlay's bytes +come from the **file id at +0x18** of its table entry, not from the overlay +index, which are different numbers that look interchangeable. + +### `src/import/NarcArchive.lua` + +`parse(data)` / `count` / `range(i)` / `get(i)` / `all()`. Chunks are located +by walking, not by fixed offset — the header size is a field precisely because +it varies. Verified on two archives of very different shape: `scr_seq.narc` +(1,124 members) and `pl_personal.narc` (508 members of 44 bytes, which is +Platinum's species count and its personal-record size). + +### `src/import/Gen4ScriptOps.lua` + +All 840 opcodes, `$000`–`$347`, with names and operand specs. + +Gen 4 reads its opcode as a **halfword**, not a byte — 840 commands do not fit +in one — so the smallest instruction is two bytes and a Gen 3 decoder fed Gen +4 bytes desyncs on the first command. + +**Where the widths come from, and why that is the whole story.** +`Gen3ScriptOps.lua`'s header records what guessing cost: Emerald's `$E0` was +reasoned out from handler call shapes, came out two bytes short, and produced +not garbage but the Sootopolis cutscene playing twice with no warp home — +because a two-byte slip lands on a plausible opcode and the walk sails on. So: + +* opcode numbers are the **order of the `ScriptCommand()` lines** in + pokeplatinum's `include/data/scripts/scrcmd.h`. Position *is* the opcode, + so all 840 are listed including dummies. Two of those lines are spelled in + mixed case (`SCRCMD_GetExchangeServiceCornerItemAndCost` at `$2A2`, + `ScrCmd_GETRANDOMBATTLEGROUNDTRAINERS` at `$2FB`); a reader matching only + `[A-Z_]` silently drops them and shifts **every opcode after `$2A2` down by + one** — the same class of failure, one level up. This was hit and fixed. +* operand widths are the stream reads in each handler body, in source order. + The five primitives consume fixed widths (`ReadByte` 1, `ReadHalfWord` 2, + `ReadWord` 4, and the inlines `GetVar`/`GetVarPointer` 2 each, both + `ReadHalfWord` underneath). + +**The two ways that could still be wrong, both measured:** + +1. A handler reading inside an `if` has no fixed width. Nine do. They are + marked `*` and listed in `VARIABLE_LENGTH` — **not guessed**. +2. A handler delegating its read to a helper would hide operands. Every + function in pokeplatinum taking a `ScriptContext *` and transitively + reaching a stream read was collected, and every handler body checked + against that set: **zero** handlers delegate. + +**Corroboration, which is not proof.** Walking all 1,124 members of +`scr_seq.narc` from all 4,079 entry points decodes 50,835 instructions across +381 distinct opcodes and reaches a clean `end` **4,070 times with no unknown +opcode**; 8 stop at a variable-length command and 1 runs past its member's end +(one member's header parse, listed under open questions). Gen3ScriptOps +explains precisely why this is corroboration and not proof. The proof is the +handler source; the corpus only shows nothing contradicts it. + +### `src/import/Gen4Text.lua` + +All of Platinum's text: 724 message banks, 46,053 strings. Each bank is +encrypted **twice** — once on its table of offsets and lengths, again with a +different key on the characters: + +``` +entry table: k = (seed * 765 * (i+1)) & 0xFFFF; k |= k << 16 + offset ^= k; length ^= k (length counts CHARACTERS) +characters: key = ((i+1) * 596947) & 0xFFFF + each u16 ^= key; key = (key + 18749) & 0xFFFF +``` + +Both keys stay in 16 bits. In pokeplatinum's C the character key is a **u16 +parameter** immediately overwritten by a 32-bit product, so the truncation is +the declaration doing it — and a 32-bit key decodes the first character +correctly and then drifts, which reads as a charmap problem rather than a +cipher one. + +**Why this one is known to be right, unlike a script table.** All 46,053 +strings decrypt and all 46,053 end in the `0xFFFF` terminator. A wrong key +does not land on a terminator by accident forty-six thousand times, and text +either reads or it does not — there is no plausible-but-wrong here. A second, +independent Python implementation was run over the whole cartridge and +compared line for line against the Lua: **zero differences**. + +Escapes: `0xFFFE` introduces `command, argc, args...`; if the command's high +byte is a STRVAR base its low byte is the variable index. 68 distinct escape +commands occur in the cartridge and **every one is covered** — no unknown +control codes anywhere. `0xF100` starts a bit-packed trainer name terminating +on `0x01FF` (885 strings); not decoded yet, marked and stopped rather than +printed as characters. + +### `src/import/Gen4Graphics.lua` + +Nitro containers and the LZ77 the cartridge wraps most of them in. Magics are +stored reversed, like NARC's chunk tags: `RLCN` = NCLR (palette, section +`TTLP`), `RGCN` = NCGR (tiles, `RAHC`), `RCSN` = NSCR (tilemap, `NRCS`), +`RECN` = NCER (cells). Sections are found by walking from `headerSize` for +`sectionCount` chunks — both fields exist because they vary. + +Two things believed from arithmetic rather than from a field: + +* **Bit depth**, because `tilesX * tilesY * 32 == dataSize` settles 4bpp + outright and cannot be misread. +* **Colour count**, as `dataSize / 2`. The palette depth field is the one + field here whose meaning did not survive inspection — a 16-colour Platinum + palette carries `4` where the published tables say `3` — and since the byte + count is unambiguous, nothing needs the enum to be right. + +`tilesX`/`tilesY` are `0xFFFF` on an **unsized** sheet (every party icon is +one); the tile count always comes from the byte count instead. + +**Validated by looking, not counting.** All 540 party icons in +`pl_poke_icon.narc` render as recognisable Pokémon in the right colours. The +LZ was run over every compressed member of four graphics archives: **125 of +125** decompress to exactly their declared size and each one turns out to be a +valid Nitro container (48 NCGR, 27 NSCR, 23 NCER, 23 NANR). And a second, +independent Python implementation was compared pixel for pixel across all 540 +icons — 1,105,920 pixels, **zero differences**. + +That cross-check earned its keep immediately: it caught a real bug in the Lua, +where the tile `dataOffset` (which is relative to the start of the section's +*fields*) had the 8-byte tag/size header added a second time. That does not +crash — it produces recognisable shapes displaced by a quarter tile, which is +exactly the kind of wrong that survives a glance. + +### `src/import/Gen4Species.lua` + +508 records of 44 bytes from `pl_personal.narc`, with names from bank 412. +The 44 summing exactly is the check that the layout is read right, and the +values agree with the published tables (Pikachu 35/55/30/90/50/40 with +Static; Giratina 150/100/120/90/100/120; Arceus 120 across). + +**One trap worth naming:** the stat order is hp, attack, defense, **speed**, +spAttack, spDefense. Speed is fourth, not last. Reading it in the display +order every stat screen uses swaps Speed with Special Attack and produces a +table that looks entirely plausible and is wrong for every species. + +This stage also proves the floor: cartridge → `NdsRom` → `NarcArchive` → +record + `Gen4Text`, four modules and no special cases. + +### `src/import/Gen4Moves.lua` and `src/import/Gen4Items.lua` + +471 moves of 16 bytes from `pl_waza_tbl.narc`; 446 items of 34 bytes from +`pl_item_data.narc`. Names come from message banks 647 and 392, both found by +looking rather than assumed. + +**Priority is signed.** Quick Attack is +1 and Roar is −6, and reading that +byte unsigned turns every negative-priority move into one that goes first — +no crash, no visual glitch, nothing a battle looks obviously wrong for. It +just quietly plays a different game. Verified: Quick Attack +1, Roar −6. + +Item record checks against the published tables: Master Ball 0, Ultra Ball +1200, Poké Ball 200, Potion 300, HP Up 9800. Moves: Pound 40/100/35 normal +physical, Thunderbolt 95/100/15 electric special, Hyper Beam 150/90/5, +Struggle 50 power with 0 accuracy and 1 PP. + +### Learnsets and evolutions (in `Gen4Species`) + +`wotbl.narc` holds one **variable-length** learnset per species — 24 to 36 +bytes, terminated by `0xFFFF`. Each entry is a u16 packing the move in the low +**9** bits and the level in the high **7**. That split is forced: 467 moves +need 9 bits and level 100 needs 7. Reading it the other way round gives move +ids under 128 at levels in the hundreds — a table that still looks plausible. + +The structural check is what confirms it: across all 493 species, **6,598 +learnset entries, zero move ids out of range and zero levels outside 1–100**. + +`evo.narc` holds seven `{method, param, target}` slots per species in 44 bytes. +**Every method number was derived from the cartridge and checked against a +species that can be named**, because the enum lives in a generated header +pokeplatinum builds and does not ship — there was nothing to copy. Grouping +all 246 evolutions by method identified each one: 1 friendship (Golbat, +Chansey, Pichu), 4 level (162 of them), 8/9/10 Tyrogue's three branches, +11/12 Wurmple's personality split, 15 Feebas at beauty 170, 18 Happiny by day +and 19 Gligar by night, 20 knows-move (Aipom/Double Hit), 21 species-in-party +(Mantyke/Remoraid), 24 magnetic field, 25/26 the mossy and icy rocks. + +`param` means a different thing per method — a level, an item, a move, a +species, or nothing — so `paramKind` says which. A caller that treats it as +one thing gets Pikachu evolving at level 83. + +### `src/import/Gen4Encounters.lua` + +183 areas of exactly 424 bytes — and that 424 is the check, because +pokeplatinum's `WildEncounters` struct sums to 424 to the byte. + +Two things that look like mistakes and are not. **Species ids are four bytes +here**, not the two they are everywhere else in the cartridge, with three +bytes of padding after each one-byte grass level; reading them as u16 halves +the stride and walks the table into itself. And **a water slot stores maximum +level first, then minimum** — the cartridge really does put the larger number +first, and reversing it prints "Lv55-30", which reads as a display bug rather +than a parse one. + +Twelve of the 183 areas have a grass rate of 0 with the whole grass table +zeroed: water-only routes, where level-0 slots are correct data. Validated on +that basis — 2,196 grass slots, zero species out of range, and **zero +out-of-range levels in any area whose rate is non-zero**. The first land area +comes out Geodude, Zubat and Onix at levels 4–8, which is Oreburgh Gate. + +### `src/import/Gen4Trainers.lua` + +`trdata.narc` (928 × 20-byte headers) and `trpoke.narc` (928, variable) share +an index and are meaningless apart. + +**The header's `monDataType` sets the party stride, and it must be read rather +than inferred by dividing the party file by the party size.** Dividing works +for 887 of the 928 and then quietly does not: 39 party files carry trailing +padding, so the division lands on 12 or 20 where the real stride is 10 or 18, +and every mon after the first comes out shifted. Reading `partySize` entries +at the type's own stride and ignoring the remainder is correct for all 928. + +`species` also carries the form in its high bits, so the id is the low 10 — +which matters for Wormadam cloaks and Rotom appliances. + +Validated across the whole archive: **1,878 party members, zero species, +level, move or item values out of range.** Roark comes out Geodude 12 / Onix +12 / Cranidos 14 with Stealth Rock; Cynthia's six are right down to Garchomp +at 62 holding a Sitrus Berry. + +### Packed trainer names (closing the last text gap) + +Bank 618 has exactly 928 entries — which is how it was identified as the +trainer-name bank — and 885 of them use the `0xF100` sub-format that was +deferred earlier. + +A packed name is **nine bits per character inside fifteen usable bits** of +each halfword, not sixteen, so characters straddle halfword boundaries and one +bit per halfword is skipped. It terminates on `0x01FF`, not `0xFFFF`. Both +oddities matter: packing into 16 bits decodes the first character correctly +and then drifts (which reads as a charmap problem), and watching for `0xFFFF` +means never stopping, so the name runs on into whatever follows. + +All 885 decode with no unmapped character, every gym leader resolves, and the +full 46,053-string corpus was re-diffed against the independent Python +implementation afterwards — still zero differences. + +### `src/import/Gen4Maps.lua` + +**This is where Gen 4 stops resembling every generation above it.** Gen 1–3 +give you a map: a grid of metatile ids plus a tileset, and the renderer draws +it. Gen 4 gives you a *matrix* of fixed 32×32 chunks, and each chunk carries a +movement-permission grid, a list of placed building models, a **3D mesh**, and +a separate height structure. The picture is a mesh, not a tilemap — but the +permission grid is a plain 32×32 array of u16, and that alone is enough to +know where the player may walk. + +**The matrix** (`map_matrix.narc`, 289 members): + +``` +u8 width, u8 height, u8 hasHeaders, u8 hasAltitude, u8 nameLength, char name[] +u16 headers[w*h] -- only when hasHeaders +u8 altitudes[w*h] -- only when hasAltitude +u16 mapIds[w*h] -- always; indexes land_data.narc +``` + +The two optional blocks are why this has to be parsed rather than indexed: a +reader that assumes they are present takes the map ids out of the header table +and builds the world from the wrong chunks. **All 289 members account for +their length exactly** under this layout — that is the check. Matrix 0 is +30×30 and named `map`: the Sinnoh overworld. + +**The chunks** (`land_data.narc`, 666 members): four u32 sizes, then +permissions, objects, an `BMD0` NSBMD mesh, and a `BDHC` height block. All 666 +satisfy `16 + the four sizes == file length`, all 666 have a 2048-byte +permission block, all 666 carry a BMD0 model. + +**Permissions**: one u16 per tile. Bit 15 marks a tile that is not part of the +map — the void around the land — and the low byte is the terrain behaviour. +Across the whole overworld only 54 distinct values occur and only **two** +distinct high bytes, which is what says bit 15 is a flag and not part of a +number. + +**Validated by looking.** The 30×30 matrix assembled into a 960×960 permission +image is recognisably Sinnoh: Mt. Coronet running north–south through the +middle, the three lakes as enclosed pockets, the Great Marsh's grid at +Pastoria, Route 223's water column up the east coast to the Battle Zone. A +layout error anywhere in the matrix or the chunk header would have scrambled +that beyond recognition. The Lua render was then compared pixel-for-pixel with +an independent Python one: the only differences are the 432 empty matrix cells +where one drew black and the other drew the void colour — **every real +permission value agrees**. + +**Objects** are 48 bytes each, and the offsets were not read off one sample: +every field of all 3,476 placed objects was tabulated. That is what pins the +scale at +28/+32/+36 rather than the +24/+28/+32 a hand-read hex dump +suggested — an error that silently produced `scale 0.0`. Fields +16/+20/+24 +are **always 0** and are left unnamed on purpose: they are almost certainly a +rotation, but with every object in the game at zero there is nothing to tell a +rotation from a reserved field, and naming one would be a guess dressed as a +fact. All 360 distinct model ids fall inside `build_model.narc`. + +### `src/import/Gen4Events.lua` + +`zone_event.narc`, 534 members — who stands on a map, where its exits go, and +what watches the player. + +**The counts are interleaved**, not four counts up front: each block is a u32 +count immediately followed by its own records. + +``` +u32 n; Sign[n] 20 bytes u32 n; Warp[n] 12 bytes +u32 n; Npc[n] 32 bytes u32 n; Trigger[n] 16 bytes +``` + +Reading four counts first looks right on member 0 — sixteen zero bytes, which +is an empty map either way — and then falls apart on every populated map. The +interleaved layout accounts for **all 534 members exactly**, and it is the +only combination in a search over every stride from 8 to 40 that does; the +next best fits fewer than 15. + +**Which block is which was settled by measurement, not by position:** + +* **NPCs** — all 3,555 records have a model id below 470, exactly the member + count of `mmodel.narc`, the overworld model archive. +* **Warps** — 1,207 of 1,213 have a destination below 593, the number of names + in `mapname.bin`; the other six are 4095, a "nowhere" sentinel. The fields + at +0/+2 reach 909, so they cannot be map ids and are coordinates. +* **Triggers** — **all 186** carry a value at +14 of `0x4000` or above, which + is Gen 4's variable space. A trigger is a variable, a value to match, an + area and a script. +* **Signs** — the remaining block, and the one identification here that is + inference rather than proof. Its records carry a script id, a position (+4 + and +8 are u32: the halves at +6 and +10 are zero in all 682) and a small + type field, which is the shape of an interactable. Labelled as such, with + that caveat kept in the source. + +Validated across every zone: 534 parsed exactly, zero failures, zero NPC +models outside `mmodel`, zero warp destinations outside the name table, zero +triggers below the variable base. Spot-checking one city's warps resolves them +to its two routes and its Pokémon Center by name. + +`Gen4Maps.mapNames` reads `mapname.bin` — 593 sixteen-byte zero-padded ASCII +names, which is the count every warp destination stays below. + +### `src/import/Gen4MapHeaders.lua` + +Every other Gen 4 table lives in the filesystem and opens by name. This one +does not — it is compiled into the **ARM9 binary**, and it is the record that +ties a map to its matrix, area data, script file, text bank, wild encounters, +events and music. Without it the other modules each read correctly and none of +them knows which map it belongs to. + +593 records of 24 bytes, matching pokeplatinum's `MapHeader`. + +**Found by searching, not by a hardcoded offset.** In this cartridge it sits +at ARM9 offset `0x0E601C` (RAM `0x020E601C`), but that is a fact about one +build — Rev 0 is a different binary, and a fixed offset there would read +whatever happens to live at that address and hand back 593 confident, wrong +records. `find` scans for a run of 24-byte records whose every field indexes +*inside* the archive it names: matrix < 289, events < 534, scripts < 1124, +messages < 724, encounters < 183 or the 0xFFFF sentinel, area < 75. + +**Why 593 and not 594.** A loose filter finds 594 consecutive plausible +records; the 594th is not a map. Tightened against the area-data archive it +fails immediately, while all of 0..592 pass **every** cross-check against six +separate archives. 593 is also exactly the number of entries in +`mapname.bin`. Taking the loose answer would have added one phantom map — the +kind of off-by-one that only surfaces when something walks the whole table. + +The strongest confirmation is coverage rather than shape: the 593 headers +between them reference **all 534** members of `zone_event.narc`, highest index +533, none left over and none out of range. + +#### A join that succeeded and was wrong + +`mapLabelTextID` indexes **message bank 433** — 126 display names, "Jubilife +City", "Old Chateau", "Rock Peak Ruins". It does *not* index `mapname.bin`, +which is a separate table of 593 **internal** identifiers keyed by the header +id itself: `C01`, `C05GYM0113`, `D25R0106`. + +Both tables have an entry for every map, so joining `labelText` to +`mapname.bin` never errors and never looks broken — it just quietly reports +that Jubilife City is called "C01PC0101". The bound that made it look correct, +`labelText < 593`, holds trivially: `labelText` never exceeds 125. This was +caught by checking a name against the game rather than against a range. + +With both joins right the chain reads: header 3 is **Jubilife City** (`C01`) +on the 30×30 overworld matrix with 33 NPCs and 14 warps, bike, run and fly all +allowed; header 100 is **Hearthome City**'s gym (`C05GYM0113`), 1×1, none of +the three allowed; header 300 is the **Old Chateau** (`D25R0106`) with +encounter table 130. + +#### And the event structs corrected an axis mistake + +pokeplatinum's `MapHeaderData` names its four event arrays `bgEvents`, +`objectEvents`, `warpEvents`, `coordEvents` — in exactly the order the file +stores them, which confirmed the block identification including the one that +had only been inferred. The structs also showed that measuring alone had +produced the right fields with the **wrong axes**: Gen 4 is 3D, so the two +horizontal axes are X and **Z**, and **Y is height**. The byte census found +the height fields sitting at zero on most events and filed them as padding; +they are not, and an object event's height is a 20.12 fixed-point value at +28 +rather than the u16 at +30 a census suggested. A census tells you which bytes +vary. It cannot tell you what they mean. + +### `src/import/Gen4Script.lua` + +`Gen4ScriptOps` says how wide every command is; this walks a real script file +with it and produces instructions — opcode, name, operands, and for a jump the +absolute target rather than the relative offset the cartridge stores. Lowering +sits on top of this; nothing here decides what a command *means*. + +**A member is not a script.** Each of `scr_seq.narc`'s 1,124 members holds +several scripts behind a header of u32 offsets that are relative *to the +position after the offset word*. The header ends either at the halfword +`0xFD13` or, more often, simply where the first script begins — there is no +count. So the walk stops when the cursor reaches the lowest target seen, the +only rule that works for both shapes. Reading a count that isn't there takes +the first script's opcodes as more offsets. + +**Jumps are signed and relative to the end of the instruction.** `goto` and +`call` measure from the byte *after* the operand, and the offset is negative +for every backward jump, which is most loops. Reading it unsigned sends a loop +several gigabytes forward and the decode just stops — which looks like a short +script, not a misread operand. + +**How the jump formula was checked**, since a decoder that reads its own output +will agree with itself all day: starting at every entry point and following +every jump **transitively** reaches 8,567 basic blocks and 78,093 instructions +— twice what a linear walk sees — and of the **15,002 jump operands in them, +zero point outside their own member**. 8,549 of those blocks then decode to a +clean `end`. + +That tests two things at once. A wrong sign or base would scatter targets to +negative numbers and gigabyte offsets; a wrong operand width anywhere earlier +in an instruction would shift the jump operand itself and produce the same +mess. Neither happens. Four blocks of 8,567 stop unexpectedly — two run off the +end, two reach an opcode not in the table — which is 0.05%, recorded rather +than smoothed over. + +**Coverage, which is what makes lowering plannable:** 4,079 scripts from the +entry points, 50,835 instructions, 381 distinct opcodes. + +| commonest N opcodes | share of instructions | scripts fully covered | +|---|---|---| +| 20 | 87.3% | 58.3% | +| 40 | 95.9% | 78.4% | +| 80 | 98.3% | 88.7% | +| 150 | 99.3% | 94.5% | + +The top of that list is the shape of a conversation — `lockall`, `faceplayer`, +`message`, `closemessage`, `releaseall` — so a lowering that starts there gets +NPCs talking before anything else works. Decoding a real one gives a Poké Mart +clerk: `playse / lockall / faceplayer / callcommonscript / closemessage / +pokemartcommon / releaseall / end`. + +### `src/script/Gen4ScriptVM.lua` + +A sibling of `Gen3ScriptVM` by design: `ScriptRunner` and `Commands.resolve` +are generation-agnostic, so a fourth generation needs a lowering and a verb +set, not a fourth script subsystem. Shared verbs (`jump`, `label`, +`show_text`, `ask`, `set_flag`, `play_sound`) are emitted as-is; anything Gen 4 +does that no earlier generation has gets a `g4_` verb, exactly as Gen 3 uses +`g3_`. + +**Measured against the whole cartridge** — 4,079 scripts, 50,843 instructions — +it lowers **97.8% of instructions** and leaves **85.7% of scripts** with +nothing unimplemented in them at all. + +It got there in three passes, and their shape is the useful part: + +| pass | instructions | whole scripts | +|---|---|---| +| the conversation set | 87.8% | 52.7% | +| + 8 (trainer preamble, signposts) | 97.3% | 82.4% | +| + 10 load-bearing | 97.8% | 85.7% | + +The conversation set alone reached 87.8% of instructions but only 52.7% of +whole scripts, because **a script is only as lowered as its worst command**. +The eight added next were the four generated trainer-battle commands — each +occurring exactly 928 times, once per trainer in `trdata.narc` — and the four +that drive a signpost, which have to lower together or a sign opens and never +closes. + +The last ten occur twenty-odd times each and are load bearing anyway: `warp`, +without which the player cannot leave a map; `starttrainerbattle`, without +which the trainer preamble runs and nothing happens; `pokemartcommon`, without +which the clerk says hello and sells nothing. **Frequency is a good guide to +what to lower first and a poor guide to what to stop at.** + +What remains is a flat tail of side systems — TV interviews, the journal, the +Battle Tower, Turnback Cave, the Poketch — at a few dozen occurrences each, +left as explicit `g4_unimplemented` rows rather than dropped. A silently +dropped command is a script that runs and quietly does the wrong thing, which +is much harder to find than one that reports what it could not do. + +The Poké Mart clerk now lowers completely: + +``` +{ play_sound, 1500 } { g4_lock_all } { g4_face_player } { g4_common, 2019 } +{ g4_close_message } { g4_pokemart, 1 } { g4_release_all } { jump, end } +``` + +**What is not here:** this is the lowering half. The extractor half — writing +a script pool into `data/generated` and registering a contribution per map +through `MapScripts` — is not built, so nothing calls this in a running game +yet. + +### `src/import/RomExtractorGen4.lua` + +Eleven modules read Platinum correctly and none of them wrote anything. This +is the stage runner that puts them in order and lands their output in +`data/generated`, which is what `CacheFs.mountVersion` serves to a running +game. + +**It takes a path, not the ROM's bytes** — the one place it deliberately does +not look like `RomExtractorGen2` and `RomExtractorGen3`. Those are handed the +whole cartridge as a Lua string, which is fine at 32 MiB. Platinum is 128 MiB, +the engine has to run alongside it on a phone, and almost every stage wants +*one file* out of a filesystem. `NdsRom` reads ranges on demand; handing it a +128 MiB string would undo that before the first stage ran. + +**Measured end to end against the cartridge: 4.2 seconds, 12 tables.** + +| table | entries | | table | entries | +|---|---|---|---|---| +| `gen4_text` | 724 banks | | `gen4_map_headers` | 593 | +| `gen4_species` | 508 | | `gen4_map_matrices` | 289 | +| `gen4_moves` | 471 | | `gen4_map_permissions` | 666 | +| `gen4_items` | 446 | | `gen4_map_objects` | 387 | +| `gen4_encounters` | 183 | | `gen4_events` | 534 | +| `gen4_trainers` | 928 | | `gen4_scripts` | 873 / 8,567 blocks | + +Every count matches what the individual modules measured independently, and +the spot checks land: Pikachu with 90 base speed, 13 learnset moves and one +evolution; Thunderbolt at 95; Potion at 300; Roark with three Pokémon; +map header 3 as Jubilife City (`C01`) on matrix 0. + +Two shape decisions worth naming. Permissions are stored as **one binary +string per chunk** rather than 1,024 numbers — the same choice Gen 3's map +grids make, because a 666-entry table of thousand-element arrays is slow to +load and enormous on disk while a string is neither. And **lowering does not +happen here**: the pool holds decoded instructions and `Gen4ScriptVM` lowers +at load time, so a lowering fix does not require re-importing the cartridge. + +The script pool follows jumps as well as entry points — a block reached only +by a `goto` is still a block the runner needs — which is why it holds 8,567 +blocks rather than the 4,079 a linear walk finds. + +### Wiring it into `RomImporter` + +The extractor dispatch already anticipated this. Its comment reads *"Adding a +fourth generation should be a line here, not a bug"* — and it very nearly was +one line: + +```lua +[4] = { module = "src.import.RomExtractorGen4", takesVersion = true, + takesPath = true }, +``` + +`takesPath` is the part that made it more than a line. `startData` now takes an +optional `sourcePath`, and `startPath` passes the whole path rather than +reducing it to a basename. Verification is unchanged — hashing a cartridge +means hashing its bytes and there is no way around that — but the moment the +SHA-1 matches, a Gen 4 import releases the 128 MiB string and hands the +extractor a path instead. Dropping the bytes *there* rather than after the run +is the entire point; keeping them live through extraction would have gained +nothing. + +**A dropped file may have no path.** `love.filedropped` gives a File whose +`getFilename` is a real path on desktop and need not be anywhere else, and the +Android save-directory scan reads through `love.filesystem` rather than the +disk. A Gen 4 import that gets that far without a path now fails with a +sentence telling the player to use the Import button, instead of passing `nil` +into `new` and failing inside `NdsRom` with something about a missing ROM +path. + +### Registering scripts per map + +`Gen4ScriptVM` gained `store`, `compile` and `register`, following the Gen 3 +contract exactly — including the `source` guard, because a cache built by a +different generation's extractor has a pool of the same *name* and a +completely different shape. + +**Gen 4 has no TEXT constant.** Gen 2 and Gen 3 key a map's `talk` table by +one, and the overworld looks a script up with `talkScript(mapId, npc.def.text)`. +A Gen 4 object event carries a script id and nothing else, so the key is the +script's own label — `M0002/S1321` — and both the extractor and the VM derive +it from `Gen4ScriptVM.label`, so they cannot drift apart. + +Map ids are the cartridge's **internal** names from `mapname.bin` — `C01`, +`C05GYM0113`, `D25R0106`. Unique, stable, and readable in a log. The +player-facing name is a different table and several maps share one, so it +would not do as a key. + +End to end: extraction 3.9s, **478 maps linked, 374 attached** with talk +tables, and Jubilife City's 21 scripts lower to real conversations and +signposts. + +#### A failure count that was wrong + +The first run reported 1,887 scripts linked and **2,350 "missing"** — more +misses than hits, which should never be believed without checking what the +misses are. They were not missing. Classified: + +| script id | events | what it is | +|---|---|---| +| `0` | 202 | no script | +| `1 .. entry count` | 1,887 | a real entry in this map's member | +| `>= 10000` | 796 | another namespace (590 are the single value 10001) | +| anything else larger | 1,352 | also another namespace | + +The values in that last band are plainly not indices: script `9300` in a +member holding 28 entries, `2035` in a member holding 4. Both large bands are +ids the **field engine interprets directly**, so they are now recorded as +`special` rather than counted as failures — and kept rather than dropped, +because knowing an object *has* one is worth more than silence. What each band +means is not claimed anywhere, because nothing has established it yet. + +--- + +## Presentation: battles, stats, menus, the title sequence + +Everything in this section was *decodable* before and none of it could be +**seen**. That gap is the whole subject: a Gen 1–3 screen is one picture at one +address, and a Gen 4 screen is three files at three unrelated indices in an +archive that records no relationship between them. + +### The thing that unlocked it: member names + +A NARC has no directory. Members are addressed by index and nothing inside the +file says what any of them is. pokeplatinum ships **71 `.order` files** — the +build's record of which source asset became which index — and matching them to +archives by member count resolves **68 of them, 9,511 names**, now in +`src/import/Gen4Archives.lua`. Names only; every byte still comes from the +player's own cartridge. + +Three are deliberately unresolved: `anim_ncer.order` and `anim_ncgr.order` both +match `wecell.narc` and nothing distinguishes them by count, and +`species_icons.order` has no archive of its length here. Guessing would put +wrong names on real members, which is worse than having none. + +**Why this is not a convenience.** In `pl_winframe.narc`, `message_box_00`'s +tiles are member 3 and its palette is member 26. Pairing members positionally — +in threes, or by adjacency — puts the wrong palette on every window frame in +the game. That does not fail; it produces a picture, in the wrong colours. The +names are what make the pairing checkable. + +### Composition + +`Gen4Graphics.compose` takes a tilemap, a tile sheet and a palette and returns +RGBA. `composeGroup` does the same from a named archive group. The tilemap +supplies, per 8×8 cell, a tile index, two flip bits and a 4-bit sub-palette; +4bpp sheets take their sixteen colours from that sub-palette, 8bpp sheets +ignore it. + +Verified by rendering, not by counting: the **Pokémon Platinum logo** comes out +pixel-correct from `titledemo.narc`, and the summary screen's **condition page +renders its COOL / BEAUTY / CUTE / SMART / TOUGH pentagon**. A tilemap walk +that were subtly wrong would not produce readable lettering. + +### Battle backgrounds — arithmetic, not names + +`pl_batt_bg.narc` is the one archive here with **no** `.order` file. Its 342 +members are an irregular run — 171 compressed, then 91 palettes, then 4 +tilemaps, then 76 more palettes — with no stride to find. The indices come from +what the game itself computes (`battle_display.c`): + +``` +tilemap = 2 -- shared by every background +tiles = 3 + background +palette = 172 + background * 3 + timeOfDay +``` + +Three facts, checked against the cartridge rather than taken on faith: + +* member 2 really is an NSCR and members 3…25 really are NCGR; +* members 172…240 really are NCLR — 23 backgrounds × 3 times of day; +* **all 138 member references resolve to the right kind of file, 0 wrong.** + +The count 23 is confirmed a second, independent way: `sFadeTargets` is a +per-background table with exactly 23 entries, and the only three that fade to +black instead of white are entries 9, 10 and 11 — which is precisely where the +three caves fall in the enum order derived separately from +`sTerrainForBackground`. Two unrelated tables agreeing on both the length and +the interior ordering is worth more than either one alone. + +**The tilemap is shared.** Every background reuses member 2, so a background is +a tile sheet plus one of sixty-nine palettes over a common arrangement. Looking +for a per-background tilemap that does not exist is the obvious wrong turn, and +it at least fails loudly — it produces nothing rather than something wrong. + +### Terrain platforms + +The ground each battler stands on is chosen from **the tile the player was +walking on**, not from the map — grass, sand, ice, snow, mud, cave and surfable +water each override the map's own background. 24 terrains, two sides, three +palettes; all 144 lookups resolve against `pl_batt_obj.narc`. + +Four terrains borrow another's art rather than having their own, and **BRIDGE +borrows a different one per side** — `path_puddles` for the player, `mud` for +the enemy. Collapsing that to one name per terrain would put a puddle under the +enemy on every bridge in Sinnoh, so `Gen4Battle.TERRAIN_ART` keeps the split. + +### Two naming conventions, and the planner that needed both + +The UI archives are named two incompatible ways, and a planner that knows only +one silently drops half the game's screens: + +* **Subject-named** — `logo.NCGR`, `logo.NCLR`, `logo.NSCR`. Grouping by base + name is exactly right. +* **Role-named** — `shop_gra` is `tiles.NCGR`, `default.NCLR`, `tilemap.NSCR`. + Every base name differs, so base-name grouping yields four groups of one and + not a single composable screen. + +A third case sits between them: `pl_bag_gra` names its sheet +`bag_ui_main_tileset.NCGR` and its tilemap `bag_ui_main.NSCR`. `Gen4Screens` +normalises away the role suffixes and then lets a group missing a part fall +back to the archive's own sheet — preferring **the sheet a tilemap already +points at**, because in `pl_bag_gra` the first NCGR is the player's bag *sprite* +and the screen sheet is the twelfth member. Taking the first one put the bag +sprite's pixels behind the bag's own UI, which is what the first run did. + +Borrowing is recorded on each job rather than hidden: a borrowed palette is a +guess, and a guess that is not marked is indistinguishable from a fact. + +### What the stage writes + +`RomExtractorGen4:extractGraphics` renders **416 images in under 2 seconds** and +writes them under `assets/generated/gen4/`, indexed by `gen4_graphics`: + +| Group | Contents | +|---|---| +| `battle/background/` | 23 backgrounds × 3 times of day, 512×256 | +| `battle/terrain/` | platforms, both sides, per time of day where it varies | +| `battle/` | healthboxes, type icons, interface, ball throws, trainer backs | +| `title/` | logo, "Developed by GAME FREAK inc.", screen borders | +| `menu/` `windows/` `touch/` `options/` | start-menu icons, window frames, touch buttons | +| `summary/` | all ten summary pages over one shared 480-tile sheet | +| `party/` `bag/` `trainer_card/` `poketch/` `shop/` `town_map/` `pokedex/` `mail/` `berry_tag/` `font/` | the rest of the interface | + +**Screens are final; sheets are provisional.** A group with a tilemap is drawn +as the game draws it. A group without one is an OAM sheet whose true +arrangement lives in an NCER cell bank, and it is laid out at a stated width so +it can at least be looked at. Every such image is flagged `provisionalLayout` +in the index rather than passed off as finished — correct pixels, placeholder +arrangement. + +--- + +### NCER: where a sprite's shape actually lives + +A tilemap says where every 8×8 cell of a *background* goes. A sprite has no +tilemap — it is drawn by the object engine from a handful of OAM entries, each +a rectangle of tiles at a signed offset from the sprite's centre — and its NCGR +records no width at all. `tilesX` and `tilesY` are `0xFFFF`. + +That is why a sheet laid out at a guessed width looks like the right picture +cut into strips and stacked wrongly: **the pixels were never wrong, the +arrangement simply was not in the file being read.** `src/import/Gen4Cells.lua` +reads the file it is in. + +Three things in the format are easy to get wrong and none of them fails loudly: + +* **The mapping mode is not decoration.** An OAM tile index is counted in units + of `32 << mappingMode` bytes, not in tiles. Platinum's banks use mode 1, so a + 4bpp index steps *two* tiles at a time. Reading it as tiles halves every + offset and assembles a real sprite out of the wrong halves of itself. +* **Positions are signed and centred** — Y is 8 bits, X is 9, both two's + complement. Unsigned puts everything above or left of centre at the far side + of a 256-pixel field. +* **Flipping a multi-tile entry mirrors the whole rectangle**, so the tile that + lands in a slot comes from the opposite corner *and* is itself drawn + mirrored. Doing only one of the two scrambles the sprite instead of + mirroring it. + +### Finding the bank for a sheet + +A bank is almost never named after the sheet it serves, because one bank +usually serves many. `Gen4Archives.cellBank` tries the spellings the cartridge +actually uses, strongest first, and **reports which one matched** so a weak +match is never presented as a fact: + +| Match | Example | Meaning | +|---|---|---| +| `named` | `healthbox/short` → `healthbox/short_cell` | its own bank | +| `folder` | `type_icons/fire` → `type_icons/cell` | one bank per folder | +| `folder` | `ball_throws/poke` → `ball_throws/shared_cell` | a named shared bank | +| `folder` | `terrain/grass/player` → `terrain/player_cell` | folder dropped, leaf kept | +| `prefix` | `bag_sprite_male` → `bag_sprite_cell` | the bank names a set | +| `sole` | `cheri` → `berry_cell` | one bank in the archive, many sheets | + +The terrain case is why this is a list and not a rule: **the bank is named for +the side and the terrain is the folder in between**, so nothing about the +sheet's own name predicts it. `sole` is a judgement rather than a reading, and +is recorded as such. + +Result: **302 of 458 sheets resolve to a bank.** The 156 that do not are in +archives holding no NCER at all — window frames, mail backgrounds, fonts — +which are tilesets rather than sprites, so there may be no bank to find. + +### What the banks corrected + +The platform sheets were being written 64×128, stacked eight tiles wide. The +banks say what they are: + +* `terrain/player_cell` — four OAM entries of 64×32 → the **256×32** player + platform +* `terrain/enemy_cell` — two entries of 64×64 → the **128×64** enemy platform + +All 24 terrains share those two banks, which is the reason every platform sheet +is exactly 128 tiles — a fact that had been noted and not explained. + +The stage now writes **1,405 images in 4.1 seconds, 0 skipped**: 69 +backgrounds, 88 platforms, 326 battle objects and 922 interface images, with +type icons, healthboxes, bag sprites, menu icons and Poketch digits all at +their real sizes rather than at a guessed width. + +**A bug in this work worth recording.** The first run assembled nothing in the +battle-object loop and every sheet quietly took the fallback path. The cause +was `local sheet, pal = palette and self:partsFor(...)` — in Lua, `x and f()` +is adjusted to **one** value, so `pal` was always `nil`. It did not error; it +produced the old behaviour, which is exactly the kind of regression that hides +behind a passing run. + +--- + +### NFGR: the fonts, and therefore every word the game says + +An NFGR is **not a Nitro container**. No `RGCN`-style magic, no tagged +sections — a 16-byte header, a run of glyphs, a table of widths: + +``` +u32 size offset where glyph data starts (0x10) +u32 widthTableOffset one byte per glyph +u32 numGlyphs +u8 maxWidth, maxHeight +u8 glyphWidth, glyphHeight -- in TILES, 1 or 2, not pixels +``` + +All four fonts account for themselves **exactly** — `16 + numGlyphs × 16 × gw × +gh + numGlyphs == the member's length` — which is a far stronger identity test +than a four-byte tag, and the only one available since they carry no magic. +509 glyphs each; system and message are 16×16 with mean advance 7.4px, +subscreen 9.1px, unown 12×16 with 399 zero-width glyphs (it defines only the +Unown alphabet). + +**The first trap is that these look compressed and are not.** Every NFGR begins +`10 00 00 00` — `0x10` is the font's own data offset, not a compression tag. A +decompressor that tests only the first byte accepts it, reads a declared output +size of **zero** from the next three bytes, and returns an empty string. Not an +error, not garbage: the file simply vanishes, and the caller sees a zero-length +member rather than a bad one. + +`Gen4Graphics.isCompressed` now also requires a non-zero declared size. Scanning +every named member in the cartridge, **9 of 9,511 would have been silently +emptied** by the old test — the four fonts, four `mmodel` animation files, and +`scripts_hearthome_city_dp_gym_leader_room`, a real script member. + +**The second trap is the bit order.** Glyph pixels are 2bpp packed +**MSB-first** — the opposite of the 4bpp graphics everywhere else in this +cartridge, where the low nibble is the first pixel. And within each two-byte +row the **high byte holds the first four pixels**, because the game reads the +row as a `u16` and looks up `row >> 8` before `row & 0xFF`. Getting either +backwards yields legible-looking glyphs mirrored in fours, which reads as a +font problem rather than a bit-order one. + +**The third is that a pixel is a role, not a colour.** The two bits select from +{ nothing, foreground, shadow, background }, and +`Text_GenerateFontHalfRowLookupTable` builds that lookup from three colours the +caller supplies **per text box**. So a glyph has no palette; the same glyph is +white-on-dark in a battle and dark-on-light in a menu. The cache keeps the +roles rather than baking one colouring in, which would freeze menu text into +battle colours. + +The default colouring leaves the **background role transparent**, and that is +worth stating because the alternative fails in an instructive way: on hardware +the background fills the whole 16×16 cell while a glyph only *advances* by its +width — six or seven pixels for most letters. Painting it opaque makes each +cell overwrite most of the one before it, and a line of text comes out as a row +of blocks with the letters crushed together. Legible, wrong, and easy to +mistake for a bad width table. + +**End-to-end proof.** Taking the string `PLATINUM 493!`, mapping each character +through `Gen4Text.CHARS` to its charcode, reaching the glyph by the game's own +rule (charcode 1 is glyph 0), unpacking the 2bpp bitmap and advancing by the +width table gives an 89-pixel line that renders as the words themselves in +Platinum's message font. That exercises the charmap, the glyph indexing, the +bit order and the widths in one test — any of them wrong and it does not read. + +--- + +### Publishing the fonts where the engine already looks + +`src/render/Font.lua` is generation-agnostic and already takes everything Gen 4 +needs: a page table, named faces beside it, per-glyph widths, and a charmap. So +the font stage writes `data.font` in **that** shape rather than a `gen4_` +spelling. A Gen 4-shaped font table would have meant a generation branch in +every screen that draws a word. + +``` +pages = { message = } +faces = { system, subscreen, unown } +charmap = 484 entries +frame = "drawn" -- Gen 4's window frames come from pl_winframe +``` + +**One page, three faces** — not four pages. All four fonts number their glyphs +from the same base, so registering them all as pages would leave the +code-to-page lookup answering with whichever sorted first. That is the exact +problem `Font.lua`'s own comment describes for Emerald's five Latin faces, and +its answer — a face is chosen by name, not resolved from a glyph id — is the +one taken here. + +**`base = 1`, not 0.** The cartridge reaches a glyph by subtracting one from the +character code, so code 1 is glyph 0. `Font.lua` indexes quads by `code - base` +and widths by `code - base + 1`; with base 1 both land on exactly the arrays the +stage writes, so neither side adjusts for the other. Verified end to end: code +299 → quad 298, width 6, character `A`. + +**The charmap is inverted from the text decoder**, so the two cannot disagree +about what a character is. Only codes the font actually *has* are included: +`Gen4Text` knows 2,876 characters and this cartridge's font holds 509 glyphs, so +the remainder are Japanese codes with nothing to draw and mapping them would put +a character on screen as whatever happened to sit at that quad. 484 of the 509 +are spoken for, and **none of them is a ligature** — no entry in range covers +more than one character — so there is no multi-letter sequence to mis-match +ordinary text the way Gen 3's `PK`/`MN` pair does. + +**The tone order is fixed by a shader, not by taste.** `Font.lua` recolours a +pre-tinted page by telling the two tones apart by **luminance** — `lum < 0.5` is +the ink, anything brighter is the shadow. A sheet baked light-on-dark would look +correct until the first `{COLOR}` swap, which would then paint the letter with +the shadow's colour and vice versa. So the sheet bakes the letter dark and the +shadow light, which also matches the Gen 3 page and the light boxes this engine +draws — one sheet, right for both paths. + +--- + +### A Gen 4 map is a region of a shared grid + +This is the structural break that everything downstream had to absorb, and it +does not exist in Gen 1–3 at all. + +A Gen 1–3 map **is** a grid. **84 of Platinum's 593 headers name matrix 0** — +the 960×960 Sinnoh overworld — and each one is a *piece* of it. Jubilife, +Route 201 and Oreburgh are regions of one seamless world, each contributing its +own NPCs and warps at coordinates measured from the **matrix's** corner rather +than their own. + +What says which piece is the matrix's per-cell header array — and only **2 of +the 289 matrices carry one**. For the other 287 the matrix is the map, which is +the ordinary case and needs nothing. + +`Gen4Maps.extents` reads that array into a bounding box per header, and +`Gen4Maps.crop` cuts the region out of the layout. Two things worth stating: + +* **Header 0 is excluded.** It owns 731 of the overworld's 900 chunks — ocean, + border, the space between routes — and giving it a box would produce one + "map" the size of Sinnoh overlapping every other. +* **A region is a bounding box, so four of the sixty-six are not solid + rectangles.** That is flagged as `regionSolid` rather than quietly squared + off. + +**Cutting is smaller, not larger.** The overworld's regions total 0.34 MB +against the layout's 1.76, precisely because header 0 is not a map. + +### Events, made map-local + +The events live per event-archive and their coordinates are matrix-global, +which is consistent: on the overworld the matrix is the only thing all 84 maps +share. **All 5,636 events in the cartridge fall inside their own map's matrix +grid** — that is what establishes the coordinate space rather than assuming it. +Subtracting the region origin makes them local, so a Gen 4 map's objects sit at +the same kind of coordinates a Gen 1–3 map's do and nothing downstream needs to +know the difference. + +**Gen 4 is 3D: X and Z are the two horizontal axes and Y is height.** So the +engine's `y` takes the cartridge's `z`, and the cartridge's `y` becomes +elevation. Reading them in the order they appear puts every object on the map's +top edge. + +Attached: 3,555 objects, 1,213 warps, 682 signs, 186 triggers. Warp +destinations are resolved from header id to internal map name at extraction, so +no two consumers can resolve them differently — **0 dangling**. 61 events (1.1%) +fall outside their map's bounding box, all of them in the four L-shaped +regions; they are counted and kept rather than dropped, because a strict bounds +test would silently delete real NPCs. + +**Verified by looking.** Rendering Jubilife's cropped grid with its events +overlaid puts every NPC on walkable ground, every warp on a building entrance, +and the triggers along the map edges where route transitions belong. A wrong +axis or a wrong origin would put them inside buildings. + +### One size trap, caught by measuring + +The first version inlined each map's grid into its def and the cache came to +**33.75 MB**. The twelve headers that name the overworld *without* claiming +chunks in it were each carrying the full 1.76 MB layout — 21 MB of one grid +repeated. A def now inlines `blocks` only when it owns that grid (it was +cropped, or no other header uses that matrix) and otherwise references +`layout`. **33.75 MB → 1.55 MB**, with 302 maps owning a grid and 291 +referencing a shared one. + +--- + +### Naming the ground: tile behaviours + +A Platinum permission cell is one `u16` — bit 15 says the cell is not part of +the map, and the **low byte is a behaviour**. Nothing in the cartridge says what +those 256 values mean, and only 75 of them ever occur, so colouring them by +value would have been guesswork dressed as data. + +pokeplatinum names all 256 and ships a flag table with them. +`src/import/Gen4Behaviors.lua` carries both. Two checks that make it more than +a transcription: + +* **All 75 behaviours that occur in this cartridge have a real name — zero land + on an `UNUSED_xNN` slot.** A single off-by-one in the enum ordering would + have broken that, so it is the ordering's proof as well as the table's. +* The flags matter more than the names, and are not derivable from them: the + cartridge marks six `UNUSED` behaviours as encounter tiles and eight as + surfable. Both come from `sTileBehaviorFlags`, not from reading names. + +Weighted by cells: **21.2% of walkable ground can start a wild battle, 12.1% is +surfable.** Grass is 4 behaviours, encounters 19, surfable 16, jumps 8 (with +direction), doors 1, warps 11. + +### A stand-in tileset, so a map can be built at all + +Gen 4's world is 3D — an NSBMD mesh and a texture set per chunk — and there is +no tileset in the Gen 1–3 sense anywhere in the cartridge. `MapLoader` pairs +every map def with one regardless, so until the mesh pipeline exists a Platinum +map could not be **built**, never mind drawn. + +`src/import/Gen4Tileset.lua` synthesises one: a flat colour per terrain class, +keyed by the behaviour byte every cell already carries. + +**It is shaped like a Gen 3 tileset, and that is the whole trick.** +`TileRenderer.gen3SheetsFor` takes any tileset whose `blockTiles` is 2 and hands +it to `Gen3Tiles`, which reads four plain binaries — 4bpp `tiles`, 16-byte +`metatiles`, 2-byte `attributes`, a `palettes` array. Producing those four is +cheaper than teaching the renderer a fifth kind of map, so **a Gen 4 map now +takes the same code path Hoenn does, with no renderer change at all.** Two +records are written, exactly as Gen 3 writes: the raw tileset in +`map_tilesets`, the pair record a def names in `tilesets`. + +256 metatiles, one per behaviour, so a metatile's `attributes` behaviour *is* +its own index — which is what makes `behaviourBytes` true here and lets +everything that asks a cell what kind of ground it is keep working. Two +palettes, base and darkened, checkered across each metatile's four tiles, so +the 16-pixel grid has a visible edge rather than reading as a silhouette. + +**Anything unclassified draws magenta on purpose.** The first pass left 0.50% +of cells magenta — but nearly all of them were *named* things the rules simply +did not cover: `BIKE_BRIDGE_*`, `SLIDE_*`, `REFLECTIVE`, +`DYNAMIC_HEIGHT_COLLISION`, `PASTORIA_GYM_*_GROUND`, `MART_SHELF_1`. Adding +those rules is reading, not guessing, and it took magenta down to **14 cells in +346,819 — 0.004%, every one a value the cartridge itself calls `UNKNOWN_xNN`.** +Magenta now means genuinely unknown rather than "no rule written yet". + +Cell-weighted census of Sinnoh: ground 74.9%, water 12.1%, cave 4.7%, grass +3.2%, snow 1.5%, mud 1.2%, sand 0.7%, bridge 0.6%, shallow 0.5%, ice 0.25%, +door 0.23%. The water figure lands on 12.07% against the 12.1% of cells the +flag table calls surfable — two independent routes to the same number. + +**Verified through the engine's own compositor.** `Gen3Tiles` was run against +the synthesised record directly: it reports 256 metatiles, resolves behaviour 2 +to the grass tile, 21 to water, 105 to the door colour and 56 to the ledge +colour, and bakes a 256×256 sheet in 30 colours (15 classes × 2 palettes). +Jubilife then renders from its own map def through that sheet — street grid, +building footprints, orange doorways — and all **593 map defs resolve a tileset +with 0 failures**. + +**What this is not:** an attempt at Platinum's art. It draws a legible plan, not +Sinnoh. The value is that the map, its collision, its warps and its NPCs can all +be exercised while the mesh pipeline is still missing. + +--- + +### The real reason nothing runs: the cache spoke the wrong names + +Every "Running: No" row in the tracker below had **one shared cause**, and it +was not in the extraction at all. + +`src/core/Data.lua` requires its modules by exact name — `constants`, `maps`, +`tilesets`, `text`, `text_pointers`, `pokemon`, `moves`, `items`, `type_chart`, +`trainers`, `encounters` — and the Gen 4 extractor was writing `gen4_species`, +`gen4_moves`, `gen4_maps` and the rest. **Of the eleven modules Data requires, +a Gen 4 cache provided one.** An import would have died at the first module +check with "missing generated data module 'data/generated/constants.lua'", +before a single byte of all that verified extraction was read. + +`data.constants.gen` alone is read in **seventy-one places**. It is the value +the whole engine branches on. + +Seven tables were renames — the shapes already matched, because they were built +to the engine's shape from the start. Four were genuinely missing: + +* **`constants`** — `gen = 4`, the eighteen type names, the twenty-five + natures, and the id-order lists. Types come from **message bank 624** and + natures from **bank 202**, both found by searching the banks rather than + assumed, which is also how the type *numbering* was established: bank 624 is + exactly `NORMAL … DARK` with the unused `???` at slot 9. Leaving that slot + out would shift every type above it. +* **`type_chart`** — see below. +* **`text`** — the same 46,053 strings, flat, keyed `TEXT_B_`. + Gen 1–3 address a string by one id; Gen 4 needs a bank and an index, so the + label carries both. `Gen4Text.label` owns the spelling so the writer and + whatever lowers a `message` command cannot drift — the same discipline the + script labels already use. +* **`text_pointers`** — required by `Data.lua` and read by nothing. Written + empty, because Gen 4 has no equivalent and inventing entries would put names + in the cache that resolve to nothing. + +### The type chart has two terminators + +It is not an 18×18 grid but a list of exceptions — attacker, defender, +multiplier in tenths — and everything unlisted is 1×. It lives in **overlay +16**, the battle overlay, *not* the ARM9; a search that only looks there comes +back empty. + +**108 rows, then `0xFE`, then two more rows — Normal vs Ghost and Fighting vs +Ghost, both immune — then `0xFF`.** The game's own damage routine loops until +`0xFF` and applies all 110; other code stops at `0xFE` deliberately, because +those last two are the immunities Foresight and Scrappy lift. A reader that +stops at the first terminator produces a chart in which **Normal does neutral +damage to Ghost** — nothing errors, the game is just wrong in a way that takes +a battle to notice. + +**A bug in my own search, worth recording.** The first version walked forward +and returned the first position that started a long enough run. That is wrong +twice: a coincidental triple just before the table can start a run one byte out +of phase, and skipping past a short run can step *over* the true start. It +locked onto 0x33B97 instead of 0x33B94 — row **two** — silently dropping +"normal vs rock, half damage" and reporting 109 rows. No count would have +caught that without knowing the answer first. The fix is to keep the **longest** +run and then extend it backward while the preceding triple is also valid, which +makes the answer independent of where the scan happened to lock on. + +Verified against known matchups: fire→grass 2×, water→fire 2×, electric→ground +0×, normal→ghost 0×, ghost→normal 0×, dragon→dragon 2×, fighting→dark 2×. And +exactly one binary in the whole cartridge carries a table of that shape. + +### Where the boot now stands + +All **11 SHARED modules are present**. What remains is that `Data.lua` has no +Gen 4 branch: it detects Gen 3 by the presence of `save_layout` and otherwise +assumes Gen 1/2, so a Gen 4 cache is asked for `trainer_headers`, `sprites`, +`field` and `battle_anims` — four Gen 1/2-shaped modules it has no business +having. + +That change is **deliberately not made yet**. It edits the module lists every +generation boots through, and unlike everything above it cannot be checked +offline — the constraint that Gen 4 work must not break Crystal, Gold, Silver +or Prism applies most sharply to exactly this file. It wants a `GEN4_MODULES` +list and a marker (`gen4_map_headers` is written by the Gen 4 extractor and +nothing else), made with the engine in front of you. + +--- + +### The Gen 4 branch in `Data.lua`, and how it was proven safe + +`Data.requiredModules` decides which module set a cache must provide. It knew +two answers — Gen 3 if `save_layout` loads, Gen 1/2 otherwise — so a Platinum +cache was being asked for `trainer_headers`, `sprites`, `field` and +`battle_anims`, four Gen 1/2-shaped modules it has no business having. + +The change adds a `GEN4_MODULES` list and a third answer. What is in it is +short, because a Gen 4 map def carries its own grid, warps and objects: +`map_layouts`, `map_tilesets`, `map_scripts`, `font`. What is *not* in it +matters as much: + +* **`save_layout` is deliberately absent.** It is the Gen 3 marker, and writing + one would make a Gen 4 cache claim to be Gen 3. +* `sprites`, `icons`, `scenes`, `songs`, `audio` are stages the Gen 4 extractor + does not have yet, and requiring a file nothing writes would refuse every + import. They sit in `OPTIONAL`, so a cache that gains one later picks it up + with no change here. + +A Gen 4 cache is also **blocked from the `CLASSIC_ONLY` modules**, exactly as a +Gen 3 one is and for the same reason: the version overlay is additive, so any +module Platinum did not write still resolves — to the root cache, which is +Red's. That is how NEW GAME on Emerald once opened Red's intro in Red's house, +and Gen 4 writes fewer of these than Gen 3 does. + +**This file is every generation's boot path, so it was not changed on +judgement.** Both the old and the new `requiredModules` were extracted and run +against four synthetic caches: + +| Cache | Old | New | | +|---|---|---|---| +| no marker (Gen 1/2) | gen3=false, 16 modules | gen3=false gen4=false, 16 modules | **identical** | +| `save_layout` (Gen 3) | gen3=true, 22 modules | gen3=true gen4=false, 22 modules | **identical** | +| `gen4_map_headers` | gen3=false, 16 modules | gen4=true, 15 modules | changed, as intended | +| both markers | gen3=true | gen4=true | gen4 wins | + +**Crystal, Gold, Silver, Prism and Polished Crystal take the first row; +Emerald and FireRed take the second. Both are byte-identical to before.** The +only behaviour that changed belongs to caches carrying a marker no existing +cache has. (The fourth row cannot occur — the Gen 4 extractor does not write +`save_layout` — and is listed because a precedence that is never exercised +should still be a decision rather than an accident.) + +Run against the real thing: the cache writes **23 tables**, is detected as +gen4, and satisfies all **15 required modules with 0 missing**. + +--- + +### The overworld NPCs did not need the 3D pipeline + +`mmodel.narc` reads as a wall. Gen 4's overworld is 3D, so the people in it +must be models, and models mean NSBMD, a mesh pipeline and a long detour. + +**They are not models.** 421 of its 470 members are **BTX0 — Nitro *texture* +archives** — and only 24 are BMD0. A Gen 4 NPC is a flat quad wearing a +texture: the geometry is shared and the per-character art is a texture set. The +member names say so outright — `youngster.nsbtx`, `lass.nsbtx`, `hiker.nsbtx`. + +> **This paragraph used to end "…and every object event in the map defs already +> carries a `graphicsId` that indexes this archive."** It does not index this +> archive, and believing it meant that not one of the 3,555 map objects was +> drawing its own art. See *The lookup that could not miss*. + +So the entire overworld cast comes out by reading textures, and the mesh work +is not on the path to it. + +**What a texture entry says.** Eight bytes: `{ u32 texImageParam, u32 +extraParam }`. The parameter is the **first** word — offset in bits 0–15 (in +units of 8 bytes), width and height exponents in bits 20–25, format in 26–28, +colour-0-transparency in bit 29. + +> **This section previously said the second word, and everything that followed +> from that is corrected below.** The full account is in *The dictionary that +> was only right for two* further down; the short version is that the reader +> was *also* four bytes short of the records, and on a two-texture member the +> two errors cancel exactly. 208 of the 421 texture members have two textures, +> which was enough to make the pair look right and to make every claim about +> "empty slots", exotic formats and enormous reserved regions below it false. + +Counted over all 3,567 entries, once both were fixed: **3,564 are format 3 (16 +colours) and 3 are format 2 (4 colours)**. That is the entire list. This +archive holds nothing but paletted sprite frames — no A3I5, no A5I3, no 4x4 +block compression, and not one empty slot. The other formats are still +**reported, not silently skipped**, in case a mod adds one. + +**A member is not only its character** — but it is much closer to it than this +document used to claim. Frame sizes across the archive are 32×32 (3,158), +16×32 (385), 64×64 (9), 16×16 (7), 64×32 (5), 128×64 (2) and 128×32 (1). A +member holds 1, 2, 3, 4, 7, 12, 13, 16, 17, 24 or 32 frames: **16 for a +standard NPC, 32 for the player, 1 for an item ball.** The strip is still +built from the **modal frame size** — whichever size the most decodable +textures in that member agree on — because a member can legitimately mix sizes, +but it is no longer separating art from reserved space, because there is no +reserved space. + +Result: **421 sprite sets, 3,567 frames** — every `BTX0` member in the archive, +none skipped, nothing undecodable. + +Two small things worth keeping, because both were caught by a number looking +wrong rather than by an error: `2^n` in Lua is a **float**, so every size +reported as `32.0x32.0` and any key built from it failed to match the integer +one a caller would write. And the palette dictionary's offsets *appeared* to +read implausibly — one entry claiming 3,064 bytes into a 32-byte region — which +was the same records bug; with it fixed, all 618 palette entries land inside +their own region. The range check stays as a cheap guard that now never fires, +rather than as the thing doing the work. + +--- + +### Publishing the sprites, and the bug that would not have errored + +`data.sprites` is a flat map of key to sheet in every generation, and a Gen 3 +entry carries exactly the fields a Gen 4 one needs — `image`, `frames`, +`frameWidth`, `frameHeight`, `walker`, `trueColor`. So the sheets publish under +that name with no Gen 4 spelling, and map objects gain a `sprite` key derived +from their `graphicsId` the way Gen 3 derives `SPRITE_G3_026` from 26. Both the +sprite table and the objects pointing into it come from one +`RomExtractorGen4.spriteKey`, so they cannot drift. + +`trueColor` is true because these are real colours from the cartridge's own +palettes rather than a four-shade Game Boy set the renderer has to map — it is +what makes `SpriteRenderer` use the PNG as it is. + +**Frames stack downward, not across.** `SpriteRenderer` cuts frame *f* with +`newQuad(0, f * frameHeight, ...)`, so a sheet is a **column**. A horizontal +strip is the obvious thing to build and it would have loaded without +complaint — every quad lands inside the image — but every frame after the first +would then be cut out of empty space beside the strip, so an NPC would stand +correctly and **vanish the moment it took a step**. Gen 3's own sheets are 16 +wide and 288 tall for exactly this reason; Gen 4's are the same shape, 32×512 +for a standard NPC and 32×1024 for the player. + +`walker` is `true` only when a member has more than one frame, so the fifteen +single-frame props — the item ball, the cut tree, the boulder — are not handed +to the renderer as a walk cycle with nothing to step through. + +Result: **421 sprite entries, and 3,555 of 3,555 map objects resolve one.** + +--- + +### The dictionary that was only right for two + +Everything above about the overworld archive was written from a reader with two +compensating off-by-four errors, and the compensation held on exactly the +members common enough to make it look correct. This is the account, because the +wrong version was published and because the shape of the mistake is worth more +than the fix. + +**The structure.** A Nitro 3D dictionary is a 4-byte header, an 8-byte block, a +4-byte patricia node per entry, and then a 4-byte sub-header holding +`{ u16 sizeUnit, u16 namesOffset }`. The fixed-size records start immediately +after that sub-header. `namesOffset` is measured **from the sub-header**, and it +points at the **names**, not at the records. + +**The two errors.** The reader derived the records from `namesOffset` measured +from the *dictionary* start, which puts them at `base + 4 + n·unit` instead of +`base + 16 + 4n`. And it then read `texImageParam` from the record's *second* +word instead of its first. Set the two equal and they cancel when + +``` +n·(unit − 4) = 12 → with unit = 8, n = 2 +``` + +**208 of mmodel's 421 texture members have exactly two textures.** On every one +of them the pair of errors produced byte-identical, entirely correct output. On +everything else it read the middle of a name, or a patricia node, or the next +record, and called the result a texture. + +**How it announced itself — and how it didn't.** It never errored. It produced +plausible-looking data: sizes of 8×8 and 1024×512, a format field of 0, blank +names. Each of those was then written into this document as a fact about the +cartridge — "621 empty slots", "181 A3I5", "82 4x4-compressed", "120 of 120 +large slots decode to fully transparent", "nine 32×32 frames per NPC". Every one +of those was a misread, and the transparency measurement in particular is a +caution worth keeping: **decoding a region that is not texture data and finding +it blank is not evidence that the region is blank.** It is what you should +expect. + +The thread that led back to it was not any of those numbers. It was +`pokeball.nsbtx` — 308 bytes, present in the archive, clearly not empty — sitting +under a claim that its art "is not in `mmodel.narc` at all". A 308-byte file +that the reader says holds one 8×8 texture of format 0 is a file the reader is +wrong about. + +**The check that settles it.** For each member, decode every texture entry under +both readings and ask whether *all* of them come out as a format this decoder +handles: + +| textures in member | members | old reading OK | new reading OK | +|---:|---:|---:|---:| +| 1 | 15 | 0 | 15 | +| 2 | 208 | **208** | 208 | +| 3 | 1 | 0 | 1 | +| 4 | 5 | 4 | 5 | +| 7 | 2 | 0 | 2 | +| 12 | 4 | 1 | 4 | +| 13 | 2 | 0 | 2 | +| 16 | **177** | **4** | 177 | +| 17 | 1 | 0 | 1 | +| 24 | 2 | 0 | 2 | +| 32 | 4 | 0 | 4 | +| **total** | **421** | **217** | **421** | + +The `n = 2` row is the whole story: perfect under both, and half the archive. + +**What changed in the output.** 208 members decode byte-identically, 192 +decode differently, 21 appear that did not exist before, and none was lost. +**1,310 frames the old reader never produced** — a standard NPC went from nine +frames to sixteen, the player from nine to thirty-two. + +And the eleven "missing" field objects were never missing. Their texture names +match pokeplatinum's `field_sprites.order` basenames exactly, which is +independent confirmation that the new reading is right rather than merely +different: + +| object | member | decoded | texture name | pokeplatinum basename | +|---|---:|---|---|---| +| `strength_boulder` | 82 | 16×16, 1 frame | `rock` | `rock` | +| `rock_smash` | 83 | 16×16, 1 frame | `breakrock` | `breakrock` | +| `cut_tree` | 84 | 32×32, 1 frame | `tree` | `tree` | +| `pokeball` | 85 | 16×16, 1 frame | `monstarball` | `monstarball` | +| `surf_wake` | 89 | 32×96, 3 frames | `shibuki.1`–`.3` | — | +| `briefcase` | 153 | 32×32, 1 frame | `bag` | `bag` | +| `vent` | 161 | 32×32, 1 frame | `venthole` | `venthole` | +| `regigigas` | 162 | 64×64, 1 frame | `sppoke12` | `sppoke12` | +| `moss_rock` | 168 | 16×16, 1 frame | `moss` | `moss` | +| `ice_rock` | 169 | 16×16, 1 frame | `freezes` | `freezes` | +| `bollard` | 170 | 32×32, 1 frame | `pole` | `pole` | + +`build_model.narc` — the place this document said to look next — turned out to +hold doors, counters, stairs and furniture, and nothing on that list. The +search for where the art lived was the right instinct aimed at the wrong +question: the art was where the names said it was, and the reader was lying +about it. + +**The rule this leaves behind.** A parser that is wrong only for some inputs +will be validated against whichever inputs are commonest, and two-thirds of +this archive's members are its most-common shape. Checking that *every* member +of a corpus parses — not that a sample renders — is what found it, and the +"all entries decodable" test above is cheap enough to keep. + +--- + +### Species pictures, and the sixteen with no male sheet + +`pl_pokegra.narc` is 2,964 members: **494 species slots of six**, in the order +pokeplatinum's own packer writes them (`tools/scripts/make_pl_pokegra.py`, +`for face in back, front / for gender in female, male`): + +``` ++0 back female +1 back male +2 front female +3 front male ++4 normal palette (NCLR) +5 shiny palette (NCLR) +``` + +Positional, with no name table — `pl_pokegra` has no `.order` file for +`Gen4Archives` to match — so that ordering is the only thing saying which +member is which, and it comes from the packer rather than from a guess at the +pattern. + +**Sixteen species have an empty male slot.** The obvious read is "the male +sheet is the sprite, the female sheet is the optional variant". For a +female-only species it is the other way round, and reading only `+3` leaves +Nidoran♀, Nidorina, Nidoqueen, Chansey, Kangaskhan, Jynx, Smoochum, Miltank, +Blissey, Illumise, Latias, Wormadam, Vespiquen, Happiny, Froslass and Cresselia +**with no picture at all** — and not as an error, because an empty member is a +perfectly legal member. `Gen4Pokegra.sheet` falls back and reports which slot +it used. + +A second archive confirms it without being asked to. `height.narc` is 1,976 +one-byte members — four per species, same slot order — and for exactly those +sixteen the two *male* bytes are zero-length while the female ones carry real +offsets. Two unrelated archives agreeing on which slot is empty is better +evidence than either alone. + +**The pixels are encrypted and not tiled.** Every sheet is 20×10 "tiles" at +4bpp, but the NCGR's layout flag says LINEAR BITMAP — rows of the picture, not +8×8 cells in reading order. Read as tiles it still produces *a* picture: the +right pixels in 8×8 blocks shuffled across the frame, which reads as a palette +bug rather than a layout one. `Gen4Graphics.tiles` already reports `bitmap`, +and the pixels go through `Gen4Graphics.decryptSprite` first. + +**20×10 tiles is 160×80, which is two 80×80 frames side by side** — and that is +already the shape `PicAnim` reads, so nothing is restacked. Measured across the +477 species with a male front sheet: 475 have two genuinely different frames, 2 +are identical and 2 have an empty second frame. It is a real two-frame +animation, and the shiny strip is decoded a second time through the shiny +palette rather than reusing the normal one — the cartridge loads **one** palette +for the battler and draws every frame through it, and the Gen 3 version of this +stage had the still pic shiny and the animation not, so a shiny lost its colours +for the two frames it was moving. + +**The filename carries both the id and the name.** Gen 3 names its files by the +species slug alone, which it can because a Gen 3 row is keyed by a `SPECIES_*` +constant. A Gen 4 row is keyed by its integer id and **the names collide** — +`NIDORAN♀` and `NIDORAN♂` slug identically — so a slug-only path would have one +species quietly overwrite the other's picture. `029_nidoran` and `032_nidoran` +are unique by construction and still readable to whoever writes an override. + +**What it writes.** The same field names Gen 3 writes, onto the same species +rows: `spriteFront`, `spriteBack`, `spriteShiny`, `spriteShinyBack`, +`spriteFrontFemale`/`spriteBackFemale` where the female art actually differs, +and `picAnim = { sheet, count, width, height, play, shinySheet }`. `Sprites.path` +reads the first two for every generation and `PicAnim` reads the last, so a Gen +4 spelling of either would mean a generation branch in every screen that draws a +Pokémon. + +| | | +|---|---| +| species with a front **and** a back | **493 of 493** | +| reached through the female slot | 32 (16 species × 2 faces) | +| distinct female variants written | 175 (89 front, 86 back) | +| two-frame strips, normal **and** shiny | 493 | +| species missing a picture | **0** | +| images this stage writes | 2,640 | + +The 322 species whose female member repeats the male art byte for byte get no +second file; writing it would double the stage's output for nothing. + +The `play` pattern — two beats out and back, then a settle — is **this port's, +not the cartridge's**. Platinum times the pair from a per-species animation +script that is not extracted, so the timing is deliberately identical to Gen 3's +rather than invented, so that a Pokémon does not move at one speed in Hoenn and +another in Sinnoh for no reason anyone chose. + +--- + +### The archive with two orderings inside it + +`pl_otherpoke.narc` holds the alternate forms, and it is the one piece of this +cartridge's graphics with no pattern at all. pokeplatinum's own build says so +out loud — `otherpoke_index = {} # otherpoke uses a unique, non-uniform +structure` — and the index is assembled from the per-species `meson.build` +files that populate it rather than inferred from the archive. + +**The shape**, from `make_pl_otherpoke.py` and the `154`/`94` counts the build +passes it: + +``` +0..153 sprites (NCGR), 20×10 tiles at 4bpp — the same shape as pl_pokegra +154..247 palettes (NCLR), 16 colours +248..250 the Substitute doll: back, front, palette +251..252 the in-battle shadows and their palette +``` + +Checked against the cartridge rather than taken on faith: every member in +0–153 *is* an NCGR of that shape and every member in 154–247 *is* a 16-colour +NCLR — 154 and 94, 0 wrong. + +**Two orderings coexist, and neither announces itself.** + +| ordering | species | +|---|---| +| **interleaved** — back, front, back, front, one pair per form | Deoxys, Unown, Burmy, Wormadam, Arceus, Shaymin, Rotom, Giratina | +| **grouped** — *every* back, then *every* front | Castform, Shellos, Gastrodon, Cherrim | + +Castform's four backs are 64–67 and its four fronts are 68–71. Read as +interleaved, sunny Castform's "back" is rainy Castform's back and its "front" is +base Castform's front: four perfectly plausible pictures, all of the wrong +forms, and nothing anywhere reports an error. The palettes split the same way — +Castform and Cherrim group normal-then-shiny while everyone else alternates — so +the same guess also hands sunny Castform snowy's colours. This is why the index +is a table and not arithmetic. + +**Thirty forms have no palette of their own.** Deoxys's three alternate formes +and Unown's twenty-seven letters are drawn through their species' *base* +palette; only the sheets differ. `palette` is nil on those rows and +`Gen4Otherpoke.palettes` falls back to the base form's, returning a `borrowed` +flag so a record can say so rather than imply the form has its own. + +**The egg is not a Pokémon here.** Members 132/133 are the ordinary egg and the +Manaphy egg — a front picture and a normal palette each, no back and no shiny, +because an egg is never sent out and never sparkles. They carry `species = 0`. + +Every `base` form duplicates what `pl_pokegra` already holds for that species. +That is the cartridge's doing, not a mistake: the form-switching code reads one +archive rather than two. + +| | | +|---|---| +| form records written | **78**, across 12 species | +| images | 463 | +| forms borrowing the base palette | 30 (Deoxys ×3, Unown ×27) | +| forms with a two-frame strip | 78 (76 also in shiny — the two eggs have none) | +| forms with a back picture | 76 (the two eggs have none) | +| species rows carrying `.forms` | 12 | + +Plus the three that belong to no species: the Substitute doll's front and back, +and the in-battle shadow sheet. Both are wanted the moment a battle runs — the +doll as soon as something uses Substitute, the shadow under every battler. + +Verified by looking at all 78: Castform's four weathers are the right four +colours, Shellos and Gastrodon are pink and blue the right way round, Cherrim is +closed then open, Arceus's eighteen plates each recolour correctly, Unown's +twenty-eight letters are twenty-eight distinct shapes sharing one palette, and +Rotom's five appliances are five appliances. The four grouped species are +exactly the ones a wrong reading would have scrambled, which is what makes +looking at them the test rather than a formality. + +--- + +### Thirty bands, and the lookup that could not miss + +Two things were settled at once here, and neither would have been settled alone. + +#### What a script id means + +The old note in this file said 2,148 events carried an id past the end of their +map's script list, called them `special`, and stopped: *"what each band means is +not claimed here, because nothing has established it yet."* The cartridge +establishes it. `ScriptContext_LoadAndOffsetID` is a linear scan from the +highest threshold down: + +``` +if id >= 10490 -> scratch-off cards, entry id - 10490 +else if id >= 10450 -> frontier records, entry id - 10450 +... +else if id >= 2000 -> common scripts, entry id - 2000 +else if id >= 1 -> THIS MAP's own file, entry id - 1 +else -> the dummy script +``` + +Thirty bands, each naming a file in `scr_seq.narc` and a text bank. An id in a +band is not special — it is an ordinary entry point in a shared script file. +**1,885 of the 1,920 land inside the file their band names.** The 35 that do +not are all the single value 65535, which is the "no script" sentinel wearing +the top band's clothes. + +The battle bands are worth a line of their own: **3000 and 5000 point at the +same file**, and which one an id came through is what tells the engine singles +from the second half of a double. `Script_GetTrainerID` subtracts the band's own +threshold either way, so the trainer number is identical and the *band* carries +the battle kind. + +**A bug fell out of writing this.** The link stage kept its unresolved ids in one +table keyed by an object's `localId` *and* by a sign's index — both small +integers — so a sign with index 1 silently replaced the object with `localId` 1. +**228 of 2,148 entries, better than one in ten, were being overwritten**, and +nothing counted them because the count was taken before the write. Objects and +signs have a table each now, and all 2,101 survive. + +#### The lookup that could not miss + +Classifying the bands made a second table available: what each object *is*. So +cross-tabulate it against what each object is *drawn as*. That table read: + +> `berry_tree_interactions` — 118 objects, every one drawn as **Team Galactic's +> Mars**. `mystery_gift_deliveryman` — 13 objects, every one drawn as a +> **blooming Lum berry**. `tv_reporter_interviews` — 12 objects, drawn as +> **Teala**, the Pokémon Centre attendant. + +A map object's `graphicsId` **does not index `mmodel.narc`.** It is a key into +`gObjectEventGfxTexturesTable`, a flat list of `{ u32 graphicsID, u32 narcIndex }` +pairs that the field engine searches linearly and that ends at `0xFFFF`. Reading +the id as a member number resolved for every single object — every id in range +names *a* member — and was wrong for all 3,555 of them: + +| id | count | what it is | what it was drawing | +|---:|---:|---|---| +| 85 | 591 | `ROCK_SMASH` | `pokeball` | +| 87 | 331 | `POKEBALL` | `unused_woman_1` | +| 27 | 101 | `TEALA` | `scientist_m` | +| 124 | 77 | `GRUNT_M` | `mira` | +| 84 | 50 | `STRENGTH_BOULDER` | `cut_tree` | +| 86 | 49 | `CUT_TREE` | `unused_woman_0` | + +**Zero of 3,555 objects had the right art**, and the earlier line in this +document — "3,555 of 3,555 map objects resolve one" — was not evidence of +anything. A lookup that cannot miss cannot tell you it is wrong. + +**The table is read from the cartridge**, not transcribed: found by its own +shape, 441 rows at overlay 5 + 0x2BC34, matching pokeplatinum's table row for +row. Only the *names* come from pret. Finding it took three attempts and each +failure is worth keeping: + +1. **First match is the wrong match.** The same overlay holds + `gObjectEventGfxModelsTable`, identical row shape, also starting at id 0, and + it sits *earlier*. A first-match scan returns 18 rows instead of 441 and + answers every id it knows with a member from the wrong archive. This is the + second time in this project a first-match scan locked onto the wrong run — + the type chart did it too. +2. **Longest match is not enough either.** The table maps `gfx1`–`gfx6` to + members 0–5, so reading it twelve bytes late — one column out — *also* sees + `0,1,2,3,4,5` ascending at a stride of eight, and then runs 1,320 rows past + the end because the real terminator is in the other column. +3. **An invariant separates them, but only the right invariant.** The second + column is an `mmodel.narc` member index, so every value must be in range for + that archive; read one column late it is the next row's `graphicsId`, and the + berry ids (4096+) are far out of range. The obvious companion check — that + the ids ascend — *rejects the real table*, because the berry block is spliced + into the middle rather than appended and the run breaks three times. An + invariant that is almost true is worse than none: it throws away the right + answer and keeps a plausible wrong one. + +**An id with no row is not a missing sprite.** 427 objects carry one and they +are objects with no billboard of their own, recorded with a reason rather than +as a failure: + +| reason | objects | what they are | +|---|---:|---| +| `signpost` | 212 | signboards, arrow signs, gym signs — part of the map | +| `berry_soil` | 118 | the planting spot; what grows on it is a berry id above 4095 | +| `runtime_variable` | 62 | `VAR_0`–`VAR_C`, substituted at run time exactly as Gen 2 substitutes its `$F0+` sprites | +| `map_prop` | 35 | the Snowpoint snowball, the Elite Four doors, a readable book, the wall across Rotom's room | + +After the fix: **3,128 resolved, 427 no-billboard-by-design, 0 broken** — and +the cross-tabulation now reads the way it should. + +| band | objects | the art they wear | +|---|---:|---| +| `field_moves` | 688 | `rock_smash` ×590, `strength_boulder` ×49, `cut_tree` ×49 | +| `visible_items` | 329 | `pokeball` ×329 | +| `berry_tree_interactions` | 118 | `berry_soil` ×118 | +| `pokemon_center_2f_common` | 54 | `teala` ×54 | +| `mystery_gift_deliveryman` | 13 | `mystery_gift_deliveryman` ×13 | +| `tv_reporter_interviews` | 12 | `reporter` ×12 | +| `follower_partners` | 4 | `cheryl`, `marley`, `buck` — the three dungeon partners | +| `day_care_common` | 2 | `expert_m`, `expert_f` — the Day Care couple | + +Each of those is two independent tables agreeing. That is the check; one table +looking plausible never was. + +--- + +### What is in the balls, and under the ground + +591 of the map's events are a pickup, and the cache recorded only that they had +a script. The item each one holds is knowable — and the two kinds are knowable +in completely different ways, which is the whole content of this. + +**A visible item carries its item in the script.** Id `7000+n` reaches entry `n` +of `scripts_visible_items`, and every entry is the same three instructions: + +``` +setvarfromvalue 0x8008, +setvarfromvalue 0x8009, +goto +``` + +327 of that file's 328 entries are exactly that; the odd one out is the last. +Every quantity in the game is 1. + +**A hidden item does not.** All 284 entries of `scripts_hidden_items` point at +**one offset** — they are literally the same routine 284 times — and it reads +its item out of script variables the engine fills in beforehand. Those come +from `gHiddenItems` in the ARM9: 257 rows of + +``` +u16 item, u8 quantity, u8 searchRange, u16 padding (always 0), u16 script +``` + +**searched by `script == id - 8000`, not indexed by it.** The script numbers are +derived from flag ids and are not consecutive, so a lookup by row position is +wrong for most of the table. + +Both tables are read from the cartridge. The hidden one is found by shape — +and the padding halfword is what finds it, because two zero bytes in the middle +of every row is not what code or a string table looks like. Measured: the same +address comes back with the item ceiling anywhere from 468 to 65535, so the +shape identifies it and the ceiling is only a bound. + +**Which is exactly where this went wrong once.** Bounding the item id by +`pl_item_data.narc`'s member count looks like the careful choice — use the +cartridge's own number rather than a magic constant — and it is wrong. The data +archive has **446** members and item ids run past it; the real table holds one +of **451**. So the run breaks in the middle, the finder returns **216 rows +instead of 257**, and 41 hidden items vanish with no error anywhere. The item +*name* bank (468) is the id space; a plain large bound is safer still. Third +time in this document that an invariant which is *almost* true did more damage +than no invariant at all. + +| | | +|---|---:| +| item balls resolved | **329 of 329** | +| hidden items resolved | **262 of 262** | +| maps holding at least one | 140 | +| commonest | Rare Candy ×32, Ultra Ball ×30, Star Piece ×20 | + +Spot-checking against the game rather than against the tables: Route 228 comes +out holding PP Max, Protector, Shiny Stone, Shed Shell and TM37, and the water +route W220 holds a Splash Plate. Those are the right items on the right routes. + +--- + +### The trainer field that is not the class + +Same audit, applied to the 417 map objects whose script id is in a battle band. +The trainer number comes out of the band exactly as the cartridge derives it — +`id − 3000 + 1` or `id − 5000 + 1` — and all **417 land inside the 928-entry +trainer table**. That part was right. + +What was not: a trdata header is + +``` +u8 monDataType, u8 trainerType, u8 sprite, u8 partySize, +u16 items[4], u32 aiMask, u32 battleType +``` + +and the obvious reading is that `sprite` says which trainer to draw. Measured +across all 928 trainers, **`sprite` is zero for every single one.** The class is +`trainerType` — 102 distinct values in 0–102, against a 105-entry class list — +and the game agrees: `TRDATA_CLASS` reads `trainerType`. Cross-checking the +wrong field made every trainer in Sinnoh a Player (Male). + +**The check that says the join is right** is again two tables that know nothing +about each other. Of the 63 trainer classes that appear on a map, **60 are drawn +as exactly one overworld sprite**, and the three that are not are pair classes +fought by two visible people: + +| class | drawn as | +|---|---| +| `belle_and_pa` | `rancher` ×2, `cowgirl` ×2 | +| `double_team` | `ace_trainer_f` ×3, `ace_trainer_m` ×3 | +| `young_couple` | `pokemon_breeder_f`, `beauty`, `pokemon_breeder_m`, `guitarist` | + +A wrong join does not produce a 60-out-of-63 one-to-one mapping with its +exceptions all being couples. + +Map objects now carry `trainer = { id, class, className, name, partySize, +battler }`. `battler` is which half of a double this object is, and it comes +from the **band** rather than from the number, because 3000 and 5000 reach the +same file and only the band tells them apart. Class names come from message bank +**619**, found by looking rather than assumed: it has 105 entries and answers +"Youngster" and "Lass" at 2 and 3, exactly where the class list puts them. + +**One thing that looks broken and is not.** Sixteen class names come out as +`₧₦ Trainer`, `₧₦ Breeder`, `₧₦ Ranger`. Those are character codes `0x01E0` and +`0x01E1`, and rendering the two glyphs out of the message font settles what they +are: they draw **`Pĸ`** and **`Mɴ`**, the double-width PK/MN ligature pair, 12 +pixels each against 6 for an ordinary letter. The cache is correct and the font +draws the real thing; `₧₦` is only the ASCII stand-in, and it is deliberately +*one character each* so the charmap still round-trips. Do not "fix" it into +`Pokémon` — that changes every measured line width in the game for a difference +no player sees. + +--- + +### The height field, and the two checks that make it believable + +A Gen 3 map is flat and its one elevation byte per tile is the whole story. A +Gen 4 map is a mesh: a bridge crosses a path and the two share an (x, z), a +slope rises between two tiles that are both "ground". The `BDHC` block in each +land chunk is the cartridge's own answer, and it is small, self-checking and +independent of the mesh — so it comes out now while the NSBMD does not. + +**The layout**, read strictly in file order from `BDHC_LoadHeader` and +`BDHC_PrepareBuffers`: + +``` +"BDHC" 4 bytes +u16 points, normals, constants, plates, strips, access +point { fx32 x, z } 8 bytes each +normal { fx32 x, y, z } 12 bytes each +constant fx32 4 bytes each +plate { u16 first, second, normal, constant } 8 bytes each +strip { fx32 scanline, u16 count, u16 start } 8 bytes each +accessList u16 2 bytes each +``` + +**Check one — the identity test**, because "BDHC" is four bytes and four bytes +is a weak promise: `16 + 8p + 12n + 4c + 8P + 8s + 2a` must equal the declared +block size exactly. **666 of 666 chunks pass**, none empty, none failing. A +layout that is right for most files and wrong for some is the failure mode this +catches, and it catches it per chunk rather than on average. + +**Check two — reproduce the cartridge's index and compare it to brute force.** +The game does not test every plate: it binary-searches the strips by scanline, +then tests only the plates in that strip's slice of the access list. That search +is transcribed including its off-by-one shape (on the "go higher" branch it +answers `mid + 1`), because reproducing the *search* rather than the intent is +the point — if the cartridge picks strip *n*, an extractor that picks *n−1* +disagrees with the game about where the ground is. Then: sample 56,832 points +and compare the strip-indexed answer against testing all 8,974 plates. +**56,830 agree exactly.** The two that differ are points where brute force finds +an extra coincident plate the strip does not name, and the height is the same +either way. That one test exercises the header, all six blocks, the fixed-point +conversion, the plate geometry, the plane equation, the binary search and the +access list at once. + +**The units, which the data states rather than the docs.** `fx32` is 1:19:12 — +divide by 4096. A land chunk is 32×32 tiles, and a flat chunk's single plate +spans **−256..256**, so a tile is **16 world units and the chunk is centred on +the origin, not cornered at it**. Measured: 49 of the 54 single-plate chunks +cover all 1,024 of their tiles under exactly that mapping. Getting the origin +wrong first put coverage at 17%. + +**And a negative result worth as much as the positives: this is not a +walkability map.** Tiles the permission grid calls void are covered by a plate +**75.6%** of the time; real ground, **72.3%**. That is no correlation at all, +and the reason is in the engine: where no plate covers a point, +`CalculateObjectHeight` returns FALSE and the height is *left alone*. Plates are +the exceptions, not the surface. The permission grid and the height field answer +different questions and neither can be derived from the other — which also means +this was never going to be a cross-check, and saying so is better than quietly +reporting the 72% as if it confirmed something. + +| | | +|---|---:| +| chunks with a BDHC | **666 of 666** | +| plates | 8,974 | +| points / normals / constants | 15,464 / 1,275 / 2,227 | +| strips / access-list entries | 3,569 / 24,838 | +| normals that are exactly (0,1,0) | 666 — one flat normal per chunk | +| parse failures | **0** | + +Heights come out quantised to multiples of 8 — 8, 16, 32, 48, 64, 80, 96, 112 — +which is what a tile-based game's elevation steps look like, and is the cheapest +sanity check that the fixed-point conversion is right. + +### The crash that proved the cache was reading Red's world + +The first New Game on an imported Platinum died in `MapLoader.lua:66` with +`unknown map: REDS_HOUSE_2F`. Nothing in the Gen 4 extractor mentions Red's +house, and that is exactly the point. + +`Game:bootConfig()` reads `self.data.field.boot` to learn where a new game +starts. The Gen 4 extractor wrote 28 tables and `field` was not one of them. +`Data.lua`'s overlay is **additive**: a module the current cache does not +provide is not an error, it is inherited from the classic data that ships with +the engine. So the boot config resolved, cleanly and silently, to Red's — and +the first thing the engine did with a Platinum cache was ask a Platinum map +registry for a Kanto map name. + +This was predicted in writing and still happened. `Data.lua`'s own comment on +the module says `field` is where `boot.startMap` and `boot.screens` live, and +notes that this is how NEW GAME on Emerald once opened Red's intro in Red's +house. `field` had been unblocked from `CLASSIC_ONLY` when Gen 3 learned to +write one; Gen 4 inherited the permission without inheriting the stage. **A +documented trap is not a fixed trap.** + +#### Two tables, and the one that anchors the other + +Platinum keeps the answer in two ARM9 tables that have to agree. + +*Location pairs* — 20-byte records of `{ mapId, x, z, facing, warpId }`, +holding the new-game start and the post-blackout respawn. Searching for the +shape alone found three candidates, then two after the obvious junk was +dropped, and a shape search that returns two answers has told you nothing. + +*`sSpawnLocations`* — 16-byte rows mapping a fly/heal destination to its map +and coordinates. Row 0 is Twinleaf Town, which is also where a new game starts +and where the player wakes after a blackout. + +So the pairs table is not identified by its own shape at all. It is identified +by **agreeing with row 0 of the other table**: the respawn record must name the +same fly map and the same coordinates that spawn row 0 does. One candidate +survives, and it survives for a reason that is about the cartridge rather than +about the search. + +#### Two almost-true rules, one kept and one thrown out + +Finding `sSpawnLocations` by longest run of plausible rows landed on a bogus +26-row table at `arm9+0xEA548`. The rule that killed it is a real invariant: a +blackout position has to fall **inside the map definition it names** — every +respawn in this game is indoors (a Pokémon Centre or a bedroom), so each row's +`(x, z)` must sit within its own map's bounds. That is a property of what the +data *means*, and it holds for all 20 real rows. + +The rule that had to be thrown out was mine. "First-arrival ids increase down +the table" looked true, matched the first sixteen rows, and truncated the table +to sixteen — row 16's value is 5, after row 15's 17. It was a pattern in the +data, not a constraint on it, and it cost four heal locations. + +That is the fourth time this session a nearly-true invariant has been the bug +(the type chart's first terminator, ascending object-graphics ids, the hidden- +item ceiling taken from an archive's size, and now this). The distinction that +matters each time: an invariant derived from *what the field is for* holds; one +derived from *what the values happen to look like* does not. + +#### What the stage writes + +``` +start T01R0202 (4,6) facing up heal locations 20 +boot.startMap = T01R0202 +boot.startFacing = up +boot.lastHeal = T01R0201 (8,8) +boot.screens.newGame = false +source = ROM:Platinum (locations arm9+0xEA12C, spawns arm9+0xE97B4) +`maps` contains T01R0202 : YES +heal rows with both maps named : 20 of 20 +``` + +`src/import/Gen4Field.lua` holds the reader; `extractField` is the 29th stage. +The two checks that make the result believable are the cross-table anchor above +and the fact that every map either table names is a map the `maps` stage +independently produced — 20 of 20, with no name invented by the field reader +itself. + +#### The half of the fix that is not a fix + +An **existing** Platinum cache has no `field.lua` and nothing would make it +re-import: the boot would keep resolving to Red's forever, and the only symptom +is a crash that names a Kanto map. So `"data/generated/field.lua"` is now in +`REQUIRED_FILES_GEN4` in `RomImporter`. It is not one of `Data.lua`'s required +fifteen — it is listed because a *missing* one is invisible rather than fatal, +which is the property that makes it dangerous. Naming it turns an old cache +into a reported gap and an offer to re-import. + +The same list had a second hole found on the way in: `requiredFiles` had no Gen +4 case at all and fell through to the Gen 1 list, so a *successful* Platinum +import would report five phantom missing files for ever after. + +**Platinum must be re-imported for this to take effect.** + +### The title, the card and the menu — and the eighteen tiles that decide them + +The second half of the crash report was that nothing appeared: no Platinum +intro, no main menu, no menu graphics behind NEW GAME, CONTINUE or OPTION. The +art for all of it was extracted months of work ago. What did not exist was +anything that drew it — and, underneath that, anything that knew what shape a +Gen 4 window *is*. + +#### Boot screens are named data, and Gen 4 named none of them + +`Game:init` does not decide which intro or title to show. It reads +`field.boot.screens.splash` and `.title`, and `Data.lua` overrides each one only +while it still equals the default — Gen 2 takes `Gen2Intro`, Gen 3 takes +`Gen3Title`, and a generation that names nothing keeps **Red's**. That is the +same additive-overlay failure as the start map, one field along, and the fix is +the same shape: the field stage now writes + +``` +screens = { splash = "Gen4Intro", title = "Gen4Title", newGame = false } +``` + +Every override in `seedDefaults` is guarded by `== BOOT_DEFAULTS.screens.x`, so +a value stated here is left alone by all of them. `newGame = false` stays +deliberate: `OakSpeech` replays a professor's intro out of text Platinum does +not have, and Platinum's own opening is Rowan's, which is a script on the intro +map rather than a screen. + +#### The frame is eighteen tiles and the arrangement is not guessable + +Everything a menu draws goes through `Font.drawBox`, and `Font.drawBox` wants a +frame. Platinum has two kinds in `pl_winframe`: + +* **`standard_system` / `standard_field`** — nine tiles, and a genuine + nine-slice. `DrawStandardWindowFrame` places `+0..+8` row-major with `+4` + never drawn, because the interior is the window's own text bitmap. That is + exactly the engine's existing nine-slice contract, so these needed **no new + drawing code at all** — only the sheet composed three tiles across instead of + eight, which is the same pixels at a different stride. +* **`message_box_00` … `19`** — eighteen tiles each, with their own palette + each, and **not** a 3×3. + +Eighteen reads equally well two ways. As 3 wide by 6 tall it is two stacked +frames; as 6 wide by 3 tall it is one frame with fat caps. **Both draw a +rectangle.** Rendered at three across, the result is a plausible tall box with a +brown smear down its middle — the kind of output that gets accepted because it +is not obviously broken. + +It is 6×3, and the cartridge says so outright rather than leaving it to be +inferred from what the tiles look like. `DrawMessageBoxFrame` places them +against a window at `(x, y)` sized `(width, height)`: + +``` +row y-1 +0 at x-2 +1 at x-1 +2 across width +3 +4 +5 at x+width..+2 +rows y..y+h-1 +6 at x-2 +7 at x-1 [window content] +9 +10 +11 +row y+height +12 +13 +14 across width +15 +16 +17 +``` + +One rule for all three rows: row bases 0, 6, 12, and within a row the columns +are `+0, +1, +2` repeated, `+3, +4, +5`. The caps are **unequal** — two tiles +left, three right — which is why it reaches past both sides of the box and why +no symmetric reading of it is right. + +`+8`, the middle row's repeating tile, is the one member of the eighteen the +cartridge never places, because the window's text bitmap covers exactly that +region. This engine draws text straight onto the box, so it *does* place that +tile and gets the same picture. It is also the only solid tile in the sheet, +which makes it the honest place to read the interior colour from for a box too +narrow to have a middle. + +A first pass here also produced a *wrong* invariant worth recording: measuring +whether the four "middle" tiles of each frame were identical, 19 of 20 said no — +which looked like proof the frame was **not** a nine-slice. It was proof the +grid was six wide, not three. **A test that rejects your hypothesis has not +told you which of your assumptions it rejected.** + +Both records go into `data.font`, which is the table `Font.lua` already reads: + +``` +frames = { image, count = 2, cell = 24, tile = 8 } +dialogueFrame = { image, count = 20, tiles = 18, layout = "gen4", fill } +``` + +`Font.lua` gained one guarded branch. Its existing dialogue reader is FireRed's +— five-tile rows mirrored back down — so a Gen 4 strip run through it would draw +a box that is merely wrong instead of failing; `layout = "gen4"` is what stops +that, and the quad builder now indexes one row per frame, which for FireRed's +single-frame strip is bit-for-bit what it built before. Nothing on the Gen 1, 2 +or 3 path changes. **The payoff is that every box in the game is Platinum's in +one step** — menus, choice boxes and dialogue — instead of twenty screens each +drawing their own slightly different rectangle. + +#### The menu is the cartridge's, including its numbers and its words + +`main_menu.c` states the layout outright and it is transcribed rather than +eyeballed: options are windows at **x = 3, width 26 tiles**, the first at +**y = 1**, each next one `height + 2` below ("Add 2 to account for the window +border"), `height` being `TEXT_LINES_TILES(n)` = `n × 2`. CONTINUE is five lines +because it carries the save summary *inside its own window* rather than opening +a panel; everything else is one. Labels sit 32 px in, values right-aligned to +the same 32 px — one constant, `CONTINUE_WINDOW_MARGIN`, used on both sides. + +The words come from **message bank 550**, read out of this cartridge and +confirmed entry by entry: 21 strings, 0 = CONTINUE, 1 = NEW GAME, 12 = PLAYER, +13 = TIME, 14 = POKéDEX, 15 = BADGES — exactly the order +`sOptions` and `sContinueOptionStringsIDs` expect. The NEW GAME warning +("there is already another saved game file") is bank 14 entry 5. + +Two honest departures, both marked in the data rather than buried in the screen: + +* **Six of Platinum's eight rows are not offered** — Mystery Gift, the Ranger + link, GBA migration, both Wii rows and the Wi-Fi settings. None of them can do + anything here, and a row that does nothing is worse than a row that is absent. +* **OPTION is this engine's, not the cartridge's.** On the DS, text speed and + the message frame live in the in-game START menu, so a player who has not + started a game cannot reach them. Every other title this engine boots offers + the row; the record says `fromCartridge = false` so it cannot later be + mistaken for extracted data. EXIT GAME is the launcher's, on the same terms. + +The dex row is **skipped, not blanked**, the way `RenderContinueOption` +`continue`s past it — so the summary is four rows or three, never four with a +hole. + +#### Where the artwork actually is + +The title archive's sheets are 256×192 or 256×256 and mostly empty. Drawn at +0,0 on the sheet size, the logo lands forty rows low and "VERSION" is cut off at +the screen seam — which looks like a layout choice rather than a measurement +nobody took. So the menus stage measures each picture's content box off the +composed pixels: + +| | content box | +|---|---| +| `logo` | x 15, y 27, 225 × 122 | +| `copyright` | x 64, y 64, 128 × 56 | +| `gf_presents` | x 5, y 181, 151 × 9 | +| `top_screen_border` | the full 256 × 192 | + +Two different kinds of background occur here and only one of them is +transparency: the logo is on a transparent sheet, the copyright is white text on +an **opaque black** 256×256 sheet where alpha finds everything. Treating "fully +transparent **or** exactly the top-left pixel's colour" as background covers +both without the measurer needing to know which kind it has. + +`Gen4Title` then splits the screen by what has to fit in each half — the logo's +height above, the copyright block's below, the fourteen spare rows shared — and +centres each in its band. Nothing in the file is a magic number. + +#### What the three screens are, and what they are not + +* **`Gen4Intro`** — the GAME FREAK card, faded up and away, skippable from the + first frame. The cartridge's opening is that card, then Giratina turning + through a portal, then the title; the middle one is `giratina.nsbmd`, a 3D + model, and there is no 3D path yet. So the card plays and **nothing invents a + substitute for the portal.** When the mesh pipeline lands it belongs exactly + here. +* **`Gen4Title`** — `top_screen_border` with the logo faded up on it, the + copyright block on the dark strip below. A DS title is two panels and this + engine draws one surface, so the two are composed into one 256×192 screen + rather than the copyright being dropped; when the second screen exists, the + file splits along the seam the two borders already mark. "PRESS START" is this + port's — Platinum simply waits, and a window with no hardware START button has + to say which key opens the menu. +* **`Gen4MainMenu`** — the rows above, drawn in Platinum's own window frame and + its own font, with the save summary inside the CONTINUE window. + +One detail that would otherwise have read as a font bug: the menu draws its text +at **white, not black**. Platinum's font page is pre-tinted — the sheet bakes the +letter dark and its shadow light — so multiplying it by black, which is what +every Game Boy screen in this engine does because its glyphs are a mask, would +paint the shadow black too and thicken every letter. + +`gen4_menus.lua` joins `field.lua` in `REQUIRED_FILES_GEN4`, for the same reason +that one is there: a cache with the boot screens and without the record boots to +a black title with nothing on it, which looks like a broken screen rather than a +missing table. + +### OPTIONS, and the row that would have stored the opposite of what you picked + +`boot.screens.options` was still the default, so OPTION on the new main menu +opened the **Game Boy** screen: Gen 1's rows, and a FRAME row that cannot reach +Platinum's twenty message boxes. The record now names `Gen4Options`. + +The vocabulary is **message bank 220**, read out of this cartridge and checked +entry by entry — 53 strings: 0 = OPTIONS, 3–8 the six row names, 10–21 their +values, 22–41 the twenty frame names spelled out one per entry, 43–48 a +one-line description per row, 52 = "Return to the game." + +**The reason this is not the Gen 3 screen with different words** is one line of +`constants/game_options.h`: + +``` +OPTIONS_SOUND_MODE_STEREO = 0, +OPTIONS_SOUND_MODE_MONO +``` + +Platinum lists **STEREO first**. Emerald lists MONO first. Reusing Gen 3's row +would have put the right two words on screen in the wrong order, stored the +opposite of what the player chose, and looked completely correct doing it — +there is no symptom until someone notices the sound did not change. BUTTON MODE +differs the same way: Emerald's middle setting is LR, Platinum's is START = X. +Every row's values are the enum's order, not a reading of what looks natural. + +Which rows actually bite, stated rather than implied: + +| row | what it does here | +|---|---| +| TEXT SPEED / BATTLE SCENE / BATTLE STYLE / SOUND | map onto options this port already honours | +| **FRAME** | **live** — picks one of Platinum's twenty message boxes, which `Font.drawBox` now reads | +| BUTTON MODE | stores all three; only `L = A` bites, because `START = X` is a DS mapping with no counterpart here | + +FRAME is the interesting one: it is inert on the Game Boy screens and real +here, because the dialogue-frame work above gave it twenty things to choose +between. It writes `options.gen3Frame` — the field `Font.lua`'s frame chooser +reads on every generation. The name is Gen 3's because that is where the +chooser was written; renaming it would mean editing `Font.lua` for nothing, and +a save carried between cartridges keeping one frame choice is what a player +would expect anyway. + +**CONFIRM is extracted and deliberately not offered.** Platinum stages the +changes and applies them when you pick CONFIRM; this engine applies each change +as it is made, the way every other OPTION screen in it does. A CONFIRM row here +would either do nothing or promise a staging model that does not exist. The +strings are kept for the day it does. + +**The engine's own rows follow the cartridge's**, through the `ui.options.rows` +hook rather than straight from `buildRows` — the same argument, and the same +bug-avoidance, as the Gen 3 screen. Platinum has no volume sliders, no video +mode, no key bindings and no mod manager; a player on Platinum who could not +reach those would have lost every setting the other versions have because a DS +had no menu for them. Going through the hook is what stops a mod's rows being +present on every version but this one. + +### The opening, read out of the cartridge — and four things the log said + +Platinum booted, the menus drew, and a New Game opened straight into the +bedroom with no intro at all. Five separate faults, and the running game's own +log named four of them outright. + +#### `player sheet: SPRITE_RED -> ninja_boy` + +One line, and the most visible bug in the build. With no `field.playerSprites` +the avatar falls back to a **Game Boy sprite id**, which a Gen 4 cache then +resolves through its own sprite table to whatever happens to sit there. +`ninja_boy` is mmodel member 1; the player is member 0. So the hero walked +Sinnoh as a passer-by, and nothing anywhere reported an error — the lookup +succeeded, at the wrong thing. + +The field stage now writes both shapes `Player:refreshForm` reads — +`playerSprites` for the default pair and `playerForms[gender]` for the override +the intro's answer selects — for walking, cycling, surfing, fishing and the +Poké Ball pose, for both characters. Found **by name**: `player_m` and +`player_f` are what the object-graphics table calls them, and a name cannot be +off by one the way an id can, which is exactly the mistake being fixed. The +result cross-checks itself: boy 90 / girl 91, bikes 92 / 93, surf 159 / 160 — +adjacent pairs throughout, which is what a hero pair looks like and what a +mis-resolved lookup does not. + +#### The opening: a television, then Rowan + +`boot.screens.newGame` was `false`, and that was the whole of "New Game has no +intro". OakSpeech replays a professor's intro out of text Platinum does not +have, so there was nothing to put there. + +There is now. Two archives, **neither of which has a name table** — ten members +and fifty, none named — so the generic screen planner cannot touch either and +every member index is arithmetic. Arithmetic is the thing this project has been +burned by most, so none of it is inferred: all of it is transcribed from the +cartridge's own loaders (`RowanIntroTv_InitGraphics`, +`RowanIntro_LoadInitialTilemaps` / `_LoadLayer3Tilemap` / `_LoadTilemap`), and +every composed picture is then looked at. + +Which caught the television immediately. Its palette is **two loads, and the +picture is in the second one**: + +``` +Graphics_LoadPalette(..., 6, PAL_LOAD_MAIN_BG, 0, 0, ...) +Graphics_LoadPaletteWithSrcOffset(..., 9, ..., 0x20*2, 0x20*2, 0x20*14, ...) +``` + +Member 6 fills the whole background palette; member 9 then overwrites colours +32 upwards, and the broadcast is drawn almost entirely out of those. Composed +with member 6 alone it is a black rectangle with coloured noise in it — which +reads as a decoder bug rather than as a half-loaded palette, and would have +sent the next hour into the tile reader. With both loads applied it is a +Sinnoh town under a blue sky, which is what the news report shows. + +The set is three background layers, back to front: the broadcast (tiles 8, +tilemap 7, 256-colour), a scanline overlay (2 / 5), and the bezel (1 / 4). +`BG_LAYER_MAIN_2` is initialised and then cleared — it is the CRT band the app +scrolls at run time, not a picture, so there is nothing to extract for it. + +Rowan's scene is one tile sheet and five tilemaps for the backdrop, and **ten +figures that are all full-screen pictures rather than sprites**: each is a +(tiles, palette) pair laid out with the *same* tilemap, member 23, which is why +the cartridge's table is a list of pairs and nothing else. They come out as +Rowan, four poses of Lucas, four of Dawn, and Barry — everything the intro +needs, including both characters for the boy-or-girl question. + +A figure's palette is loaded into row 7 or 8 and the app then rewrites its +tilemap's cells to point there. Extracting one picture at a time there is no +second layer to keep out of the way, so the sixteen colours are replicated +across every row instead: whatever palette index a cell carries, it lands on +that figure's own colours. Same picture the hardware draws, no cell rewriting. + +The script is **bank 389**, 45 entries, checked against every id in +pokeplatinum's own text file — and the page breaks were already in it. Gen 4 +writes `0x25BC` for "wait, then clear" and `0x25BD` for "wait, then scroll", +and the decoder renders them as CR and FF, so a page is a split on those +characters rather than something the screen has to count. The television's line +is **bank 607**, one entry. + +`Gen4RowanIntro` plays the main line: the greeting, who you are, your name, +your friend's name, the send-off. **CONTROL INFO and ADVENTURE INFO are +extracted and not offered** — they are tutorials about a touch screen and a ++Control Pad this port does not have, and a lecture describing hardware the +player is not holding is worse than one they never saw. Both are in +`gen4_intro` so a mod can put them back. + +Gender writes `save.player.gender` **and calls `refreshForm`**, because New Game +pushes the overworld first and this screen on top of it: the Player object +already exists and has already chosen its sheets, so writing the save alone +changes what the *next* one would wear. That is the bug that sent a player who +chose the girl out of the bedroom as the boy on Gen 3, and it is the same bug +here. + +#### The black screen was the studio card + +"Developed by GAME FREAK inc." occupies rows 181–189 of a 256×192 sheet — the +bottom-left corner — because on the hardware it is the **bottom** screen and +the credit sits under the picture on the top one. Drawn as it stands on one +screen it is a black field with a line of type in the corner, which reads as a +screen that failed to load. It is centred now, using the content box the menus +stage already measures. + +#### "The tiles are too small" + +The main menu's geometry was the cartridge's and still looked wrong, for a +reason the cartridge never has to think about: its menu is **eight** rows, six +of them link features this port does not offer, and CONTINUE's window alone is +ten tiles. With no save and no link rows the stack is three short windows, +which at the cartridge's own `y = 1` sit in the top third of the screen and +leave the rest empty. The window sizes and spacing are still `main_menu.c`'s; +only where the block starts is this port's. + +#### OPTION in the START menu was still Kanto's + +The boot record named `Gen4Options`, but **only the main menu reads that +record** — every other call site in the engine hardcodes the Game Boy id, which +is the same gap `GEN3_ALIASES` exists to close. There is now a `GEN4_ALIASES` +beside it, and it has exactly one entry, because Platinum's bag, party screen, +summary pages and trainer card are all *extracted* and none of them has a +screen to draw with yet. Those ids still open the Game Boy ones. Adding an +alias for a screen nobody has written would open a module that is not there. + +#### And one thing that was not a bug + +"I spawn in a weird area that isn't my bedroom." `T01R0202` **is** the bedroom, +and the map loader put the player on the right tile of it. What is wrong is +that it does not look like one: Gen 4's world is 3D and has no 2D tileset, so +every map is drawn with the synthesised stand-in — one flat colour per terrain +class — and a bedroom floor and a meadow are both "walkable", so both are +green. The map meshes are the fix, and they are still the largest thing +outstanding. + +### The mesh reader, and the one number that could not be argued with + +"Make sure the starter select box animation and 3D models working" turned out +to be the same job as the map meshes, Giratina on the title, and every field +effect: **Platinum's starter selection is real 3D.** `choose_starter_app.c` +loads six NSBMD models and four NSBCA animations out of +`/graphic/ev_pokeselect.narc` — the briefcase with its opening animation, three +Poké Balls each with their own, and a ground plane — and renders them with the +DS geometry engine. Nothing in this engine could read an NSBMD at all. + +`Gen4Models` reads NSBTX, the *texture* archives the overworld sprites turned +out to be, and stops there on the grounds that the sprite question did not need +a mesh pipeline. It did not. Everything else does. + +#### Why a display-list decoder cannot be checked by looking at it + +A decoder that is subtly wrong does not fail. It emits *some* vertices and +*some* triangles, and the result is a mesh — crumpled, inside out, or missing a +limb, but a mesh. A wrong entry in the command-length table desynchronises the +stream and it keeps decoding, into geometry-shaped garbage. There is no error +to catch and no exception to report. + +The cartridge settles it. Every model header states **`numVertex`, +`numPolygon`, `numTriangle` and `numQuad` outright**, and not one of those +numbers is used to decode anything — they are the file saying what a correct +decoder should have found. So the test is exact equality on all four, for every +model in the cartridge: + +``` +214 archives scanned, 24 hold models +1,030 models decoded -- 1,030 exact, 0 mismatched +``` + +Including `build_model.narc`'s **590 buildings**, `mmodel`'s 24, `fldeff`'s 145 +field effects, `titledemo`'s 3 (Giratina), and `ev_pokeselect`'s 6. A wrong +command length, a missed primitive type, an off-by-one in the strip winding — +any of them moves at least one of those four numbers on at least one model out +of a thousand, and none of them moved. + +This is the best verification this project has had, and it is the same shape as +every other one that worked: **two tables that cannot borrow from each other.** + +#### What the shape record actually is + +Sixteen bytes, of which the two that matter are the display list's offset — +relative to the *record*, not the file — and its size. Both were measured, and +each confirms the other. On `pmsel_bg` the offset lands exactly on a +`40 22 21 24` packet (BEGIN_VTXS, TEXCOORD, NORMAL, VTX_10, which is how every +display list in this cartridge opens), and record + offset + size lands exactly +on the first byte of TEX0. Neither of those is likely by accident and both had +to hold. + +#### What comes out + +| | | +|---|---:| +| `psel_all` (the briefcase) | 12 bones, 15 materials, 29 shapes, 2,430 vertices | +| `psel_mb_a` (a Poké Ball) | 4 bones, 3 materials, 3 shapes, 328 vertices | +| `pmsel_bg` (the ground) | 1 bone, 1 material, 24 quads | + +Rendered offline from the decoded triangles, the briefcase has its handle and +clasps, the ball has its open-lid flap, and the ground is a flat quad grid. The +material names are the cartridge's own — `trank_a` … `trank_i` for the trunk, +`op_mb` for the ball, `op_grand` for the ground — which is what pairs each +shape with a texture out of the same file's TEX0. + +The strip winding is worth naming as a trap that this check caught rather than +a detail: a triangle strip alternates its winding, and getting that wrong turns +every other face inside out. It is invisible on a wireframe and invisible on a +flat fill; it shows up the moment the model is lit, by which time the decoder +has been trusted for weeks. The quad count would have been unaffected. The +triangle count would not. + +#### What is NOT done, stated plainly + +The reader is done and the **renderer is not**. Reading geometry and drawing it +are separate problems, and this engine has no 3D path of its own yet: + +* **Somewhere to put the field animations.** All five formats decode their + values now and all 378 check out, but the 98 texture scrolls and 72 flipbooks + in `bm_anime` are laid over **map meshes that are not built**. They are + extracted, verified and carried; nothing draws them until the terrain and + building geometry exists. +* **The 2D side.** 796 `NANR` cell animations over 810 `NCER` cell banks are + counted but not decoded — those are the sprite and UI animations, a separate + format from the five 3D ones. +* **Seeing it run.** The starter select has still never been on a screen. The + placement is the cartridge's own now rather than mine, which removes the part + most likely to be wrong, but the camera distance and pitch are chosen rather + than derived and that is what a single screenshot settles fastest. +* **The terrain meshes** are not in the sweep above: they are embedded in + `land_data`'s chunk records rather than being narc members, so they are found + a different way. The reader does not care — an NSBMD is an NSBMD — but the + 1,030 is honest about what it covers. + +### Two more places the pairing is stored somewhere other than where it is used + +The mesh reader decoded geometry. Getting a *picture* out of it needed two more +bindings, and both of them are recorded backwards from where you would look — +which is the same trap as `pl_winframe`'s palettes, one layer down. + +#### Which material a shape is drawn with + +A model's shapes and its materials are two independent dictionaries and neither +says how they pair. The pairing is in a third place: a little bytecode, the +**SBC**, that the hardware walks to draw the model. `0x04 n` binds material n, +`0x05 n` draws shape n, and a shape takes whatever material was bound last. + +Its operand widths were derived from the streams rather than assumed — +`NODEDESC` takes three, plus one for each of the two low flag bits, which is +why `26 00 00 00 00` is four operands and `06 00 00 00` is three — and then +held to an invariant: **no shape drawn twice, every material index in range, no +unknown opcode.** A desynchronised walk breaks all three at once. + +That check found two real opcodes the first table was missing (`0x07` +billboard, `0x0D` projection map) and one variable-width one (`0x09`, skinning, +whose width has to be read from its own operands). It also found `kurotama` in +`demo_tengan_gra`, which draws 38 of its 40 shapes — each exactly once, with +materials 0–6 out of seven, no unknown opcode. That is not what a desync looks +like; a cutscene model carrying two shapes its commands never reach is ordinary +content. So "every shape is drawn" was dropped as a clause and is **counted** +instead — a relaxation stated here rather than quietly made. + +#### Which texture a material wears + +Also backwards. A texture entry does not say "material n uses me" by sitting at +index n; it carries a list of the material indices that use it, packed as bytes +just before the material records, at an offset measured from the **section** +rather than from the dictionary. + +And the order is not the same. On the briefcase, texture 1 (`op_mb`) belongs to +material 2 and texture 2 (`op_mb_a`) to material 1; `trank_a` and `trank_a_` +are swapped the same way. Walking the two dictionaries in step — the obvious +reading, and correct on every *simple* model in the cartridge — gives those +four the wrong picture and reports nothing. + +#### The invariant that made "untextured" an answer instead of an excuse + +Requiring every material to have a texture failed on **122 of 1,030** models — +always one or two materials on an otherwise complete model, never a whole one, +which is not the shape a wrong base address produces. A DS polygon can have no +texture at all, and this cartridge has plenty that do not. + +What turns that from an excuse into a fact is the material record's own +**texture-scale field**: zero on exactly the materials the name lists leave +unbound, non-zero on exactly the ones they bind. Two records that know nothing +about each other, agreeing on every material in the cartridge. So the invariant +is not "everything is textured" but **"a material is bound to a texture if and +only if it declares one"**, and that holds everywhere. + +Final state of the reader, all three checks at once: + +``` +1,030 models -- 1,030 exact, 0 mismatched + geometry: vertices, triangles, quads, polygons == the header's own counts + draws: no shape twice, materials in range, no unknown opcode + binding: bound <-> declares a texture scale, all indices in range +``` + +`Gen4Models.parse` gained a section argument for this. It read `u32(data, 16)` +to find TEX0, which is right for a texture archive and **wrong for a model +file**, where that word is the MODEL section — a parse that succeeds and builds +a texture table out of geometry. This format's recurring hazard, avoided by +passing the offset rather than re-deriving it. + +Rendered end to end, the briefcase comes out with its leather straps, buckles +and handle, and the Poké Ball with its shadow: geometry from the display lists, +textures resolved through the name lists, colours from each material's own +palette. + +### The animations, all five formats of them + +A Nitro model does not animate itself. Everything that moves in Platinum's 3D +is a **separate file** that names what it drives. There are five formats and +the cartridge holds 378 of them: + +| | | | | +|---|---|---:|---| +| `BCA0` | `JNT0` | 181 | joints move, rotate, scale | +| `BTA0` | `SRT0` | 98 | a texture scrolls or spins | +| `BTP0` | `PAT0` | 72 | the texture itself is swapped per frame | +| `BMA0` | `MAT0` | 24 | a material's colour changes | +| `BVA0` | `VIS0` | 3 | a shape appears or vanishes | + +Plus 796 `NANR` cell animations on the 2D side, over 810 `NCER` cell banks. + +#### The size rule, fitted rather than assumed + +A joint's animation block is a flags word and seven channels — three scale, +one rotation, three translation — eight bytes each when animated, four when +the channel holds one value for the whole animation. Which are which is the +flags word, and **nothing states the block's size.** Get it wrong and the walk +desynchronises, exactly like a bad display-list command length. + +But the file does state it, indirectly: each block runs to the next joint's +offset. So there are 1,090 known sizes and 64 distinct flag words, and the +rule can be *solved for*: + +``` +size = 60 - 12*bit1 - 24*bit9 - 4*(bit3 + bit4 + bit5 + bit6 + bit8) + bits 0, 11, 12, 13 cost nothing +``` + +Maximum error over all 1,090 blocks: **zero**. A fit that is wrong anywhere is +wrong by four bytes somewhere, and there is nowhere. Sixty bytes is exactly +4 + 7×8, which is what says the seven-channel reading is right rather than a +coincidence that happens to add up. + +#### The cross-check: an animation names something in another file + +An animation header states no counts of its own, so there is nothing inside it +to reproduce. The check has to come from outside: **a joint animation must +have as many blocks as some model in the same archive has joints, and every +other format's target names must be joint or material names of a model there.** + +``` +BCA0 joint 181 animations, 181 structurally exact +BTP0 texture pattern 72 animations, 72 structurally exact +BMA0 material colour 24 animations, 24 structurally exact +BTA0 texture SRT 98 animations, 98 structurally exact +BVA0 visibility 3 animations, 3 structurally exact +cross-check against models in the same archive: 279 of 280 resolve +``` + +That is **378 of 378**, and it was 367 until two more of my own tests turned +out to be the thing that was wrong: + +* *"A material name cannot contain a colon."* Eight SRT animations — the Wi-Fi + lobby's fireworks and three gym pieces — drive materials called + `water:lambert5` and `hanabi2:main1`. A character class I wrote by looking at + the names I happened to have seen rejected every one of them. +* *"Every non-joint format hangs its targets off a dictionary."* Visibility does + not. It is a bit per node per frame with no names at all, positional the way a + joint animation is, and demanding a dictionary reported all three of them as + unreadable. See *Visibility has no names, and that is the format*. + +Both were caught the same way: a failure that lands on **every file of one +kind** is a bad test, not a bad cartridge. + +Two of the three checks that were tried and **rejected** earlier are worth +keeping for the same reason: + +* *"A dictionary record is four bytes."* True in the model section, where a + record is a bare offset. False here: a texture SRT record is **forty**, with + its scale, rotation and translation descriptors inline. Requiring four + rejected all 98 SRT and all 24 material-colour animations — every file of two + whole formats at once, which is a bad test, not a bad cartridge. +* *"A dictionary has at least one entry."* Nine of the ninety-eight SRT + animations have **none** — a file that drives nothing. A wrong offset does + not produce a clean zero; it produces a large arbitrary count. + +#### The other four formats, with their numbers + +The joint format got its values first because the briefcase needed them. The +other four are what animates the **world** rather than a skeleton, and almost +all of them live in one archive — `/arc/bm_anime.narc`, the field's own — which +holds **no models at all**: 98 texture scrolls, 72 flipbooks and a pile of door +and machinery animations laid over map meshes that are not built yet. + +**Texture SRT (`BTA0`)** is five channels per material — scale S, scale T, +rotation, translate S, translate T — at eight bytes each: a frame count, a flag +word, and then either the value or an offset to one per frame. `0x2000` means +the value is in the record; `0x1000` means the array is `fx16` rather than +`fx32`. Rotation is a sine and a cosine, so its "value" is a pair. `funsui` — +the fountain — is identity scale, identity rotation, no S scroll, and a T +scroll that counts steadily downward. That is water, in five numbers. + +The check here is a **budget rather than a tiling**, and the difference is worth +naming because every other format tiles exactly. A constant channel also gets a +four-byte array written for it, which the record then duplicates inline, so the +arrays cannot all be placed from the records alone. What can be demanded is that +every animated array sits inside the animation, that none overlap, and that the +leftover is exactly four bytes per constant channel. **All 98 satisfy that.** + +On top of the placement, every rotation frame in the cartridge is a unit pair — +sin² + cos² = 1 — which is an independent way of saying the pair is read in the +right order and at the right scale. + +**Texture pattern (`BTP0`)** is the flipbook: a material swaps which picture and +which palette it wears at named frames. The animation carries its own two name +lists, and the layout tiles to the byte — dictionary, then each target's +keyframes, then the texture names and the palette names, which end exactly at +the animation's last byte. **All 72.** Every keyframe is in frame order, inside +the frame count, and indexes a name the animation actually carries. + +**Material colour (`BMA0`)** is five channels of four bytes: diffuse, ambient, +specular, emission and alpha. The four colours are 15-bit `GX_RGB` at two bytes +a frame and the alpha is a single byte — and it is the alpha being one byte +wide that makes the tiling come out exact rather than nearly. **All 24**, with +no colour ever setting bit 15 and no alpha ever exceeding 31. + +#### Visibility has no names, and that is the format + +`BVA0` is three files and it had been reported as three failures for as long as +the reader has existed, because the check insisted on a target dictionary. There +isn't one. The animation is a header and then **one bit per node per frame**, +positional exactly like a joint animation, with the node count in the header. + +The header proves the reading by itself: twelve bytes plus one bit per node per +frame is the animation's exact length, in all three. + +The packing direction is settled by the data rather than by convention. +`kurotama` is 42 nodes over 601 frames: + +``` +read frame-major : 80 visibility changes in total, mean run 393 frames +read node-major : 3,497 changes, mean run 35 frames +``` + +Nobody authored the second one. And the count is checkable from outside the +file: `kurotama` has 42 nodes and `ari_start` has 9, which is exactly what the +models of those names carry — two files agreeing on a number neither derived +from the other. + +#### A rule about what goes in the cache + +`bm_anime` has no models, so its joint animations have nothing in the archive +that can wear them. Writing their matrices anyway cost **2.2 MB** of runtime +cache for poses nothing could apply. + +So the extractor now writes a joint animation's matrices **only where a model in +the same set has that many nodes** — the same pairing `Gen4Anim.resolve` checks. +The texture scrolls and flipbooks, which are what make water move and are 200 KB +rather than 2.2 MB, are written regardless. When the map meshes land, their +joint animations come with them. + +#### What the starter select actually is + +With the reader in place, `ev_pokeselect` reads out exactly as +`choose_starter_app.c` loads it, and every animation's name matches its +model's: + +``` +0 joint anim "psel_all" 41 frames, 12 joints <-> model psel_all (12 bones, 2,430 verts) +2 joint anim "psel_mb_a" 73 frames, 4 joints <-> model psel_mb_a (4 bones, 328 verts) +4 joint anim "psel_mb_b" 73 frames, 4 joints <-> model psel_mb_b +6 joint anim "psel_mb_c" 73 frames, 4 joints <-> model psel_mb_c +8 model "psel_trunk" 3 bones, 12 materials, 20 shapes -- the open case +9 model "pmsel_bg" the ground +``` + +**The box animation is 41 frames over 12 joints**, and the three balls are 73 +frames each. Names and joint counts agreeing across two independent files is +the cross-check at its sharpest. + +#### What is still not done + +The keyframe **values** are not decoded, and that is deliberate rather than +unfinished. A value decoder for these formats cannot be checked the way the +mesh reader could — no header states a count for it to reproduce — so guessing +at which of the seven channels is which would produce numbers that animate +something, plausibly, and wrongly. The structural pass is what makes the value +pass checkable: it establishes exactly where each channel's data begins and +ends, and a decode that runs off the end of one now has somewhere to be caught. + +### The renderer, and getting geometry into the cache without it becoming a megabyte + +Reading a model and drawing one are separate problems, and the second one had +no home: the engine core has exactly one mesh in it (the tilt ground quad) and +one shader. The voxel 3D lives in a mod. So Platinum's 590 buildings, Giratina, +the field effects and the briefcase all had geometry decoded and nothing able +to put it on screen. + +#### Two binary strings, not 2,430 tables + +`Gen4Nsbmd` hands back a model as Lua tables — one per vertex, one per +triangle. That is the right shape to *check*, because the corpus test reads it, +and the wrong shape to ship: the briefcase alone is 2,430 vertex tables and +1,644 triangle tables, and written as Lua source that is a megabyte of +`{ x = ..., y = ... }` for one model out of six. + +So a packed shape is two binary strings, the same trick the map grids already +use (`def.blocks` is a 2 KB string, not 1,024 tables): + +``` +vertex 14 bytes x, y, z, u, v as s16; r, g, b as u8; one byte of pad +index 2 bytes u16 into this shape's own vertex array +``` + +**The precision is the cartridge's own**, which is what makes this lossless +rather than a compromise. A Gen 4 coordinate *is* fx16 — a signed 16-bit number +over 4096 — so the raw fixed-point value keeps every position exactly as the +display list gave it. Texture coordinates are already integers in sixteenths of +a texel. Nothing here rounds anything that was not already round. + +The whole cartridge packs to **3.97 MB across 238,372 vertices**, which is +small enough that publishing all 1,030 models is a choice rather than a +constraint. Only the starter selection's six are published so far, because a +model in the cache is worth having only when something draws it, and exactly +one screen will. + +#### The check the pack needed + +The mesh reader's own corpus test proves the *decode*. It says nothing about +the *pack*, and a packer that drops a field, swaps two, or mis-signs a negative +produces a model that still draws — inside out, or with a wall missing — which +nothing else would notice. + +So the stage packs each model and **unpacks it again**, requiring every +coordinate and every index back unchanged to within half a fixed-point step, +which is the only rounding the format does. Run over the cartridge: + +``` +1,030 models packed, 0 failed round-trip +``` + +#### What the renderer is, and what it deliberately is not + +`src/render/Gen4Model.lua`: a vertex shader with a model-view-projection +matrix, a `discard` on transparent texels, LÖVE meshes with an index buffer, +and a depth canvas. + +It does **not** register a pipeline, touch the world pass, or ask the Renderer +for anything. A screen makes one, draws it into its own canvas, and throws it +away. That is because the first thing to use it is the starter select — a menu +with three Poké Balls in a briefcase, with no world behind it and no business +being in the world's pipeline. The map meshes will want the pipeline; a menu +does not, and building for the harder case first would have meant neither +worked. + +Three decisions in it that are the cartridge's rather than mine: + +* **Transparent texels `discard` rather than blend.** A Gen 4 texture keeps + colour 0 transparent, and a transparent texel must not write depth — or the + hole punched through a strap occludes what is behind it. +* **Back faces are not culled.** A DS polygon carries its own front/back flags + in its material, and plenty of Gen 4 geometry is single-sided sheets meant to + be seen from both. Culling uniformly loses the far wall of the briefcase; + with a depth buffer, drawing both costs a few overdrawn pixels. +* **Nearest filtering, always.** These are 16- and 64-pixel textures on a model + drawn several times its own size. Smoothing turns a Poké Ball's seam into a + smear. + +The depth mode and cull mode are both saved and restored around the draw, +unconditionally. This runs inside somebody else's `draw`, and a depth test left +switched on makes the next ordinary 2D blit vanish in a way that looks like a +bug in whatever came after it. + +The starter set as published: + +``` +psel_all 29 shapes, 2,430 verts, 1,644 tris (the closed briefcase) +psel_trunk 20 shapes, 1,450 verts, 828 tris (the open one) +psel_mb_a/b/c 3 shapes, 328 verts, 272 tris each +pmsel_bg 1 shape, 62 verts, 48 tris (the ground) +59 shapes, every one textured -- 86.9 KB of packed geometry +``` + +with the four joint animations carried beside them rather than folded in, +because an animation names what it drives and the pairing is by name — which is +the cross-check, and merging them would throw it away. + +### The starter select, and the three lists that had to agree + +`Gen4StarterSelect` draws Professor Rowan's briefcase from the cartridge's own +geometry: the open case and the three Poké Balls are NSBMD models out of +`ev_pokeselect`, decoded, packed, textured and drawn by the mesh renderer. It +is the first 3D this port draws itself. + +#### Three lists, none of which knows about the others + +The species ids come from `choose_starter_app.c` +(`STARTER_OPTION_0 = SPECIES_TURTWIG`), the offer lines from **message bank +360**, and the ball models from `ev_pokeselect`'s members 3, 5 and 7. Nothing +links them — they are three parallel orderings that happen to line up, and if +they ever stopped lining up the briefcase would hand over the wrong Pokémon +with nothing to report. + +So the stage checks them against each other: the species the id names must be +the one the offer line is about. All three agree. + +``` +TURTWIG 387 psel_mb_a "Tiny Leaf Pokémon TURTWIG! Will you take this Pokémon?" +CHIMCHAR 390 psel_mb_b "Chimp Pokémon CHIMCHAR! Do you choose this Pokémon?" +PIPLUP 393 psel_mb_c "Penguin Pokémon PIPLUP! Is this Pokémon for you?" +``` + +A `mismatch` field is written when they do not, and nothing should ever read +it — it exists so that if the three ever drift, the drift is in the data rather +than in somebody's memory. + +#### The placement was never mine to invent — it is in the model's nodes + +The first version of this screen placed the three balls itself. It measured the +case's inside floor (41 vertices cluster at y = 0, the outer base at −30, the +lid to 98), divided the case's own 152 of width into three, and set named +constants `FLOOR_Y`, `FRONT_Z` and `SPREAD_X` with a paragraph explaining which +of them were measured and which were arithmetic. + +All of it was wrong, and honestly explained wrongness is still wrongness. The +positions are **data**, and they were sitting in a part of the model file this +reader had never opened. + +Every NSBMD carries a node list beside its shapes, and a shape's vertices are in +**its node's** space rather than the model's. On most of this cartridge the +nodes are identity and nothing is lost by ignoring them — which is exactly why +ignoring them survived 1,030 models of corpus testing without a complaint. On +`psel_all`, the model that holds the case *and* the three balls *and* their +shadows, the nodes are the only record of where anything is: + +``` +node 4 psel_mb_a T = (-30, 50, 0) +node 6 psel_mb_b T = ( 0, 44, 0) +node 8 psel_mb_c T = ( 30, 50, 0) +node 10 tran_down identity (the case body) +node 11 tran_top identity (the lid) +``` + +Thirty apart, not forty-two; the middle ball six units lower than the other +two, which no amount of dividing a width was ever going to produce. + +A node block is a flags word, a spare `fx16` belonging to the rotation, and then +only the parts that are not the default: three `fx32` of translation unless +bit 0, a rotation unless bit 1 (a pivot pair under bit 3, otherwise the other +eight elements of a 3×3, the first being that spare word), and three scales with +their reciprocals unless bit 2. **Every node block in the cartridge ends exactly +where the next one begins under that reading**, which is what makes the widths +right rather than plausible. + +Arranging them is the SBC again — the same little bytecode that says which +material a shape wears. `renderCommands` had been reducing it to that one fact; +posing needs the *order* as well, because a node descriptor multiplies onto +whatever matrix is current, may restore from a saved slot first, and may save +its result for a later shape to come back to. `poseCommands` keeps the sequence +and `pose` replays it. The rest pose is that walk with the model's own node +matrices; an animated pose is the same walk with a joint animation's frame. + +That is the whole reason the animation was worth decoding as matrices rather +than as keyframes: **the two are the same walk over different numbers.** + +#### The opening animation, decoded + +A joint block is four bytes of header and then seven channels in this order — +translation x, y, z, the rotation, scale x, y, z. Each is absent, constant or +animated: + +| | absent | constant | animated | +|---|---|---|---| +| translation | bit 1 | bits 3, 4, 5 — one `fx32` each | 8 bytes | +| rotation | bit 6 | bit 8 — a `u16` index | 8 bytes | +| scale | bit 9 | bits 11, 12, 13 — **8 bytes**, a value and its reciprocal | 8 bytes | + +Scale is the one that hides: constant and animated are the same width, so bits +11–13 cost nothing and are the *only* way to tell them apart. That is why the +earlier fitted per-bit size rule could be exact and still not say what a block +contained. + +An animated channel is a start frame (zero in all 1,287 of them), a word holding +the frame count with `0x2000` meaning the values are `fx16` rather than `fx32`, +and an offset. + +**The invariant.** Walking every joint channel by channel lands exactly on the +block's stated end **1,099 times out of 1,099**. Then the value arrays, the two +rotation tables and nothing else **tile every one of the 183 joint animations** +from the end of its joint blocks to its last byte — no gap past three bytes of +alignment, no overlap anywhere. An element size read wrong leaves a hole +somewhere, and there is nowhere. That check ships as `Gen4Anim.checkValues`. + +The half-precision bit got its own confirmation for free: of 418 full-precision +translation channels, **not one** stays inside the `fx16` range, and of 123 +half-precision channels, **not one** leaves it (the largest is 7.650 against a +limit of 8). The two encodings are the same units, and the flag means exactly +what it looks like. + +#### Two rotation tables that had to agree with each other + +A rotation frame is a `u16` whose top bit picks the table: set for the pivot +table at +0x0C, clear for the compressed 3×3 table at +0x10. That is not a +convention borrowed from elsewhere — in `psel_all` the indices with the top bit +set are exactly 0..90 and the ones without are exactly 0..139, while the tables +hold exactly **91** six-byte records and exactly **140** ten-byte records. Two +counts, neither derived from the other, partitioned with nothing left over and +nothing out of range. + +**A pivot record** is a rotation one of whose rows is an axis: a ±1 at position +`flags & 0x0F`, its row and column zero, and the remaining 2×2 holding a cosine +and a sine — which is why a² + b² is 1 in all 24,538 of them. The sign of the +±1 follows the parity of the position, and bit 6 flips it. + +That last bit is where the first attempt was wrong, and where the check caught +it. Where an animation crosses between the two tables mid-channel, the pivot +frame and the compressed frame beside it are **the same rotation described twice +by two encodings that share nothing**. Under a parity-only rule, six of the +fifteen flag words that occur disagreed with their neighbours by a whole unit — +and they had to, because negating the ±1 alone turns a rotation into a +reflection. Flipping the 2×2 with it — `(a, b / b, −a)` rather than +`(a, b / −b, a)` — restores the determinant and the agreement. + +**A compressed record** is five numbers for a nine-number matrix: the first row, +then the first two of the second. The rest follows from the matrix being a +rotation. The obvious closed form — solve perpendicularity for the missing +element — divides by the first row's third element, which is a rounding error +from zero in **727 of the 6,876 records here**; it produces components past 11, +which is not a rotation at all. Taking the magnitude from unit length and only +the *sign* from perpendicularity is stable everywhere and agrees with the +division wherever the division means anything. All 6,876 come out orthonormal +with determinant +1. + +Across the cartridge, 406 of the 419 table crossings are smooth relative to +their own channel's typical motion. The 13 that are not sit in four animations — +the Spear Pillar cutscene and one Mime Jr. animation — and mostly at frame 2, +which is what a deliberate cut looks like. That is reported as a counted +statistic rather than as a pass, for the same reason the SBC walker reports +"38 of 40 shapes drawn" instead of claiming every shape. + +#### What the briefcase actually does + +``` +tran_down identity → −90° about X by frame 30, overshooting around 15–25 +tran_top tilts and returns to identity — so the lid opens 90° relative to it +psel_mb_a (−30, 50, 0) tumbling to (−44, −4, 32), at a constant 0.8 scale +psel_mb_b ( 0, 44, 0) → (0, −4, 62) +psel_mb_c ( 30, 50, 0) → (38, −4, 26) +``` + +The case starts closed and upright, rotates down, the lid swings open, and the +three balls tip out and settle in a row in front of it. **Frame 0 of every ball +joint equals that ball's own node transform in the model** — a third independent +agreement, between a file that says where things rest and a file that says how +they move. + +A track is packed at 48 bytes a frame: the top three rows of the matrix at the +cartridge's own 1/4096, so the round trip can demand the values back *unchanged* +rather than close. All 1,099 joint tracks in the cartridge round-trip exactly. + +That number was 30 bytes in the first version, which stored the rotation as +`s16` on the grounds that a rotation element cannot leave −1..1 and the scale +beside it never left 0.8..1.25. True of the briefcase, false of the cartridge: +41 tracks carry scales past 8 and Giratina's pillars reach 18.4. The round trip +said so — which is the only reason it is not still wrong — and the fix was to +stop assuming a range rather than to widen the one I had assumed. + +#### What the screen invents now + +Two things, down from four: + +* **The lift on the selected ball.** The cartridge has a 73-frame animation per + ball for this and all three are extracted; what the app does with them is code + rather than data, so this raises the chosen one instead. +* **The camera.** Distance and pitch frame the posed model; the cartridge's + camera comes from its own movement steps. + +One more thing had to be said out loud rather than assumed: `psel_all` is +**Z-up**. It was exported with the vertical axis last — its lid reaches 116 in +what the case model calls depth — while the engine's camera assumes y is up. The +screen applies that rotation to the *scene* rather than to the geometry, so the +cartridge's numbers stay the cartridge's numbers. + +#### A bounding box that does not agree with its own geometry + +While measuring the framing: the model header's stated box matches the decoded +geometry on only **312 of 1,032 models**, under any reading of it I could find +that works on the rest. (It reads as a corner and a size at 1/2048, which is +right on the simple models and wrong on the ones with real hierarchies.) + +Rather than ship a camera that trusts it, `Gen4Model:framing` now measures each +shape's own box while the vertices are still in hand and unions them **through +the pose** — which it has to do anyway, since the three balls are only 60 apart +once their nodes have placed them. The header box is left unread. It is listed +under open questions rather than called a bug, because a reading that works on +a third of the corpus is more likely to be an incomplete reading than a broken +file. + +--- + +## The map meshes, and the block that agrees with them + +The 3D half of the world is two archives and they both read now. + +**Terrain.** Every land chunk's fourth block is an ordinary NSBMD, and all +**666 of them decode exactly** against their own headers' vertex, polygon, +triangle and quad counts — 7,547 shapes and 1,066,987 vertices of Sinnoh. + +**Buildings.** `/fielddata/build_model/build_model.narc` is 590 models, **all +590 exact**, 1,362 shapes and 89,253 vertices. 568 of them carry their own +textures; the other 22 do not, which is the first hint that where a picture +lives is a separate question from where a mesh lives. + +### Where the ground is, checked against a block that was not used to find it + +A land chunk holds four things: a permission grid, the objects standing on it, +the mesh, and a `BDHC` height field. The height field was decoded long before +the mesh, from the cartridge's own `BDHC_LoadHeader`, and it measured a tile at +**sixteen world units with the chunk centred on the origin** off nothing but +its plate extents. + +The mesh, read independently, spans −256..256 on both horizontal axes. Same 512 +units over 32 tiles, same centring. Two blocks of the same file, neither read +from the other, agreeing about the size of a tile. + +Then the harder question: do they agree about the *height*? Sample every fourth +tile of every chunk, keep only the tiles the permission grid calls land rather +than void — a third block, used to decide where the comparison is even +meaningful — and ask the mesh how high its surface is under that point: + +``` +331 of 666 chunks agree with the BDHC at every sampled point +8,505 of 11,356 points agree to within 0.1 units -- not close, exact +1,655 more within four units; 55 further than sixty-four +``` + +**The agreement being exact where it happens is what makes this a check rather +than a correlation.** A wrong scale, a flipped axis or a missed `posScale` +produces a cloud of near-misses; it does not put three quarters of the points +on the answer to a tenth of a unit. All four axis flips were tried and every +one of them made it worse. + +And the tail says something specific rather than being noise: **9,497 walkable +tiles have no land-mesh triangle beneath them at all.** The land mesh is not +the whole floor. Indoor chunks are a shell, and their floors are building +models — which is the next thing to establish, not a disagreement about this +one. + +### Looking at it + +The permission grid was validated by drawing it and recognising Sinnoh. The +meshes were validated the same way, and the second picture is the one that +settles the whole chain at once. + +`docs/images/gen4-sinnoh-mesh.png` is the entire 30×30 overworld matrix — +900 chunks, one pixel a tile — rasterised top-down from the decoded geometry, +with every texture sampled through the interpolated UVs and a depth test +keeping whatever is highest. `docs/images/gen4-town-mesh.png` is three chunks of +one town at eight pixels a tile, with its buildings placed from the chunk's own +object records. + +Mt. Coronet runs north to south through the middle in bare rock; the three +lakes sit in their pockets; the snow starts at the top of the map; the Great +Marsh, the east-coast water column and the Battle Zone are all where the +permission grid put them — **and the permission grid was never consulted to draw +this.** Two independent blocks of the same 666 files producing the same Sinnoh +is the strongest statement available about either of them. + +The town picture says more about the details: the plaza's paving, hedges, +benches, the fountain, a bridge and the roofs all land in the right places at +the right sizes, which exercises the node transforms, the SBC pose walk, the +`posScale` on every model, the 16-units-per-tile chunk placement, the object +records' fixed-point positions and the texture binding, all at once. A mistake +in any of them is visible immediately. + +Two things are visible and honest to name: the black areas are chunks with no +mesh at all, and a few faces come out untextured white where a material's +texture is not in the library. + +WHAT THIS RENDERER IS NOT. It is a throwaway top-down rasteriser in the +harness, not the engine — no perspective, no lighting, no alpha, no animation. +It exists to answer "is the geometry right", and it does. + +### Which pictures a map wears + +A land mesh carries no textures — all 666 of them — so `areaDataArchiveID` in +the map header is what says where the pictures are. The area record is eight +bytes: + +| | | | +|---|---|---| +| `+0` | buildings | `area_build.narc` **and** `areabm_texset.narc` together | +| `+2` | mapTexture | `map_tex_set.narc` | +| `+4` | lighting | 0..9 | +| `+6` | flags | 0..2 | + +The ranges pin the fields. Over all 75 areas `+2` reaches 73 against a +74-member archive, so it can only be the map textures; `+0` reaches 70 against +two archives of 71 each, and those two are parallel — one names the building +models an area uses, the other holds their textures. `+4` never passes 9 and +`+6` never passes 2, so neither indexes anything here; both are named for what +they are not. All 71 area build lists end exactly on their own leading count, +and all 2,818 model ids they contain are inside `build_model.narc`. + +The pictures above resolve every texture through a **library of all 74 map +sets plus all 71 building sets, first match by name** — which is why they look +right and is not what the cartridge does. + +**What does not resolve yet, stated plainly.** Of the 2,307 distinct texture and +palette names the terrain meshes ask for, **2,287 exist in some `map_tex_set` +member** — the twenty that do not are the Underground and a handful of gym +objects, which are elsewhere. But matching each map to *its* set is not +finished: attributing chunks to maps through the matrix's own header grid, 729 +of 867 chunk-and-map pairs find every name they need in that map's set, and 138 +do not. The names they miss are the commonest ones in the game (`ngrass`, +`grass`, `tshadow`), each of which appears in around 23 of the 74 sets, so this +reads like a second set being loaded alongside the first rather than a wrong +index — and "reads like" is exactly why it is written down here instead of +being coded. + +--- + +## The item table was wrong for two thirds of its rows + +Building the Gen 4 bag started with a simple question — which pocket does an +item live in — and the answer came back nonsense: the Bicycle, the Town Map and +the Old Rod all claimed the same pocket as TM79. + +**The name bank has 468 entries and the data archive has 446.** The difference +is the twenty-two unused ids 113..134, which the names keep as `???` +placeholders and the data archive simply does not have. A member index is +therefore item `i` up to 112 and item `i + 22` after that — and this reader had +the comment "member index is the item id" written at the top of it. + +Every item from Adamant Orb onward — **333 of the 446** — carried the price, +pocket, hold effect, fling data and party-use block of an item twenty-two +places further on. Twenty-two more items (ids 446..467, up to the Secret Key) +did not exist in the table at all. + +**Why it survived.** The note above that code listed its own verification: +"Master Ball 0, Ultra Ball 1200, Poké Ball 200, Potion 300, HP Up 9800". All +five are below the gap. So are the balls, the medicines and the battle items, +which is why every pocket anyone had thought to look at came out right. + +**What found it** was asking a question that covers the whole table instead of +the start of it: group every item by the pocket its own record claims, and +print the id runs. + +``` +pocket 2 ids 1..16 (16) Master Ball .. Cherish Ball +pocket 1 ids 17..54 (38) Potion .. Old Gateau +pocket 6 ids 55..67 (13) Guard Spec. .. Red Flute +pocket 5 ids 115..126 (12) ??? .. ??? +pocket 4 ids 127..190 (64) ??? .. Kebia Berry +pocket 3 ids 306..405 (100) Sky Plate .. TM78 +pocket 7 ids 406..445 (40) TM79 .. Old Rod +``` + +Every run is exactly the right SIZE — 16 balls, 38 medicines, 13 battle items, +12 mail, 64 berries, 100 TMs and HMs, 40 key items — and every run from the +mail onward sits twenty-two ids too low. **Eight independent counts agreeing +while eight independent positions disagree is one offset, not eight +coincidences.** + +The fix ships with the check rather than instead of it. `Gen4Items.POCKET_RANGES` +is the cartridge's own id ranges, written down separately from the record field +that is decoded — so the two can disagree — and `Gen4Items.checkPockets` +requires every item in a range to claim that range's pocket and every item +outside them to claim ITEMS. Under the old reading it fails 333 times. Under +the new one: + +``` +pocket check: 445 of 445 items sit in the pocket their id range says +``` + +## The trainer card + +The second of the four screens reported as falling back to Kanto's art. It has +two pages, as Platinum does. + +The **badge case** is entirely the cartridge's: the "LEAGUE BADGES" panel +composed from its own tilemap, the eight badge sprites, and where they go — +measured off the panel rather than guessed, since the sockets are the only +shapes on it darker than its own background and come out at x = 43, 99, 155, +211 and y = 59, 115. Each badge's art sits in the top-left 40×40 of its 64×64 +frame, so a badge drawn twenty pixels up and left of a socket's centre lands in +it. + +The **card face** is not the cartridge's, and the file says so at the top rather +than leaving it to be discovered. Platinum draws the card's background and its +field labels as background tiles; `trainer_card/trainer_card` is that strip, +extracted at 64×240 and uncomposed, because nothing in that archive pairs it +with a tilemap the way the badge case is paired with its own. The portrait is +the same story. Both are one tilemap away, and finding it is the work — not +drawing something close enough over the gap. + +--- + +## The bag + +The third of the four screens reported as falling back to Kanto's, and the one +the item fix above was really for. + +**Eight pockets, not five.** Gen 1–3 have four or five; Platinum has ITEMS, +MEDICINE, POKé BALLS, TMs & HMs, BERRIES, MAIL, BATTLE ITEMS and KEY ITEMS. +Every one of those words is bank 395's, and the bank's order is exactly the +order an item record's own `fieldPocket` numbers them — so the bank index *is* +the pocket number and nothing pairs the two by hand. The counts agree item for +item: 163 / 38 / 16 / 100 / 64 / 12 / 13 / 40. + +The art is the cartridge's: `bag/bag_ui_main` is the screen, and the pocket +icons are one 64×64 sheet whose top eight sixteen-pixel cells are the icons and +whose bottom eight are the small markers that sit under an unselected one. That +split was measured, not assumed — on a sixteen-pixel grid the top eight cells +carry 160 opaque pixels each and the bottom eight carry 40. + +**What an item does is not reimplemented here.** Using, giving and tossing live +in `BagMenu.useItem`, which is published for exactly this reason and is what +Emerald's bag calls too. This screen is Platinum's list and Platinum's words. + +And it declines what it cannot answer: a push carrying `sell`, `itemPc` or +`store` falls through to the screen that has those flows, the same way the Gen 3 +aliases have always worked. Better a Kanto shop counter than a Platinum bag +that cannot sell. + +--- + +## The party screen, and where it stops + +The last of the four. The six panels' positions are measured off `party/menu` +rather than guessed: the panels are one teal, striped every other row, so the +stripes give the bands away — on the left half they run 5..47, 53..95 and +101..143, and on the right half 13..57, 61..107 and 109..151. Two columns of +three, a pitch of 48 on both sides, the right one starting eight pixels lower, +which is the stagger Platinum's screen has. The cursor art being 128 wide is the +third independent way of saying a column is half the screen. + +**Where it stops is the interesting part**, and it is in the alias rather than +buried in the screen: + +| push carries | screen | why | +|---|---|---| +| `onCancel` alone — the field menu | Gen 4 | this is the reported bug | +| `pickOnly` / `forceSwitch` | Gen 4 | a pick with no submenu to draw | +| `battle` | Gen 3 | SHIFT, forced switches and item targets already work there, in Emerald's own words | +| `tmhm` | Gen 3 | ABLE / NOT ABLE are cartridge words this cache does not carry | +| `chooseOrder` / `onOrder` | Gen 3 | the Frontier's four sentences, likewise | + +A declined push is Hoenn's screen doing the job. A served one that cannot finish +it is a dead end, and a dead end in a party menu is a save the player cannot get +out of. + +The one thing the field menu does not offer is GIVE/TAKE an item. +`BagMenu.giveItem` is published and would serve it; what is missing is +Platinum's own word for it, and a menu entry in the engine's English on a screen +that is otherwise the cartridge's is exactly the seam this port does not leave. + +--- + +## The summary pages, and the bank that had to be found by a phrase + +The party menu's SUMMARY had nowhere to go, so this is the fifth screen. + +**Bank 455 is the whole screen's vocabulary** — the six page titles, every +field label, the twenty-five nature lines and the twenty-five characteristic +lines, 187 strings that are this screen and nothing else. Finding it is the +part worth recording. + +Searching for the obvious words found the wrong banks twice. Bank 326 carries +"Exp. Points", "ID No.", "Nature" and "Item"; bank 336 carries "Exp. Points", +"Held item", "Ribbons" and "Ability". Both are **debug menus** — the giveaway +is what sits beside those words: "Random value", "HP rnd", "Set Ribbons", "msg +location", "Player's side 1". + +What found the right one was searching for the phrase nothing else in the +cartridge says. **"To Next Lv." occurs exactly once in all 1,127 banks.** Three +common words in common is not an identification; a phrase that occurs once is. + +``` + 7 "POKéMON INFO" 109 "POKéMON SKILLS" 128 "BATTLE MOVES" + 8 "Pokédex No." 110 "HP" 135 "PP" + 10 "Name" 111 "Attack" 147 "POWER" + 12 "Type" 112 "Defense" 148 "ACCURACY" + 13 "OT" 113 "Sp. Atk" 149 "CATEGORY" + 15 "ID No." 114 "Sp. Def" 152 "SWITCH" + 17 "Exp. Points" 115 "Speed" 24..48 the 25 natures + 19 "To Next Lv." 116 "Ability" 76..100 the 25 characteristics +``` + +**The rows are measured off the pages themselves.** Each page is a panel of +stripes and a stripe boundary is a row: on `page_info` the colour changes at +y = 40, 56, 72, 88, 104, 120, 136 — a sixteen-pixel pitch, one row per label, +and there are exactly seven labels. On `page_battle_moves` the changes come at +50, 82, 114, 146: four move rows at a pitch of thirty-two. The white value +boxes sit at x = 180, which is where the values go. + +**Three pages, not six.** CONDITION, CONTEST MOVES and RIBBONS are named in the +bank and are not drawn. Contest stats and ribbons are not modelled by this +engine, and an empty page carrying the cartridge's own title would claim they +were. The move-learn screen's "which move to forget" is likewise not served — +the alias does not list `choose`, so that push keeps the Gen 3 screen. + +--- + +## Two screens that were "missing a tilemap" had one all along + +The trainer card's face and the Poketch's twenty-five app screens were both +written up here as blocked on a tilemap nobody could find in their archive. +Both archives have one. What was wrong was the pairing. + +`/graphic/poketch.narc` has **27 NSCRs**, named `calculator.NSCR`, +`digital_watch.NSCR`, `coin_toss.NSCR` — one per app. `/graphic/trainer_case.narc` +has ten, including `trainer_card_front.NSCR`, `lucas.NSCR` and `dawn.NSCR`. + +**Three separate pairing faults, each of which composed something rather than +failing:** + +1. **`_bg_tiles` was not a role suffix.** Every Pokétch app names its + background `_bg_tiles.NCGR` beside `.NSCR`. The suffix list had + `_tiles` but not `_bg_tiles`, so the two never grouped, and every app's + tilemap fell through to the archive's shared sheet — the device border. + Composing the border's tiles through the calculator's map is what produced + the repeating tile soup. Adding the suffix **before** `_tiles` (so the + longer one matches first) fixes twenty of the twenty-five. +2. **A base can own two sheets.** `stopwatch_bg_tiles.NCGR` and + `stopwatch.NCGR` normalise to the same name; the first is the app's + background and the second is its sprite sheet, and the merge took whichever + came first in the archive. A sheet whose own name carries a role suffix is + the background, and now wins. +3. **Some screens name their sheet after something else entirely.** Both + watches draw on `watch_bg_tiles`; the card's front and back both draw on + `trainer_card_tiles`; Lucas and Dawn both draw on `player_tiles`. No rule + over the names finds those — `lucas` and `player` share nothing — so they + are written down per archive as `tilesFrom`, and the suffixes handle the + rest. + +The calculator now composes as a calculator keypad and Lucas composes as Lucas. + +**What is still wrong: the palettes.** Most Pokétch apps carry no `NCLR` of +their own and take the archive's shared one, and several then compose black on +black. The Pokétch's display colour is a runtime setting on the hardware — the +Color Changer app sets it — so "which palette" may not have a static answer, +and that is the next thing to establish rather than to guess at. + +Checked for regressions: the badge case, the bag, the party screen and the +summary pages all compose exactly as before, and the party panels' measured +bands are unchanged to the pixel (5..47, 53..95, 101..143 and 13..57, 61..105, +109..151), so the party menu's slot layout still holds. + +## The Poketch's apps, from two banks that disagree about order + +Twenty-five apps, and neither bank knows everything about them. + +Bank 29 entries 11..35 are the descriptions the Pokétch Company's receptionist +reads, **in app order** — which is what an app id means. What they do not do is +say where a name ends: "The Digital Watch displays the current time" is one +sentence. + +Bank 213 entries 83..107 are the same twenty-five written for the underground +shop, and there each name is wrapped in a colour code: +`The {COLOR 2}Digital Watch{COLOR 0} app displays...`. So the names come from +213 and the order from 29 — and 213 is in a **different order** (Calculator +second, Memo Pad third), so pairing them is by text, not in step. + +The pairing is the check. Matching "The ``" against the start of each +description paired twenty-four of twenty-five and left the Calendar out, +because its description is the one that does not begin that way: "Use the +monthly Calendar to make a note of important dates." Matching anywhere instead +needs the LONGEST name, because "Counter" occurs inside "Trainer Counter" — and +then the twenty-five assignments have to be a permutation, which is what says +the looser match did not fold two apps onto one name. + +``` +poketch: 25 apps, 0 unpaired +art keys that do not exist in the cache: 0 +apps with no face of their own: Friendship Checker, Pokémon History, Dot Artist +``` + +Those three have no screen of their own in the archive and fall back to the +cartridge's own `unavailable` picture rather than to a blank one this port +drew. + +--- + + +## Eight faults from play, and what each one actually was + +Reported after a session on the imported cache: + +> the intro and intro animation is still missing from the main menu just shows +> a black screen on startup with press start at the bottom, the new game menu +> is still not correct the boxes surrounding the text are still too small thry +> should match the rom, the options menu is missing gen4 platinum game +> options, and when i load in it looks like the screenshot and when i try and +> walk my character spins in circles, the trainer card still isnt correct and +> neither is the pokedex, or the pokemon party menu or the bag theyre looking +> like gen1 still. Also talking to npcs does nothing. + +Eight complaints. They are **not** eight bugs. The log from that session is +what separated them, and it is worth saying how, because the screenshots and +the descriptions on their own pointed at the wrong places entirely. + +### What the log said, and what it ruled out + +`AppData/Roaming/LOVE/Gen2Recomp/log.txt`, one boot, in order: + +``` +[debug] state stack: push Gen4Intro (depth 1) +[warn] state stack: popping Gen4Intro emptied the stack +[debug] state stack: push TitleState (depth 1) +[debug] state stack: push Gen4MainMenu (depth 2) +[debug] state stack: push Gen4Options (depth 3) +... +[warn] gen4 bag: this cache carries no pocket names -- falling back to the engine's own +[debug] state stack: push Gen4BagMenu (depth 2) +[warn] gen4 trainer card: this cache carries no badge case art -- the badge page + will be drawn in the engine's own frame +[debug] state stack: push Gen4TrainerCard (depth 2) +[info] gen2 script vm: 0 maps attached (talk) +[info] gen2 script vm: 478 maps attached (scenes) +[warn] gen4 intro: this cache carries no `gen4_intro` record; skipping the opening +[warn] no text for Twinleaf Town/nil (x10) +``` + +Three things fall out of that immediately. + +**The Gen 4 screens were not falling back to Gen 1 at all.** `Gen4BagMenu`, +`Gen4TrainerCard` and `Gen4Options` are on the stack by name. The alias table +in `Screens.lua` worked, `data.isGen4Cache` was set, and every push routed +correctly. What was wrong was one layer in: each screen found its cartridge +record missing and drew itself in the engine's own frame, which is what "looks +like gen1" was describing. Chasing the alias table would have found nothing, +for as long as it took to stop and read the two warnings above the pushes. + +**`TitleState` on the stack is `Gen4Title`.** `Game:makeTitleState` ends with +`title.screenId = title.screenId or "TitleState"`, and the stack logs +`screenId` first. The proof it is the Gen 4 one is the next line: `Gen4Title` +is the only screen that pushes `Gen4MainMenu`. So the black screen was not the +wrong screen; it was the right screen with no pictures. + +**`gen2 script vm: 478 maps attached (scenes)` should not exist in a Platinum +log at all.** + +### Fault 1 — one missing list, five faults + +`Data:load` builds its module list from three tables and never named a single +`gen4_` module. `gen4_map_headers` is loaded once, as the probe that decides +the cache is Gen 4, and its result is thrown away. So `gen4_menus`, +`gen4_graphics`, `gen4_intro` and `gen4_models` — all four written by the +extractor, all four present on disk, all four read by name from `game.data` by +the screens that need them — were never on the table. + +Every consumer is written to degrade when its record is absent. That is why it +failed silently and completely: + +| Reported as | Actually | +|---|---| +| "black screen on startup with press start" | `Gen4Title` drew its border field with no logo, because `gen4_menus.title` was nil | +| "the intro animation is still missing" | `Gen4RowanIntro` said so in the log: no `gen4_intro` record | +| "the boxes surrounding the text are still too small" | `Gen4MainMenu` sized its boxes from the engine's defaults, not `gen4_menus.layout` (`optionWidth = 26`, `margin = 32`, `lineTiles = 2`) | +| "the options menu is missing gen4 platinum game options" | `gen4_menus.options` holds seven real rows — TEXT SPEED, BATTLE SCENE, BATTLE STYLE, SOUND, BUTTON MODE, FRAME (20 values), CLOSE — and none of them was read | +| "the trainer card / party menu / bag look like gen1" | `gen4_graphics.screens` holds 922 composed pictures including all of `bag/`, `party/`, `summary/`, `trainer_card/` and `pokedex/`; the screens found nil and used the engine's frame | + +Fixed in `src/core/Data.lua`: a `GEN4_PREFIXED` list, loaded by name and only +on a Gen 4 cache, each one optional with its own log line. The prefix stays +deliberately — an un-prefixed `menus` or `graphics` would resolve through the +additive cache overlay to the **root** cache, which is Red's, which is the +exact trap `CLASSIC_ONLY` exists to close. + +The other nine `gen4_` modules on disk (`gen4_text`, `gen4_events`, +`gen4_map_permissions`, `gen4_overworld`, …) are extractor **input**: they are +lowered into `text`, `map_scripts`, `maps` and `sprites` before the cache is +written, and no running screen reads them. They are deliberately not in the +list. + +### Fault 2 — "my character spins in circles" + +`SpriteRenderer` has read every sheet in every generation with six fixed +slots: `stand down, stand up, stand left, walk down, walk up, walk left`, with +east drawn by mirroring west because no Game Boy or GBA cartridge has east +art. A Platinum sheet out of `mmodel.narc` is a stack of textures in the +**archive's** order and is not in that layout at all. On a sixteen-texture NPC, +the slot the renderer takes for "standing south" holds a **back** view and the +one it takes for "stepping south" holds a **left** view. Walking therefore +changed the apparent facing on every step. The description was exact. + +**The order is in the cartridge, in the same archive as the sheets.** The last +twenty-five members of `mmodel.narc` are not models: they are the +`BillboardGfxSequence` tables, four fields back to back — + +``` +u32 seqCount +u16 startFrame[seqCount] +u8 textureIdx[seqCount] +u8 plttIdx[seqCount] +``` + +— and a frame's texture is the last segment whose `startFrame` is `<=` it, +exactly as `billboard_gfx_sequence.c` reads it. The grouping into animations +is code rather than data (`object_event_gfx_data.c`): a walker's animation *N* +covers frames 16N..16N+15, and the direction picks *N* through `{0,1,2,3}` +with `DIR_NORTH 0, DIR_SOUTH 1, DIR_WEST 2, DIR_EAST 3`. + +So each direction is four segments of four frames, and a cycle is +**stand, step, stand, step**. + +Read that way out of the ROM: + +| sequence | up | down | left | right | +|---|---|---|---|---| +| `generic_walk` (16, every NPC) | 0, 8, 10 | 11, 12, 14 | 15, 1, 3 | 4, 5, 7 | +| `walk_and_run` (32, the player) | 0, 11, 26 | 27, 28, 30 | 31, 1, 3 | 4, 5, 7 | +| `bike` (24) | 0, 11, 18 | 19, 20, 22 | 23, 1, 3 | 4, 5, 7 | +| `pokecenter_nurse` (17) | 0, 9, 11 | 12, 13, 15 | 16, 1, 3 | 4, 5, 7 | + +as `{ stand, step, step }`. + +**Checked against the pictures, not against itself.** Laying the player's 32 +frames out in that grouping gives four rows of one consistent direction each — +back, front, left, right — and the same grouping on `lass` and `prof_rowan` +does too. A lookup that cannot miss cannot tell you it is wrong; a picture can. + +Three further checks are in the reader itself, so a member that is not one of +these tables cannot be mistaken for one: the four fields must account for the +member's length **exactly**, the start frames must begin at zero and ascend, +and every texture index must be inside the sheet it is read against. Walking +the archive backwards from the end, 25 members parse and the 26th does not, +which is the count the table of names says there should be. + +The sequence **names** are pret's (`field_sprites.order`); every number is the +cartridge's. A sheet is matched to a sequence by texture count — 16 is +`generic_walk`, 32 is `walk_and_run` — with the player's own variants matched +by name first. The only other 16-entry sequences are `contest` and `fishing`, +and both produce **identical** cycles to `generic_walk`, so the ambiguity +cannot produce a wrong answer. + +New file `src/import/Gen4Facings.lua`; the extractor writes `facings` onto +every sheet in `sprites.lua`; `SpriteRenderer.facingFrames` uses it when it is +there and **never mirrors**, because Platinum draws a real east side and +mirroring it would put the bag on the wrong shoulder. Gen 1, Gen 2 and Gen 3 +take a byte-identical path — checked by running `poseFrame` over a six-frame +and a nine-frame def and comparing every slot. + +### Fault 3 — "talking to npcs does nothing" + +Two causes, and the second was hiding behind the first. + +**`Gen2ScriptVM` was claiming Platinum's script pool.** Its ownership test was +a refusal list with one entry: `if pool.source == "RomExtractorGen3" then +return nil end`. It has to be a refusal list rather than an allow list, because +a Gen 1 cache and a hand-built developer pool both carry no `source` at all. +Gen 4 was simply missing from it. So the Johto VM lowered Platinum's decoded +instructions with Gen 2 semantics and attached the result to 478 maps as their +scene and coord-event wiring — which is what `gen2 script vm: 478 maps +attached (scenes)` in a Platinum log means, and it is a plausible source of the +warp-in-warp-out thrash also visible in that log. + +**Nothing ever called `Gen4ScriptVM`.** `data/scripts/init.lua` has a Gen 2 arm +and a Gen 3 arm and had no fourth. Platinum's 4,079 decoded scripts sat in the +cache unlowered. + +**And a Gen 4 object has no TEXT constant.** `OverworldState:talkTo` reads +`npc.def.text`; a Gen 4 object event carries a **script id**, an index into its +map's own entry-point list, and that list does not exist until the scripts have +been decoded — which is after the maps table has been written. Hence +`no text for Twinleaf Town/nil`: the field was simply not there. + +`Gen4ScriptVM.bindObjects` stamps the link stage's own label back onto each +object record at registration time. Both halves derive the label from +`Gen4ScriptVM.label`, so they cannot drift; and it costs no rewrite of a +nine-megabyte maps table to add one string per object. + +Measured on the current cache: **1,908** objects and signs take a label, 374 +maps attach talk contributions, and Twinleaf Town's five scripted objects +lower to real rows (`M1052/S05D5` → 8 rows opening on `play_sound 1500`). + +**A localId is not a key.** The link stage filed object scripts under the +object's `localId`, and the cartridge **reuses one inside a map**: 40 objects +across 28 maps share a localId with another object on the same map. Twinleaf +Town's arrow signpost, which carries the no-script sentinel, was answering with +the guitarist's dialogue. Filed by the event's position instead, which the maps +stage already writes onto the record as `index`, so both sides of the join name +the same thing. This is the third time in this port that a small integer used +as a key turned out not to be unique. + +### Fault 5 — "the boxes surrounding the text are still too small" + +This one survived the module-list fix, and it had to: the fallback layout in +`Gen4MainMenu` is character-for-character the cartridge's own numbers +(`optionX = 3`, `optionWidth = 26`, `firstY = 1`, `gap = 2`, `lineTiles = 2`, +`linePixels = 16`, `margin = 32` — `OPTION_WINDOW_WIDTH`, +`CONTINUE_WINDOW_MARGIN`, `TEXT_LINES_TILES` and `RenderOptions`' own +`nextOptionY` in `main_menu.c`). The numbers were right the whole time. They +were being drawn as the wrong rectangle. + +**A Platinum window's frame is outside it.** `Window_DrawStandardFrame` → +`DrawStandardWindowFrame` (`render_window.c`) fills + +``` +x - 1 .. x + width and y - 1 .. y + height +``` + +— one tile beyond the window on every side — and `RenderOptions` advances by +`option->height + 2`, with the cartridge's own comment: *"Add 2 to account for +the window border"*. So the window the player sees is `(width + 2)` by +`(height + 2)` tiles, anchored at `(x - 1, y - 1)`. + +`Font.drawBox` is the opposite convention: `sheetFrame` lays a `tw × th` grid +with the border on its outer ring, so the rect you pass IS the visible box. +Passing the cartridge's content rect straight through therefore drew: + +| | drawn | the cartridge's | +|---|---|---| +| column width | 208 px, x 24–232 | **224 px, x 16–240** (centred on 256) | +| one-line option | 16 px tall | **32 px** | +| CONTINUE | 80 px tall | **96 px** | + +Half the height on every one-line row, 16 px narrow, and off-centre. Fixed by +drawing `(optionX - 1, y - 1, optionWidth + 2, th + 2)` and putting the label +back at the content origin `optionX * 8`, which is where +`MainMenuUtil_ShowWindowAtPos` prints it. The CONTINUE summary moves with it: +its labels are at `+ CONTINUE_WINDOW_MARGIN` from the content origin and its +figures right-aligned at the same margin from the content's right edge, on +`TEXT_LINES(i)` — all three now literal rather than compensated with ±8 for a +frame that was in the wrong place. + +Two things fall out and both are the cartridge's behaviour: + +* **The vertical centring is gone.** It was added last round in answer to "the + tiles are too small" — three short windows at `y = 1` sat in the top third + and left the rest empty. At their real height they do not: CONTINUE plus + NEW GAME, OPTION and EXIT is 0–96, 96–128, 128–160, 160–192 — exactly 192, + the whole screen, at the cartridge's own `y = 1`. +* **The column scrolls instead of being cut off.** It can be taller than the + screen on the cartridge too — CONTINUE alone is 12 tiles and the real menu + has eight options, which is what Platinum's scroll arrows are for. The old + code `break`ed at the bottom edge, which would make EXIT unreachable the + moment a mod or a link row pushed it past 24 tiles. Now the selected window + is kept on screen and anything wholly off either edge is not drawn. + +**Full-screen was already right and is left alone.** Every one of the ten Gen 4 +screens declares `uiSize() = 256 × 192`, `wantsFillScale() = true` and +`wantsEdgeBleed() = false`; `Game:draw` resolves the surface from the topmost +state that declares one (`nativeSurfaceInStack` → `Renderer:setUISize`, which +accepts anything between 160×144 and 640×576), and `Renderer:endFrame` honours +`uiFill` with `Up = min(ph / uih, pw / uiw)` — the window filled, aspect kept, +no integer-scale letterbox and no edge smear. On the reporter's 1024×768 that +is a scale of exactly 4 in both axes. So the menus are laid out in the DS's own +256×192 pixels and then scaled to fill the window, which is what was asked for; +what was wrong was the rectangle inside those pixels. + +### Fault 4 — the cache has to be rebuilt to get any of the sprite work + +`facings` is a new field on an existing file and the object-script key changed +shape. Neither adds a **file**, so `readyReport`'s missing-file gate cannot see +it. `CACHE_FORMAT` is bumped to `rom-cache-v338:`, which is the mechanism for +exactly this. + +### Still open from this report + +* **The world is drawing `TILESET_GEN4_STANDIN`** — flat colour per terrain + class, which is the screenshot. The map meshes and textures are read (see + above); the field renderer does not use them yet. +* **The Pokedex has no Gen 4 screen at all** — no `Gen4Pokedex.lua` and no + `GEN4_ALIASES` entry, so `PokedexMenu` opens Kanto's. And a screen is not + the next thing to build, because **the Pokedex art composes blank**. Checked + pixel by pixel rather than by looking at the index: `scroll_main_background` + is 256×192 of solid black, and `info_main`, `page_panel`, `banner_sinnoh`, + `info_entry_window`, `info_species_window` and `info_footprint_window` are + every one of them a single fully transparent colour. 275 of the 282 files + under `pokedex/` are under 400 bytes. Every one of those records carries + `borrowedPalette` or `borrowedTiles`, which is the composer saying it could + not find the pairing — the same fault as the Poketch faces, one archive + along. **Fix the composition before writing the screen**: a Gen 4 Pokedex + drawn on this art would be a black screen where Kanto's at least shows + something. + + The same check on the art the four working screens DO use says they are + fine, which is what makes the contrast meaningful: `bag/bag_ui_main` has 25 + colours over 45% of its area, `summary/page_info` 16, `party/menu` 5, + `trainer_card/badge_case` 10 over the whole panel, and `title/logo` 255. + None of those is blank, so the module-list fix above puts real art on + screen rather than exchanging Kanto's for black. +* **The start menu is still the engine's** — `push StartMenu` in the log. The + two-style switch from the original brief is designed, not built. + +## Boot to main menu: the cartridge's own sequence, frame by frame + +Read out of `game_opening/ov77_021D25B0.c` and `applications/title_screen.c` +rather than from watching the game, so the numbers below are the cartridge's +and not an impression of them. This is the spec the port is built against; what +is built is marked. + +### 1. The opening cutscene — `gOpeningCutsceneAppTemplate` + +Three phases back to back, then a hold until frame **2430** (about 40.5 s at +60 fps), then it enqueues the title screen. **A or START at any point** sets +`unk_08`, clears `gSystem.showTitleScreenIntro`, fades both screens to black +and exits immediately — so a skipped opening also skips the title's own intro, +which is the detail that makes the two read as one sequence. + +It draws out of `/demo/title/op_demo.narc`: **116 members — 16 map models, +4 texture sets, 33 tile sheets, 21 palettes, 24 tilemaps, 9 cell banks and 9 +cell animations.** The models are a flythrough (`op_map01_00_00`, +`op_map02_00_00`, `titlemap05_20`, …), 500 to 2,100 triangles each, and they +carry no textures of their own — the four BTX0 members beside them are the +sets. **Not built. The archive is now extracted** (`MODEL_ARCHIVES` entry +`opening`), so the assets are in the cache and what is missing is the +camera track and the shot list. + +### 2. The title screen — `gTitleScreenAppTemplate` + +`TitleScreen_Main` is a seven-state machine: + +| state | what happens | +|---|---| +| `INIT_RESOURCES` | loads the gfx; branches on `showTitleScreenIntro` | +| `SHOW_INTRO` | the eleven-state intro below — only when the opening ran | +| `INIT_SOUND` | `Sound_SetSceneAndPlayBGM(SOUND_SCENE_TITLE_SCREEN, SEQ_TITLE01_sseq)` | +| `MAIN` | blinks the text; watches for input | +| `EXIT_NORMAL` | A/START was pressed | +| `EXIT_REPLAY_OPENING` | nothing was pressed for 900 frames | +| `CLEANUP` | releases the gfx | + +Coming **from** the opening, `showTitleScreenIntro` is true and the intro +plays. Coming **back** (from the main menu, or from the field's QUIT) it is +false, Giratina is already on screen, and **input is dead for 30 frames** +(`TITLE_SCREEN_INPUT_DISABLE_FRAMES`) so a held button cannot fall straight +through the title. + +In `MAIN`: + +* **A or START** → next app is the main menu; BGM fades over 60 frames; + **Giratina's cry plays** (`Sound_PlayPokemonCry(SPECIES_GIRATINA)`); a blur + effect is drawn; after `TITLE_SCREEN_EXIT_FADE_DELAY_FRAMES` (10) both + screens fade to **WHITE**, not black; the app waits for the cry to finish + before handing over. +* **B + UP + SELECT held** → the clear-save-file app. This is the erase + combination the NEW GAME warning text refers to, and the port does not have + it. +* **900 frames with no input** (`TITLE_SCREEN_REPLAY_OPENING_FRAMES`, 15 s) + → `showTitleScreenIntro = TRUE` and the opening replays. The title is an + attract loop, not a still. + +### 3. The title intro — eleven states + +`TitleScreen_ShowIntro`, in order: + +1. `FADE_FROM_BLACK` — Giratina's layer on, fade in from black over 15 frames + at step 3, then **hold 267 frames (~9 s) while the portal animation plays** +2. `WAIT_FOR_FADE` — counts that delay down, then arms **two white flashes** +3. `WAIT_AND_FADE_TO_WHITE` / `..._FROM_WHITE` — each flash is a 10-frame + brightness ramp to white and a 10-frame ramp back, twice +4. `RESET_COUNTER`, `WAIT_AND_FADE_TO_WHITE_2` — a third fade OUT to white + over 5 frames at step 2 +5. `WAIT_AND_FADE_MAIN_FROM_WHITE` — the logo's BG2 layer on, **Giratina's + animation starts**, main screen fades in from white over 16 at step 3 +6. `WAIT_FOR_DELAY` — Giratina marked shown, main screen to black, 10 frames +7. `FADE_MAIN_FROM_BLACK` — Giratina's BG layer on, main fades in from black + over **48 frames at step 1** — the slow reveal +8. `MOVE_IN_TITLE_CAMERA` — the title camera moves in over + `TITLE_CAM_MOVE_IN_FRAMES`, then the logo layer comes on, the top-screen + background loads, the SUB screen fades in from white over 16 at step 3, and + the copyright and logo BG layers come on + +Which is why the title is a *sequence* and not a picture: the logo and the +copyright are the LAST things to appear, after nine seconds of portal and a +camera move. + +### What the port has, and what it does not + +**Has.** The two panels and their measured content boxes, the logo fade +(`T_FADE`), the blinking prompt, and — as of this round — a footer band of the +title's own so PRESS START stops being drawn across the fourth copyright line. +That overlap was the visible fault in the last two screenshots: the comment +claimed the prompt sat "in the copyright strip where nothing else is", which +was true of the strip and false of the copyright, whose four lines are 56 rows +centred in it. Fourteen rows is exactly what the screen has spare — +122 (logo) + 56 (copyright) + 14 = 192 — so both panels keep every row they +need. + +**Built this round.** + +1. **Giratina.** `titledemo.narc` member 1 is `title_gira`: four shapes, 996 + triangles, its own TEX0, with a **121-frame joint animation** (BCA0) in + member 2. Loaded by name — the model and the animation sit in different + members and nothing but the name says they belong together, the same rule + the starter case is loaded under — and drawn into a depth target of its + own, because the UI surface has no depth buffer and 996 triangles without + one come out inside out. +2. **The camera, at both ends.** `Gen4Model.lookAt` is new: `orbit` is the + right shape for a turntable and the wrong one for a scripted shot, and + Platinum's title camera is two POINTS that interpolate — eye + (0, 192, 600) to (-64, 192, 484) over sixty frames, target fixed at + (0, 100, -18), FOV 15.996 degrees. Checked by mapping both eyes through the + matrix: each lands on the origin and its target on -Z at exactly the right + distance. +3. **The eleven-state intro**, as a timeline of the cartridge's own delays -- + 461 frames, 7.68 s. Every beat is a ramp between the picture and either + white or black, so the whole sequence drives one signed `veil` number + rather than a set of flags. Giratina's animation starts where + `GIRATINA_ANIM_STATE_PLAY` is set (frame 327), not at the top; the logo and + the copyright appear only when the camera move ends, which is what makes + the title a sequence rather than a picture. +4. **The layer order is the cartridge's**: the field is the backdrop, + Giratina draws into it, the logo goes over both -- + `ToggleGiratinaBgLayer` before the camera move and `ToggleLogoLayer` at the + end of it. +5. **The attract loop** -- 900 idle frames and the intro replays. +6. **The 30-frame input lockout** on a title arrived at from the menu, so a + held button cannot fall straight back through it. + +One deliberate departure, marked in the file: **the cartridge does not let you +out of the title intro at all** -- only the opening cutscene before it is +skippable -- and a window the player has just come back to is not an attract +cabinet, so A or START during the intro jumps to the end of it. + +**Still missing.** + +1. **The opening cutscene**, its camera track through the sixteen map models. + The archive is extracted (`MODEL_ARCHIVES` entry `opening`); the shot list + is not written. +2. **`op_ana` and `op_kao`** -- the portal (240 frames) and the face (174) are + extracted and not yet drawn, so the nine-second portal hold is currently + nine seconds of Giratina alone. +3. **The erase combination** -- B + UP + SELECT. Left for its own pass rather + than bolted on: it destroys a save file, and the cartridge's confirmation + screens are part of the feature rather than decoration around it. +4. **Giratina's cry and `SEQ_TITLE01_sseq`**, which need the audio stage that + does not exist for Gen 4 at all. + +## Boot to the bedroom: the whole chain, and what runs + +The app chain, read out of `game_start.c`, `main_menu.c` and +`applications/title_screen.c`: + +``` + opening cutscene gOpeningCutsceneAppTemplate NOT BUILT + | 3 phases, holds to frame 2430 (~40 s) + | A/START skips it AND clears showTitleScreenIntro + v + title screen gTitleScreenAppTemplate BUILT (intro, Giratina, + | 7 states, 11-state intro attract loop, lockout) + v + main menu CONTINUE / NEW GAME / OPTION BUILT + | NEW GAME + v + StartNewSave gGameStartRowanIntroAppTemplate BUILT (SaveData.newGame) + v + Rowan's intro gRowanIntroAppTemplate NOT BUILT -- 112 states + v + InitializeNewSave gGameStartNewSaveAppTemplate partly (no PlayTime start) + v + the field gFieldSystemNewGameTemplate BUILT -- the bedroom +``` + +### The 112 states of Rowan's intro + +`enum RowanIntroState`, in order, because "the intro is missing" is really +this list: + +1. **The TV**, with its CRT overlay drifting — `RowanIntroTv_*`, a sibling app + in the same folder, and the "tv screen" reported missing two rounds ago +2. fade from black; Rowan fades in and speaks +3. a choice box offering the **control info**; its pages, the X/Y icons, the + DS icon, and a yes/no that can repeat +4. the **adventure info** — six screens of text +5. "widely inhabited" +6. the **Poké Ball**: pushed in, four flashes, the Pokémon spawns, rises, + bounces down, and is put away — **built**; see *The release, frame by + frame* +7. "about yourself" +8. the **gender choice** — both avatars fading and centring, with a confirm — + **built**; see *The gender choice, and the four poses that were a run + cycle* +9. the player's **name**: dialogue, keyboard, confirm +10. "so you're" +11. the **rival**: tilemap swap, a name choice box with presets, keyboard, + confirm +12. Rowan's closing lines, then the **avatar shrink** that hands over to the + field + +`gen4_intro` is extracted and carries the backdrops, the figures, the rival +names, the control info and the adventure info. `Gen4RowanIntro` reads it and +plays the main line, the release (6) and the gender choice (8); the two +lectures (3, 4) are deliberately not offered, and the closing avatar shrink +(12) is not built. + +## The release, frame by frame + +Reported from play: *"in the intro rowan isnt thorwing out a pokemon like he +does in the rom"*. He does, and the port did not — the step was not in the +script table at all. Text id 17, `havePokeBall`, was extracted and never read. + +**It is a Buneary**, and that answer costs nothing to act on: +`RowanIntro_LoadBunearySprite` builds an ordinary `PokemonSpriteTemplate` for +`SPECIES_BUNEARY`, `FACE_FRONT`, so the picture is the species' own battle +front sprite — already in this cache since the species-sprite stage. Nothing +had to be extracted for the Pokémon itself. + +**The ball is three pictures, not one.** `/demo/intro/intro.narc` members 32, +33 and 34 are loaded into the same place in turn over one tilemap (40) and one +palette (41): that is the button being pushed in. The animation is not a +transform of the art — it *is* three pieces of art, which is why treating it as +one and moving it would have produced something that looked deliberate and was +wrong. Those three are now extracted as `intro/ball_0..2`. + +**The palette is the third row of member 41**, and nothing states it: the app +loads three rows of that member starting at background row 7 and then points +the tilemap's cells at row 9. So the ball's sixteen colours are member 41's +colours 32–47. Composing with its first sixteen gives a picture, and a wrong +one — this archive's standing hazard. + +**The flash is four brightness ramps.** +`BrightnessController_StartTransition(stepCount, target, start, …)` puts the +*target before the start*, which is exactly the argument order a reader assumes +and gets backwards. The calls are `(1, 16, 0)`, `(1, 0, 16)`, `(4, 16, 0)` and +then `(16, 0, 16)` as the Pokémon spawns: white in one frame, out in one, in +over four, and out over sixteen *while the Pokémon is already on screen*. Its +plane mask is BG0|BG1|BG3 and the Pokémon is on BG2 — so the flash deliberately +does not whiten it. Its palette is blended toward `0x6a3c` (BGR555 28, 17, 26 — +a pink-white) with weight `counter / 3` out of 16 counting down from 48, so it +is a solid silhouette for three frames and has its colours back after +forty-eight. + +**The arc is a parabola in integer arithmetic**, three phases of + +``` +y = base + coeff * 9 * t - floor(9 * t * t / divisor) +``` + +ending the first frame `y` comes back down through zero having been positive, +at which point the app *snaps* the offset to zero rather than letting the +integer arithmetic land where it will — which is why it finishes exactly on the +ground each time. A background offset is not a position: the hardware scrolls +the view, so a larger offset moves the picture **up**, and every reader is +`MON_Y - offset`. + +Reimplemented and checked against a trace of the C, the three phases come out +at 17, 9 and 9 frames, and the sprite's screen position walks + +``` +(82,176) (81,124) (80,86) (79,52) (78,23) (77,-2) … (72,-58) … (65,72) +(67,72) (69,48) (71,30) (73,18) (75,12) (77,12) (79,18) (81,30) (83,48) (83,72) +(79,72) (75,48) … (47,48) (47,72) +``` + +— rise, settle, hop right, hop left. Every number is the app's. + +**The port has one screen and the cartridge has two, and here that costs +something.** Before the arc, the app spends three frames moving a *second* copy +of the Pokémon up the **bottom** screen; the arc then plays on the top one, +starting from below its own screen. On hardware that reads as one continuous +rise across the gap. On one screen it is the same motion twice — the Pokémon +would leave upward and re-enter from below — so the port keeps only those three +frames' horizontal drift and plays the arc, which already begins off the +bottom. That is a port decision, stated rather than smuggled. + +The whole step, measured in a harness that runs the real file against stubbed +graphics: 11 frames of push-in, 6 of flash, 38 of arc, 40 of settle, then the +"they live alongside us" line, then a 16-frame fade and a 30-frame pause. Rowan +is never faded out for any of it — on hardware he is simply on the other screen +— so the ball covers him while it is pushed in and he is back underneath the +moment the Pokémon is out. + +The tint is two draw passes rather than one, because multiplying a sprite by a +colour can only darken it and the cartridge is *replacing* its palette: a +scaled-down normal pass plus an additive glow pass gives +`pixel * (1 - w) + glow * w` exactly, with the sprite's own alpha as the mask +both times. + +## The gender choice, and the four poses that were a run cycle + +Reported in the same message: *"the players sprites when selecting boy or girl +arent animated like they should be"*. + +`Gen4IntroScene` already extracted `boy_1..boy_4` and `girl_1..girl_4` and +described them as four poses of the same character. They are not poses. They +are a **four-frame run cycle**: `RowanIntro_AnimateAvatarRun` cycles the +selected side through intro members 9, 10, 11, 12 (boy) or 14, 15, 16, 17 +(girl), five frames each — and those are exactly the members the figure table +already names. **Nothing had to be added to the cache to play it; the data was +right and the reading of it was wrong.** + +The rest of the screen follows from the same function: + +* **Both avatars are on screen at once.** The boy's layer is offset −48 and the + girl's +48, and an offset moves the picture the other way, so the boy stands + 48 pixels *right* of centre and the girl 48 *left*. The old port showed one + at a time and called that the honest version of two buttons; it was a guess, + and the cartridge's answer was sitting in those two offsets. +* **Only the selected one moves.** The other holds whatever frame it is on and + is blended to 6/16 (`G2_SetBlendAlpha(…, 6, 10)`), which is what makes one of + them read as chosen. +* **On confirm the other fades out** over sixteen frames and the chosen one + slides back to the middle four pixels a frame — twelve frames from either + side — and then the confirm line and a YES/NO box. NO fades the chosen one + out and brings the pair back, which is why `{YESNO 0}` is stripped from the + text upstream: the box is drawn, not printed. + +Measured in the same harness: 32 frames of fade-in (one each), the run cycle at +`2,2,2,2,2,3,3,3,3,3,4,4,4,4,4,1,…`, 16 frames of fade-out, 12 of centring, and +the NO path returning to the start with the spread reset. + +Still not built from this list: the **control** and **adventure** lectures, +which are extracted and deliberately not offered — they describe a touch screen +and a +Control Pad the player is not holding — and the **closing avatar +shrink** that hands over to the field. + +**`CACHE_FORMAT` is now `rom-cache-v340`**, because the ball art is a new +extraction output: a Platinum cache has to be re-imported before the ball +appears. The release plays without it — the Pokémon is the species' own sprite +and does not come from that archive — so an old cache gets the sequence with +Rowan's backdrop where the ball should be, rather than nothing at all. + +## The world: the ground pipeline, measured end to end + +`TILESET_GEN4_STANDIN` is what the player sees, and the reason is not that the +data is missing. Every piece is in the cartridge and every piece reads. This +section is the measurement, chunk by chunk, so the renderer can be built +against numbers rather than against hope. + +### The chain, and what each link answers + +``` +map header --areaData--> area_data.narc --mapTexture--> map_tex_set.narc + | | + +--matrix--> map_matrix.narc --> land_data.narc member <-----+ + | | | + permissions NSBMD BDHC +``` + +**land_data.narc: 666 members, and all 666 carry both a mesh and a BDHC.** +Nothing is optional here, which the earlier note ("three of the four blocks +are routinely empty on indoor chunks") got wrong for the two that matter. + +**All 666 meshes decode.** `Gen4Nsbmd.parse` returns a model set, not a model +— the chunk's mesh is `set.models[1]` — and chunk 0 comes out as 19 shapes +against 19 materials whose texture names (`conttree_b`, `hage`, `imped`) are +real. Across the archive that is **7,547 shapes**, 13.6 MB of NSBMD. + +**All 666 pack and unpack unchanged.** `Gen4ModelPack.pack` followed by +`verify` — the same round trip the starter models go through — is **0 failures +in 666**, 17.7 MB of packed geometry, 7.9 seconds to read and pack the lot. So +the geometry can go into the cache in the form the renderer already draws. + +**A chunk's local origin is its CENTRE.** `posScale` is 32 and chunk 235's +vertices run x = −8..5, so world units are `value * posScale` and the chunk +spans −256..+256 of its 512-unit square. Reading it as 0..512 puts seven +eighths of the mesh off the edge — 236 triangles of 2,667 landed before this +was fixed, which is the sort of error that looks like a broken decoder and is +arithmetic. + +**All 3,130 map textures decode**, which they did not before: 198 A3I5 and 40 +A5I3 — 7.6% — were being skipped, and they are the shadows, the water edges +and the cloud layers, so the missing tenth was the tenth you notice. Both are +one byte per pixel split between a palette index and an alpha (five bits of +index under three of alpha; three under five), added as +`Gen4Models.ALPHA_FORMATS`. The proof they are right is that the alpha is +PARTIAL: `area4_gate_b` comes out with 176 semi-transparent pixels and `elev_e` +with 217, where a wrong split gives all-or-nothing. + +**And it draws.** Chunk 235 with texture set 28, rasterised top-down at one +pixel per world unit with a height buffer, is a Battle Frontier interior: +Poké Ball arena floors, the rope runs, the benches, the sand outside. That is +Platinum's own ground art, from the cartridge, in one 512×512 image. Nothing +in the pipeline is unproven any more. + +### Which texture set a chunk wears: settled, and it is exact + +A chunk's mesh names its textures; it does not say which of the 74 sets to +look them up in. The header does — `areaData` → `area_data.narc` (75 records, +8 bytes) → `mapTexture`. + +Asked the wrong way first, and the wrong way is worth recording because the +number it produced looked like a real gap. Taking every map header, then every +cell of its whole matrix, and asking whether the header's set holds that +chunk's texture names gives **32,307 of 40,333 pairs, 80.1%** — and the +failures all cluster on the Sinnoh overworld, one header with a 30×30 matrix +whose single area record obviously cannot cover 900 chunks. The question was +wrong: that header does not OWN most of those cells. + +**A matrix says who owns each cell.** `Gen4Maps.extents` already reads the +`headers` block for its bounding boxes — one header id per cell, zero where +nothing claims it — so the right question is per cell, of its owner. Asked +that way: + +* **175 of 175 owned cells resolve. 100%.** +* 745 cells name no owner, and those are textured by whichever map the player + is standing in, which is the answer the cartridge gives too. + +So there is no attribution gap. The 80.1% was an artefact of asking every +header about cells it does not own, and the figure quoted before that +(729 of 867) was a third measurement again; none of the three were comparable +and only this one asks the cartridge's own question. + +### The stage, built + +`src/import/Gen4Terrain.lua` and a `terrain` stage, measured on the cartridge: + +| | | +|---|---| +| chunks packed and verified | **666 of 666, 0 refused**, 7,547 shapes | +| packed geometry | 17.7 MB, 8.5 s | +| BDHC heights | 301 KB, in the cartridge's own form | +| texture atlases | **74 of 74, 0 textures undecoded**, 16.7 MB raw, 1.2 s | +| matrices gridded | 289, 1,604 chunk cells | + +Three decisions worth stating. + +**The geometry is a side-car binary.** 17.7 MB of packed vertices and indices +is nearer 50 MB as escaped Lua source and would add minutes to every cache +load. `CacheFs.write` takes raw bytes — it is what every PNG already goes +through — so `terrain/chunks.bin` and `terrain/heights.bin` are files and +`gen4_terrain.lua` is an index of offsets into them. +`RomExtractorGen4:saveBinary` is the second half of `ImageWriter.save` with the +encoder taken out. + +**The atlases are trimmed to what they hold.** Every set packs into the same +512-wide sheet, tallest texture first with ties broken by NAME so a rebuild +from the same cartridge produces the same file — then the empty shelves below +the last one are cut off. The smallest set is 36 textures on two shelves; a +full-height sheet for it was ten times the pixels for nothing, and trimming +took the 74 sets from 74 MB to 16.7 MB. + +**No pictures are baked at import.** A chunk is drawn top-down into a canvas at +run time, once, the way `Gen3Tiles` already bakes its metatile sheets — so the +cache carries the mesh and the textures rather than 666 rendered images that +would have to be re-rendered the first time the camera moved anyway. + +`CACHE_FORMAT` is `rom-cache-v339`, because the terrain's files are new but +live under `assets/` and behind a new module, so the missing-file gate cannot +see them either. + +### The renderer, built + +`src/render/Gen4Ground.lua`, and one guarded branch at the top of +`TileRenderer:drawWindow`. Nil for every other generation and for a Platinum +cache imported before the terrain stage, so everything below that branch is +untouched and still runs when the ground is absent. + +**A chunk is baked once, top-down, into a 512×512 canvas**, and the canvas is +what the map draws. That is not a shortcut around a 3D view: it is what +`Gen3Tiles` already does with its metatile sheets, for the same reason — the +geometry does not change, so rasterising it every frame is work repeated to +arrive at the same picture. When a real camera exists the meshes are already +here and this becomes the flat case of it. + +**One pixel per world unit**, which is what makes the rest line up: a tile is +16 units and the engine draws tiles at 16 pixels, so the collision grid, the +warps and the sprites land on the ground with no second scale to keep in step. + +**The projection is orthographic and checked**: `topDown(256)` maps the +chunk's north-west corner to clip (−1, +1) and its south-east to (+1, −1) with +w = 1 throughout, and ground 84 units up comes out at clip z −0.0205 against +the floor's 0 — so with the depth test on `less` the hill wins, which is the +whole reason the bake needs a depth buffer. + +**No nodes and no pose, and that is measured.** A model's shapes are placed by +the matrix slot each is bound to, and across all 666 chunks — **7,547 shapes — +every one is bound to slot 0**. Thirty-three chunks do carry more than one node +and 207 node matrices are not the identity, so the nodes are not absent; +nothing places a shape with them. The vertices are already in the chunk's own +space. + +**The store is read in ranges, not whole.** `File:seek` plus `File:read` is the +difference between 17.7 MB resident for every Platinum session and the few +hundred kilobytes a map actually touches. Sixteen baked chunks are kept, oldest +evicted first — a player walks in one direction, so the chunk longest unused is +behind them — and at most one chunk is baked per frame, because baking four at +once on a map entry is a visible hitch. + +**An atlas was tried and rejected**, which is why the stage writes 3,130 +individual texture PNGs (12 MB raw, 0 undecoded) rather than 74 sheets. A map +texture is TILED: a chunk's UVs run from about −196 to +228 texels on a +64-texel picture, the same grass laid down seven times. That needs a sampler +set to repeat, and a sub-rectangle of an atlas cannot repeat — the wrap applies +to the whole sheet, so every tiled face would smear its neighbours across +itself. `Gen4Model.new` already loads a shape's texture by path and already +sets `repeat` on it, so the honest version costs a file count and no code. + +### The height under the ground, read + +`Gen4Ground:heightsAt(tileX, tileY)` -> a list of world heights, highest +first, in the same map tile coordinates collision and the events use. A list +rather than a number, and the measurement is why. + +Over all **681,984 tile centres in the cartridge**, with all 666 BDHCs parsed +(0 refused, 8,974 plates between them): + +* **76.4%** have a plate under them +* **0.48% have more than one, up to four deep** — the bridge case, and the + reason a single number would be wrong +* about **14% of walkable tiles have none**, and that is not a gap: a tile with + no plate is flat ground at the chunk's own base + +**It walks every plate rather than using the scanline index, and that is a +correction.** The BDHC carries a strip index for finding plates quickly. +Checked against brute force over 98,304 tiles it agrees on **99.59%** and +**drops a plate on 404 of them — never the other way round**. Thirteen plates +per chunk is nothing to walk, and a height that is silently absent four times +in a thousand is a player falling through a bridge. The index stays in +`Gen4Bdhc` (`heightsVia`) and is not what the ground asks. + +The two-table check that did NOT come out clean is worth recording as well: the +permission grid's blocked bit and "has a plate" agree on only 51% of tiles. +That is the right answer rather than a fault — 37% are blocked *and* have a +height, which is exactly what scenery you can see and not walk on looks like, +and the plates cover the mesh rather than the walkable area — but it means the +permission grid cannot be used to verify the height field, and a check that +looked like one would have been worse than none. + +### The bit this project had been calling "void" + +That paragraph used to say *void bit*, and so did `Gen4Maps`, and both were +wrong about what the bit means. The behaviour was right by luck — "not part of +the map" and "you cannot stand here" both make a tile impassable — but the +**name** sent every later reader looking for a wall table that does not need to +exist. It is why this file carried a standing caveat reading *"which of the 54 +behaviour values are walls is NOT established ... a player dropped into one of +these maps would walk through fences."* **That caveat was false.** Fences carry +the bit and already block. + +`TERRAIN_ATTRIBUTES_COLLISION_MASK` is `0x8000` and +`TerrainCollisionManager_CheckCollision` reads that bit and nothing else. Then +measured against this cartridge's own 681,984 tiles, three ways that cannot +borrow from each other: + +* **335,165 tiles have it set — 49.1% of Sinnoh.** Half a region is not + missing. That is walls, cliffs, building footprints and sea. +* **The per-chunk share is spread across every decile** — 88 chunks under 10%, + 107 at 10–20%, 101 above 90%, and every band in between occupied. "Off the + map" would be bimodal: a chunk is either land or it is not. Per-tile + collision is not. +* **26 behaviour values occur both blocked and open.** A tile that is not part + of the map does not also carry a behaviour that is walkable elsewhere, so the + bit is an independent fact about the tile rather than a consequence of what + the tile is. + +And the rest of the high byte carries nothing: **bits 8–14 are clear on every +one of the 681,984 tiles**, and the highest word in the cartridge is `0x80E5`. +So a permission word is exactly a collision bit and a behaviour byte, and the +behaviour byte says what a tile *is* — grass, water, a doorway — not whether +you may stand on it. There are **94** distinct behaviour values in this +cartridge, not 54. + +`Gen4Maps.COLLISION` and `Gen4Maps.blocks` are the names now; `VOID` and +`isVoid` remain as aliases so nothing that reads them breaks. **No cache output +changed** — the map def's bytes are identical — so this needs no re-import; it +closes an open item and removes a false one. + +The elevation field is still zero, and that one is a real gap: the BDHC is +parsed (`Gen4Ground:heightsAt`) and nothing writes it into the grid, so a +bridge and the path under it are one cell. + +### The white overworld, and the two numbers behind it + +Reported from play, with a screenshot: *"the overwolrd still isnt right its +appearing as white mostly with some weird tiles in one area"*. One picture, two +independent faults, and neither of them was in the geometry — the chunks that +did draw drew correctly. + +**The white was an early return.** `TileRenderer:drawWindow` opened with + +```lua +if self.gen4Ground and self.gen4Ground:draw(camX, camY, vw, vh) then + return +end +``` + +The intent was reasonable: the Gen 4 ground IS the map's picture, so there is +no point building the stand-in tile batch underneath it. The flaw is in the +word **any**. `Gen4Ground:draw` returns true when it drew *something*, and a +screen holds up to four chunks while `BAKES_PER_FRAME` was **one** — so on a +map entry the first chunk baked, reported success, and the early return +cancelled the stand-in for the other three, which had nothing of their own yet. +The result is exactly the screenshot: one region of real ground, and white +everywhere else, because white is what is left when nothing at all is drawn. + +The fix is to stop treating the two as alternatives. The stand-in costs one +batch draw and is already built; it now draws first and the real ground draws +over whatever is ready: + +```lua +self:drawAnimated(camX, camY) +if self.gen4Ground then self.gen4Ground:draw(camX, camY, vw, vh) end +``` + +A floor under the picture rather than a replacement for it, and the screen can +no longer be empty. `BAKES_PER_FRAME` went 1 → **4** as well, so the visible +set is ready after one frame instead of four: a map entry pays one hitch and +then nothing. + +**The striped region was z-fighting, and it was a precision choice made +carelessly.** `DEPTH_RANGE = 4096` was a first guess picked for headroom. It +maps the whole height field into the depth buffer, and Platinum's heights span +tens of units, not thousands — so nearly every surface in a chunk landed in a +sliver at one end of the range. Two near-coplanar surfaces (a floor and the mat +on it) came out a couple of millionths apart in clip space, which is inside the +depth buffer's resolution, and a depth buffer that cannot separate two surfaces +shows alternating rows of each. That is the striped teal. + +`DEPTH_RANGE` is now **1024** — still far past anything the cartridge has +(Mount Coronet is hundreds), with four times the resolution to tell two floors +apart. **Headroom taken "just in case" is not free; in a fixed-point buffer it +is paid for in precision you were using.** + +### What is left + +1. **Wiring the height into movement.** The query is built and nothing calls + it: the player still walks a flat plane. That is the engine's elevation + concept, not the extractor's, and it needs the "which surface am I on" + choice the multi-plate tiles exist for. +2. **The field animations** — and what they animate is not what this file used + to say. See *What `bm_anime` actually animates* below. +3. **Buildings — built.** See *The houses on the ground* below. + +## The Pokédex, and the composition that could not be expressed + +The last screen on the reported fault list — *"neither is the pokedex, or the +pokemon party menu or the bag theyre looking like gen1 still"*. The other two +were fixed by the module list. This one was left alone on purpose, and the note +saying so was right: **the art composed blank**, and a Gen 4 Pokédex drawn on it +would have been a black screen where Kanto's at least showed something. + +### Why it was blank + +Not a decoder bug. `zukan.narc` does not name its parts to match. The palettes, +the tile sheets and the tilemaps are three separate name spaces, and a handful +of sheets and palettes serve dozens of tilemaps: **`info_main.NSCR` has no +`info_main.NCGR` and no `info_main.NCLR` anywhere in the archive.** Grouping by +base name — which is exactly right for every other screen archive — gives it a +tilemap with no tiles and no colours, so it borrows the archive's first of each +and paints nothing. Hence 275 of 282 pictures under 400 bytes, every one +carrying `borrowedTiles` or `borrowedPalette`. + +And the entry page is not one tilemap. It is **four**, laid into one 32×24 grid. +`ov21_021E96A8` is the whole of it in a single function: + +``` +Graphics_LoadPaletteFromOpenNARC(narc, banner_sinnoh_NCLR, 0, 0, 0) +Graphics_LoadTilesToBgLayerFromOpenNARC(narc, entry_main_NCGR_lz, bg, 3) +info_main.NSCR -> rect (0, 0) +info_species_window.NSCR -> rect (0, 3) +info_footprint_window.NSCR -> rect (12, 8) +info_entry_window.NSCR -> rect (0, 16) +``` + +**The palette is `banner_sinnoh.NCLR`** — the entry page's colours are filed +under the banner, which no amount of looking at names would have produced. +`info.NCLR` exists and is a *sprite* palette for the page buttons. Pairing +`info_main` with `info` would give a picture, and a wrong one. + +### Checked three ways + +A wrong member index in an archive like this still decodes, so the pairing is +checked against three tables that cannot borrow from each other: + +1. `res/graphics/pokedex/pokedex.order`, the file pokeplatinum's build feeds its + archiver, gives the indices: 6, 24, 33, 50, 51, 52, 54, 57. +2. The name table this port already carries in `Gen4Archives` agrees with every + one of them, index for index. +3. The **rect sizes** are checked against the widths and heights the graphics + stage already recorded for those five members: `info_main` 32×24 at (0,0), + `info_species_window` 12×12 at (0,3), `info_footprint_window` 6×6 at (12,8), + `info_entry_window` 32×8 at (0,16) — which ends exactly on the bottom row. + Every rect fits inside the screen and the two full-width ones *are* the + screen. + +Members are looked up **by name** at extraction time; the indices are recorded +so a reader can check the claim rather than take it. + +### `Gen4Graphics.stamp`, and what it says about the planner + +The missing capability was one function: lay one tilemap into another at a tile +offset, which is `Bg_LoadToTilemapRect`. Cells carry their own tile index, flip +bits and sub-palette, so a stamped grid composes exactly like any other tilemap. +Anything that would land outside the base is **dropped rather than wrapped** — a +rect that does not fit is a wrong offset, and wrapping would hide it. Verified +against a hand-computed coverage: 332 + 144 + 36 + 256 = 768 cells, no overlap +unaccounted for, and an out-of-bounds stamp clipped rather than wrapped. + +The new `dex` stage does not replace the graphics stage. It adds the +composition the planner cannot express and leaves the per-member pictures alone, +so nothing that reads them today changes. + +### Height, weight and category are STRINGS + +The species table carries none of the three, and the art has slots for all +three. Platinum does not store them as numbers: height, weight and the category +line are one **pre-formatted string per species**, which is why `infomain.c` +renders them with a `MessageLoader` and no formatting of its own. Banks 709, +707 and 711; the "HT" and "WT" labels are entries 9 and 10 of bank 697. + +Those bank ids come from pokeplatinum's `generated/text_banks.txt`, whose +zero-based line numbers *are* the cartridge's bank ids — checked against every +bank this project had already found by looking: 202 nature, 391/392 item, +619/620 trainer class, 646/647/648 move, and 706, which that list names +`TEXT_BANK_SPECIES_POKEDEX_ENTRY_EN`. **Nine out of nine, so the tenth is not a +guess.** + +### The screen + +`Gen4Pokedex`, with the `PokedexMenu` alias that had never existed. Two pages: +the list, and the entry that A opens. Every position on the entry page is a +literal out of `infomain.c` — the sprite at (48, 72), the name and number at +(172, 32), the category box's text at (114, 44), HT at (152, 88) with its value +at (184, 88), WT at (152, 104)/(184, 104), and the entry text centred on x = 128 +at y = 136, dropping to x = 8 when it is wider than 240, which is the +cartridge's own overflow rule rather than a clamp invented here. + +**The list page is this port's**, and says so: `scroll_main_background` and the +scroll wheel are a third composition again — a wheel of sprites over a scrolling +background — and are not composed. So is the **Sinnoh dex order** +(`/poketool/pl_pokezukan.narc`, not extracted), which is why the listing is +national order only; for Gen 4 that needs no sort at all, since Platinum numbers +its species 1..493 in national order and the species id *is* the number. + +`CACHE_FORMAT` is now **`rom-cache-v342`**. + +## The keyboard, part two: the archive nobody had opened + +The section below fixed the alignment. It did not fix the screen, and the +reason is worth stating on its own: **every number in it was invented.** The +grid was 13×6 on 16-pixel square cells at (24, 64) because that fitted; the +boxes were the engine's; the home row was inside the grid. All of it was +self-consistent and none of it was Platinum's, because `/data/namein.narc` had +never been opened. + +It is open now, as `Gen4Naming` plus a `naming` extraction stage, and the +screen is rebuilt on what it says. + +**The archive has no name table**, like the intro's, so every member index is +arithmetic — and arithmetic is what this project has been burned by most. None +of it is inferred. The order comes from +`res/graphics/naming_screen/naming_screen.order`, the file pokeplatinum's build +feeds its archiver, which reproduces this cartridge byte for byte; every index +is then cross-checked against the symbol `NamingScreen_LoadGraphicsFromNarc` +uses for it. **Two independent statements of the same fact** is the only kind +of check worth having in an archive where a wrong index still decodes. + +| member | what | +|---|---| +| 0 | `naming_screen.NCLR` — the background palette | +| 2 | `naming_screen_main_tiles.NCGR` — tiles for both backgrounds | +| 4 | `naming_screen_bg.NSCR` — the full-screen backdrop | +| 6–9 | `naming_screen_chars_bg_0..3.NSCR` — one keyboard panel per page | +| 10, 12, 14 | the sprite sheet, cell bank and animations | + +### What the cartridge actually does, against what the port had + +* **The panel's position is a background offset, not a coordinate.** + `NamingScreen_InitializeCharsPosition` parks the active layer at (−11, −80). + The hardware scrolls the *view*, so −11 puts the picture eleven pixels to the + **right**: the panel sits at screen (11, 80). +* **The character grid is 13 × 5, not 13 × 6, in cells 16 × 19, not 16 × 16.** + `Window_Add(…, BG_LAYER_MAIN_1, 2, 1, 26, 12, 1, …)` gives the rectangle — + tile (2, 1), 26 × 12 tiles, palette row 1 — and + `NamingScreen_InitializeCharsGraphics` gives the pitch: **five** + `PrintChars` calls at `i * 19 + 4`, spacing 16. Twenty-six tiles is 208, + which is thirteen columns of sixteen; twelve tiles is 96, which is five rows + of nineteen with a pixel over. So the grid's origin is (27, 88) and nothing + about it had to be chosen. +* **The home row is not in that window at all.** It is six sprites at + `y = 0x44` and `x = 4, 36, 68, 101, 136, 176`. Six buttons — which is the six + the navigation model already had, and their spacing *confirms* that model + rather than merely being consistent with it: laid out from x = 4 on the + keyboard's own 16-pixel pitch at spans 2, 2, 2, 2, 3, 2, they would start at + 4, 36, 68, 100, 132, 180. Against the cartridge's anchors that is exact on + three and within 1, 4 and 4 pixels on the rest — the difference being that a + sprite's x is its art's anchor, not its cell's edge. +* **The keyboard is a checkerboard, and it is painted by code.** Two palette + indices per page — `sCharsBgColor = {4, 7, 13, 10}` and + `sCharsAltBgColor = {3, 6, 12, 9, 9}` — with 16×19 rectangles over the odd + columns of rows 0, 2, 4 and the even columns of rows 1, 3. **Extracting the + pictures alone would have produced a frame with nothing inside it**, which is + exactly the kind of half-answer that looks finished. The stage therefore + carries the window's palette row out of the cache as sixteen colours, because + the two the checkerboard uses are indices into it and nothing else in the + cache can resolve them. + +### The consequence for the section below + +The nineteen-pixel row is why the "every number is a multiple of eight" fix +could only ever be a patch on a guess. `Font.drawBox` takes tiles and the +cartridge's rows are not a whole number of tiles, so the choice was between +rounding the layout to suit the drawing or drawing in pixels. **Rounding the +layout to suit the drawing is what made it wrong in the first place**, so the +tile boxes are gone: the frames are drawn in pixels and nothing rounds. + +### Still not the cartridge's + +The home row's **button art** and the **cursor**, both in the sprite cell bank +at members 10/12/14. Composing a cell bank is a different job from composing a +tilemap, and until it is done the buttons are the engine's frames at the +cartridge's own positions — with short words on them (`A-Z`, `a-z`, `SYM`, +`SPC`, `BACK`, `OK`) that are the port's, because the cartridge's buttons are +wordless pictures and something has to be written. They are short because a +label that does not fit its own button is the fault this screen was reported +for. + +The **prompt and the typed name** are the bottom screen's on hardware — the +keyboard is the top screen and `LoadMessageBoxGraphics` puts the message box on +`BG_LAYER_SUB_0`. With one screen they go above the keyboard, and those two +positions are the only ones on this screen that are still the port's. + +`CACHE_FORMAT` is now **`rom-cache-v341`**. + +## The keyboard, and boxes measured in two different units + +Reported from play: *"the keyboard for gen3 still needs a lot of work before it +matches the platinum rom text is outside of boxes etc"*. (Gen 3 in the report; +the screen is `Gen4NamingScreen`, reached from the Gen 4 new-game flow.) + +The layout was right — six rows of thirteen, the home row's repeated buttons, +the three English pages, all read out of `naming_screen.c`. What was wrong is +that it was expressed in **two units at once**. `Font.drawBox` takes TILE +coordinates; `Font.draw` takes PIXELS. The grid was laid out on 20-pixel rows +starting at y = 62, so every box was rounded to a tile and every label was not: + +```lua +Font.drawBox(math.floor(y / 8), ...) -- 62 / 8 -> 7, i.e. 56 px +Font.draw(label, x + 2, y + 4) -- 66 px +``` + +Ten pixels of drift on the home row, and more further down, because the error +compounds with every 20-pixel row against an 8-pixel grid. The `math.floor` +calls are what made it look deliberate; they were rounding away the evidence. + +**The fix is not to nudge the text.** A box can only land on a multiple of +eight, so the layout has to be made of multiples of eight — then both units +agree and no rounding happens at all: + +| | was | now | +|---|---|---| +| `GRID_Y` | 62 | **64** (tile 8) | +| `CELL_H` | 20 | **16** (2 tiles) | +| `TITLE_Y` | 10 | **8** | +| `ENTRY_Y` | 34 | **32** (tile 4) | + +Thirteen 16-pixel columns from x = 24 is tiles 3 → 29 of 32; the home row is +tiles 8 → 10 and the five character rows are tiles 10 → 20 of 24. Every +`math.floor` in the drawing code is gone, because there is nothing left to +round. A `GLYPH_INSET` of 3 centres a 12-pixel glyph in a 16-pixel row. + +Three smaller things came out of the same read: + +* **The selection was a `">"` prefix** on the label, which pushed every home-row + word one glyph right — out of its own button, and on the longest labels out + of the screen's share of it entirely. So the marker that was supposed to show + where you are was itself putting text outside the boxes. It is a **highlight + drawn behind the cell** now, and the label no longer moves. Labels are also + clipped to their own button's width rather than running into the next one. +* **The character cursor drew on top of the letter it was pointing at** — + `Font.drawCode(Theme.cursor, x, y)` in the same cell as the glyph. Same + highlight, drawn first, glyph second. +* **The five character rows had no panel behind them**, so the letters floated + on the screen's background. The cartridge draws a slab with letters on it; + there is one `Font.drawBox` behind all five rows now, not sixty-five little + windows. + +Still not the cartridge's: the **art**. `/data/namein.narc` is not extracted +(task #103), so the frame is this port's own Gen 4 window. The layout and the +characters are Platinum's; the pictures are not, and that stays said plainly +rather than quietly passing. + +## The houses on the ground + +A chunk's mesh is its **floor**, and the check that established that is the one +that also made this necessary: **9,497 walkable tiles have no land-mesh triangle +beneath them at all**, because indoor chunks are a shell and their floors are +building models. So Sinnoh drawn from the chunks alone is Sinnoh with no houses +in it — which is what the ground pipeline shipped. + +Three small pieces, and none of them needed new decoding. + +**The models were already readable.** `/fielddata/build_model/build_model.narc` +is 590 models, all 590 decoding exactly — 1,362 shapes, 89,253 vertices — and +**568 of them carry their own textures**. That last number is why this is an +archive added to `MODEL_ARCHIVES` rather than a stage of its own: for 568 of +590 the picture is inside the model file, and the stage that already reads a +model's own TEX0 reads it without a new line. The other 22 come out untextured +and are drawn that way, because **a wrong picture on a building looks +deliberate and a flat one does not.** + +**The placements were already parsed and thrown away.** A land chunk's second +block is its object list, and `Gen4Maps.objects` has decoded it — model index, +position and scale, all out of 20.12 fixed point — since the map work. Nothing +carried it into the cache, so the renderer had the ground and no idea what +stood on it. It rides in the chunk index rather than the geometry blob: a +handful per chunk at six numbers each, where a side-car offset would cost more +to read than the numbers. + +**The projection needed nothing reconciled.** `Gen4Maps.objects` has already +divided the fixed point out, and `Gen4Model` has already multiplied the model's +own `posScale` into its vertices — so both sides are in world units and the +placement is an ordinary scale-then-translate composed onto the same top-down +matrix the floor uses. Checked numerically before it was committed: a building +at the chunk's centre lands at (256, 256) of the 512-pixel canvas, one 100 +units east and 50 north at (356, 306), one at the far corner at (512, 0). + +Two things that would each have been a silent wrong picture: + +* **A scale of zero is not a scale.** Some records leave the three scale fields + empty. Read as zero they collapse the model to a point, which draws nothing + and looks exactly like a building that failed to load. Absent means one. +* **The buildings bake into the floor's own depth buffer**, not over the + finished picture. That is what keeps a house drawn after the ground but + *below* it — a basement, the underside of a bridge — correctly hidden; + painting in order would not manage it. + +They bake with the floor rather than drawing every frame, for the reason the +floor does: the geometry does not change, and a town with forty houses would +otherwise be forty model draws a frame to arrive at the picture that was +already there. + +**`CACHE_FORMAT` is now `rom-cache-v344`** — the terrain index gains its object +lists and there is a new model set, so a Platinum cache has to be re-imported +before a single house appears. + +## What `bm_anime` actually animates + +This file has said, in several places, that `bm_anime.narc`'s animations "are +what make Platinum's water move, and a baked chunk is still by definition", and +that re-baking a chunk on the animation clock was the shape of the answer. That +is close enough to sound right and wrong about the mechanism — which is the +kind of wrong that sends the next person to rebuild the terrain renderer. + +**They animate the props standing on the ground, not the ground.** Measured +against this cartridge, of the 95 animations in `bm_anime`: + +| | | +|---|---| +| name a **build model**, by the model's own name | **68** | +| name a texture in the area building texture sets | 3 | +| name any material, shape or texture of a **land chunk** | **0** | + +Zero. Not few — none. And the names say it out loud once you read them rather +than counting them: `door_op`, `pc_door_op`, `stair_pc_u01d`, `funsui` (a +fountain), `machine_l02`, `treeeff01`. Doors opening, fountains running, tree +tops moving, PC doors, stairs. Some of those props *are* water — `l_lake`, +`wfall`, `r04_w` — which is exactly why the water intuition was nearly right +and its mechanism was not: the lake is a model standing on the chunk, not a +patch of the chunk's own mesh. + +This lands well, because the buildings were wired in one section ago and these +are the same objects. The two archives are separate, so the models stage's +per-archive animation pairing cannot see across them; there is now **one +cross-archive pass** that links each build model to its animations by name, so +nothing at run time searches 95 animations per building per frame. + +### The scrolls play + +Both halves are built, and the units were measured rather than assumed. + +**The units.** `Gen4Anim` decodes a BTA0 into per-frame scale, rotation and +translate channels, and nothing in pret says what a translate of `1.0` means — +the application lives inside NitroSDK, which is not in the decompilation. The +*data* says it outright: + +``` +funsui 16 frames tT 0.0000 .. -1.0000 scale const 1.0 +r04_w 121 frames tS and tT 0.0000 .. -1.0000 +wfall 61 frames tT 0.0000 .. -2.0000 +l_lake 61 frames tS and tT 0.0000 .. -1.0000 +machine_l04 60 frames tS -0.0166 .. -1.0000 +``` + +Every scroll runs to **exactly** −1.0 over its own frame count, and the +waterfall to −2.0, which is two cycles in the same time. A translate landing on +whole units is one full wrap of the texture; texel units would have run to 16 +or 64. This port's UVs are already divided by the texture's size when the mesh +is built, so the transform applies with nothing to convert. + +**The shader** takes a 2×2 and an offset now, alongside the MVP. Sent on +*every* shape, including the overwhelming majority that are identity — a +declared uniform that is not sent reads as zero, and a zero texture matrix +collapses every coordinate onto one texel, which is a model drawn in a single +flat colour. `Gen4Model` was also dropping each shape's **material name**, +which is what a texture animation names; nothing could have driven one even +once the animation was decoded. + +**The split** puts the moving props on a second canvas per chunk, rebuilt when +the clock moves; the static floor and the static buildings stay baked. Two +details in it are the difference between working and nearly working: + +* **The terrain is drawn into that canvas with the colour mask off**, filling + the depth buffer without painting anything. That is what keeps a lake behind + a cliff behind it — the canvas comes out transparent everywhere the props are + not and correctly occluded everywhere they are. Props drawn alone would have + floated in front of the terrain above them. +* **That chunk's terrain mesh is held**, not rebuilt. `modelFor` reads geometry + out of the side-car file and builds a LÖVE mesh per shape; calling it once a + frame for every chunk with a fountain on it would cost more than the + animation it pays for. Only chunks that have moving props hold one. + +**The clock is deliberately not wrapped.** The obvious wrap is a round number, +and this archive's periods — 16, 20, 21, 25, 60, 61, 91, 121 among them — do +not all divide into any of them, so a wrap would jump the phase of every +animation coprime to it. A Lua number counts frames exactly past any session +anyone will play. + +### The flipbooks play too + +BTP0 was the open half: its keys name a texture by NAME out of the animation's +own list, and where that name resolved to was not established. It is now, and +the answer is as tidy as it could be — **of the 16 build models carrying a +BTP0, all 16 have every one of that animation's texture names in their OWN +TEX0.** No misses, and not one of them lacking a TEX0. The alternate frames sit +inside the model file beside the picture they replace. + +Which exposed why they could not have been played anyway: **the models stage +writes the texture each SHAPE references**, and for an animated material that +is frame zero and nothing else. A door's other three pictures were decoded, +named, and never written to disk. They are now, under the same +`/` key as everything else, and recorded on the model as +`patternImages`. + +Two details that decide whether a flipbook runs right or merely runs: + +* **The keys are sparse, and key N is not frame N.** `c1_s02` holds frame 0 for + ten frames, then the next for twenty, then thirteen, then nine. Indexing the + key list by the frame counter would run every flipbook at the wrong speed and + the wrong rhythm. The lookup walks to the last key at or before the frame. +* **A material can carry both a scroll and a flipbook**, so the evaluator + merges into the material's entry rather than assigning a fresh table — + otherwise whichever was evaluated first is silently dropped. + +A BTP0 counts as animated only when the model actually carries the pictures its +keys name. An older cache has the animation and not the frames, and re-baking a +chunk every frame to redraw an unchanging door is pure cost. + +`CACHE_FORMAT` is now **`rom-cache-v346`** — the flipbook frames are new files +and `patternImages` is a new field, so doors need a re-import. The scrolls do +not; they were three renderer files. + +`CACHE_FORMAT` is now **`rom-cache-v345`** — build models carry their +animations. + +## Sinnoh makes a sound + +Platinum's audio is `/data/sound/pl_sound_data.sdat` — 7.9 MB of SDAT: SYMB +names, INFO records, FAT, and one FILE block holding everything. Most of it is +SSEQ sequences over SBNK banks over SWAR wave archives, which is a +**synthesiser** and not a decoder. That is the music, and it is not this. + +**The cries are not that**, and the gap between the two is what makes this a +stage rather than a project. Measured across the cartridge: + +| | | +|---|---| +| species whose bank points at wave archive **index == species id** | **493 of 493** | +| those archives holding exactly one sample | **493** | +| those samples that are **PCM8** | **493** | +| sample rates | 10512 Hz (388), 13379 Hz (105) | + +No ADPCM, no multi-sample instruments, no sequencing. A cry is one 8-bit +recording, and 493 of them come to 4.6 MB of WAV. + +### The trap, which was a good one + +The wave archives are **named** `WAVE_ARC_PV001`..`PV518`, 493 present. Read +the numbers out of those names and 387–411 are missing while 494–518 are +spare — which looks *exactly* like a block of 25 species relocated by +107, and +is a coherent enough story that I wrote it down as the answer before checking +it. + +It is wrong. **The names are shuffled; the indices are the species.** Species +387's bank is called `BANK_PV432` and its wave archive index is 387. The +cartridge says the same thing in one line — +`NNS_SndArcPlayerStartSeqEx(handle, -1, waveID, -1, SEQ_PV001_sseq_1)` with +`waveID = species`: one sequence for every cry, and the bank number *is* the +species. Two statements that cannot borrow from each other, agreeing on all +493. + +Following the names would have given 25 species the wrong cry — audible, +plausible, and attributable to nothing. + +### Two small things that are the whole difference + +* **SWAV PCM8 is signed and WAV PCM8 is unsigned.** One addition, and the + difference between a cry and a burst of noise. The check is that a decoded + cry's mean byte lands on 128.0 — Pikachu's does, across 8,236 samples, with + the range running 3 to 252. +* **A SWAV's length is in 32-bit words** counted from the end of its own + twelve-byte header. `WAVE_ARC_PV001` is 8,260 bytes and + `0x3C + 4 + 12 + 2046 × 4` is 8,260 — the layout and the file agreeing to the + byte, which is what makes the reading a check rather than an assumption. + +### What it cost the engine: nothing + +`Sound.playCry(data, species)` already reads `data.audio.cries[]` and +already accepts `{ file = ... }`, because that is the shape Gen 1, 2 and 3 +write. The stage fills the same table with the same shape, so **not one line +of the engine's audio path needed a Gen 4 branch.** Cries are keyed by species +name, as everywhere else. + +`CACHE_FORMAT` is now **`rom-cache-v347`**. + +**The music is still silent**, and that is the honest line: it needs an SSEQ +player — a sequencer over sampled banks — and nothing here is a step toward +one. What this stage establishes is only that the archive is readable and the +file table is right, which a sequencer would need first anyway. + +## The split that was being thrown away + +Generation 4 is the one that abolished the type-based physical/special rule. +That is the defining mechanical change of the generation, and this port was +discarding it — not in the battle engine, which is not wired for Platinum yet, +but one field earlier, where it would have poisoned every fight the moment it +was. + +`Damage.categoryOf` reads `move.category`. The Gen 4 extractor writes the split +as **`class`**. So every Platinum move fell straight through to +`TypeChart.category(move.type)` — the pre-Gen-4 rule the cartridge exists to +replace. + +**Measured against this cartridge's own 471 moves: 92 of the 301 damaging ones +disagree with the type rule — 31%.** + +| move | class | type rule would say | +|---|---|---| +| Fire Punch, Ice Punch, ThunderPunch | physical | special | +| Hyper Beam, Gust, SonicBoom, Razor Wind | special | physical | +| Bite, Razor Leaf, Vine Whip | physical | special | +| Acid, Night Shade | special | physical | + +Nearly a third of Sinnoh's attacks on the wrong stat, both ways. **A battle +would have run to the end and simply been wrong** — wrong numbers, right +structure, reading as bad luck rather than as a bug. This is the cheapest +moment to find it: before anything depends on it. + +The fix is `move.category or move.class or TypeChart.category(move.type)`, in +the engine rather than by renaming the field in the extractor, so it needs **no +re-import**; and `class` is what the cartridge's own move record calls it. +Gen 1, 2 and 3 write `category` on their moves and nothing else in the project +puts a `class` on one, so their precedence is unchanged — checked against all +four cases. + +The same field is read a second time where a **status** move must not roll +damage at all. Platinum has 170 of them and every one carries its class under +the other name; asking only for `category` there left each of them saved by +`power == 0`, which is true today and is not the thing being asserted. + +## The ruleset that was written and never used + +Looking at what a Platinum battle would run under turned up something that is +not about Platinum at all. + +`BattleState` picks its rules as `constants.defaultRuleset or "gen1_faithful"`. +**Nothing has ever written that constant** — not the Gen 3 extractor, not the +Gen 4 one. Checked against the caches rather than the code: neither Emerald's +`constants.lua` nor Platinum's contains the string anywhere. + +So on a fresh save, **Hoenn has been fought under Generation One's rules**: + +* the 1/256 miss on a hundred-percent-accuracy move +* crit rate derived from speed rather than the 1/16 stage ladder +* a crit that doubles the **level** inside the formula rather than the damage + — which also doubles the formula's `+2` and re-floors, so it is a different + number, not a reformulation +* the random factor read as 217..255 out of **255** instead of 85..100 out of + **100** +* Gen 1's sleep turns, status divisors, Focus Energy bug, and unlimited enemy PP + +And `src/battle/rulesets/gen3_emerald.lua` — which states every one of those +differences, each with a disassembly address — has been sitting beside it, +registered in `Builtins`, referenced by nothing. Its own opening comment is +*"the mechanics were already right and every constant was Gen 1's, which is the +kind of wrong that looks fine until you count."* That was written about the +status constants inside it. It turned out to describe the file's own fate. + +The one thing that worked is the OPTIONS row, which cycles the merged registry +— so a player who happened to flip it got the right rules and had no way to +know the default was wrong. + +`src/battle/RulesetDefaults.lua` is the fallback now, in one place because two +files need it and a second copy is how they disagree later. A cartridge that +names its own still wins; nothing about the mod registry or the options row +changes; **no cache output changed, so no re-import.** + +Two deliberate non-changes: + +* **Gen 2 stays on Gen 1's.** There is no Gen 2 ruleset in this repo and + Johto's constants differ from Kanto's in ways nobody here has measured. + Moving it onto a ruleset written for Hoenn would trade a known wrong answer + for an unknown one. +* **Gen 4 took Gen 3's, and that was a judgement rather than a reading.** It is + a reading now — see the next section. The judgement was *nearly* right, and + wrong in exactly one place, which is the interesting part. + +## `gen4_platinum`, measured rather than assumed + +The caveat above named three things nobody had checked: the damage formula's +internal rounding, the sleep counter, and the hooks Gen 4 added. Two of them +are now read out of pokeplatinum with the line beside each, and the third — +abilities and held items — is not a ruleset field in this engine and so is not +pretended at in one. + +**The result is not what the caveat expected.** Of every constant this engine +models, **exactly one moved.** + +### The one that moved: spread damage + +Emerald halves a spread move, and only a move whose target byte is *exactly* +`MOVE_TARGET_BOTH`. Earthquake, Explosion, Self-Destruct and Teeter Dance are +target `$20` on that cartridge and are **not reduced at all** — they hit three +Pokémon for full. + +Platinum, `battle_lib.c:7035-7044`, is **two consecutive blocks**: + +```c +if (DOUBLES && range == RANGE_ADJACENT_OPPONENTS + && CountAliveBattlers(TRUE, defender) == 2) damage = damage * 3 / 4; +if (DOUBLES && range == RANGE_ALL_ADJACENT + && CountAliveBattlers(FALSE, defender) >= 2) damage = damage * 3 / 4; +``` + +Three quarters, not a half — and the two blocks **count different things**. +`BattleSystem_CountAliveBattlers` (`battle_lib.c:2825`) branches on its +`sameSide` argument: `TRUE` (`:2840`) counts the living on the **defender's +side**, `FALSE` (`:2833`) counts **every living battler except the defender**, +across both sides. + +So Blizzard is reduced only while both foes are up — Emerald's condition, kept. +But **Earthquake is reduced whenever two others are still standing**, including +when the defender is the last foe alive, because the user's own ally is taking +the hit too. Reading the second gate as the first gives Earthquake full damage +in exactly the case the cartridge reduces it. + +### The range byte, measured off the ROM + +`Gen4Moves.parse` already reads `range` at offset 8, but nothing had ever +checked what the values were. Across all 471 members of +`/poketool/waza/pl_waza_tbl.narc` the field only ever takes 0, 1, 2, 4, 8, 16, +32, 64, 128, 256, 512 and 1024 — bit positions, one for one with +pokeplatinum's `generated/move_ranges.txt`. Spot-checks against that list: +Blizzard, Rock Slide and Hyper Voice read **4** (`RANGE_ADJACENT_OPPONENTS`); +Surf, Earthquake, Explosion, Self-Destruct and Teeter Dance read **8** +(`RANGE_ALL_ADJACENT`). + +**`0x08` does not mean the same thing in the two generations** — it is +`MOVE_TARGET_BOTH` in Hoenn and `RANGE_ALL_ADJACENT` in Sinnoh — and a Gen 3 +move record calls the field `target` while a Gen 4 one calls it `range`. That +is why the test could not stay a literal in the engine. + +`BattleState:computeDamage` used to carry `move.target == 0x08` itself, which +meant **Emerald's rule was the only rule any ruleset could have**, measurement +or no measurement. It is now two fields on the ruleset: + +| Field | `gen3_emerald` | `gen4_platinum` | +|---|---|---| +| `spreadField` | `"target"` | `"range"` | +| `spreadRanges` | `{ [0x08] = "defenderSide" }` | `{ [0x04] = "defenderSide", [0x08] = "othersOnField" }` | +| `spreadNum` / `spreadDen` | 1 / 2 | 3 / 4 | + +`gen1_faithful` and `modern_clean` have neither field, never set `spread`, and +are untouched — Gold, Silver, Crystal and Prism see no change from any of this. + +### The sleep counter, which needed reading to come back the same + +`subscript_fall_asleep.s:59` is `Random 3, 2`. That reads as 2..4 and **is +not**: `BtlCmd_Random` (`battle_script.c:3200`) reads the bound, **adds one**, +and then adds the offset — `(rand % 4) + 2`, so **2..5**, identical to +Emerald's `sleepTurnsMin = 2, sleepTurnsMax = 5`. The only way to know that was +to go and look, which is the whole argument for writing the matching constants +down rather than inheriting them silently. + +### Everything else that was checked and did not move + +| Constant | Platinum | Source | +|---|---|---| +| Crit ladder | 1/16, 1/8, 1/4, 1/3, 1/2, stage clamped to 4 | `sCriticalStageRates[]`, `battle_lib.c:7097`, clamp `:7130`, roll `:7134` | +| Crit multiplier | ×2 (×3 with Sniper, not modelled) | `:7139`, `:7142` | +| Random factor | 85..100 out of **100** | `:7084` — `damage *= (100 - RandNext() % 16); damage /= 100;` | +| Screens in doubles | `damage * 2 / 3`, else `/ 2` | Light Screen `:7023`, Reflect `:6982`, both on `CountAliveBattlers(TRUE) == 2`, both skipped on a crit | +| Burn | halves the running damage unless Guts | `:6978` | +| Burn / poison residual | maxHP/8 | `subscript_burn_damage.s`, `subscript_poison_damage.s` | +| Toxic | maxHP/16 × counter, counter is a 4-bit field so stops at 15 | `MON_CONDITION_TOXIC_COUNTER` | +| Freeze thaw | 1 in 5 per turn | `RandNext() % 5 != 0` keeps it | +| Poison immunity | POISON, STEEL | `subscript_poison.s:34-37`, `subscript_badly_poison.s:22-25` | +| 1/256 miss | gone | accuracy is a percentage out of 100 | + +Every one of those is written out in `gen4_platinum.lua` **anyway** rather than +left to be inherited, because an absent field reads as *nobody looked* and a +field with a citation reads as *somebody did*. + +### What this changes for a player + +`RulesetDefaults.DEFAULT_BY_GENERATION[4]` is `"gen4_platinum"`, and +`Builtins`' `rulesets` registrant lists the file alongside the other three, so +the OPTIONS row cycles it like any other. **No cache output changed, so no +re-import.** A player on the old default was playing something very close to +right — and wrong in every double battle. + +Gen 2 is still on Gen 1's, and stays there until somebody does to Johto what +this did to Sinnoh. + +## Sinnoh was drawn upside down + +Reported from play: *"The map is rendering in flat 2d and also seems like its +upside down and maps arent properly aligned nor walkable areas the pokeball +screen where your supposed to touch the pokeball isnt rendering properly as +well."* Three complaints, two of them one bug, and the third is real but is not +a bug. + +### The flip + +`Gen4Ground.topDown` built its orthographic matrix with `ndc.y = -z/half`. A +custom `position()` in a LÖVE shader returns clip coordinates **directly**, +which bypasses the projection LÖVE would set up for the target — and **a +canvas's framebuffer counts its rows the opposite way from the screen**. Every +chunk in Sinnoh baked mirrored top to bottom. + +This trap is already written up in this repo. `Gen4Title` carries it — *"CLIP Y +POINTS THE OTHER WAY INTO A CANVAS … Reported: Giratina is showing upside +down"* — along with the `FLIP_Y` matrix that fixes it. `Gen4Ground` was written +without the compensation and nothing connected the two. + +**Measured on Twinleaf Town** (T01, matrix 0, chunk cell 3,27) rather than +eyeballed, because "looks wrong" and "is mirrored" are different claims: + +* the four houses' **collision footprints** are 5×5 at the top-left and + bottom-right and 4×4 at the top-right and bottom-left. Segmenting the + screenshot's four teal roofs gives 135×89 px at **bottom-left and top-right** + and 99×65 px at **top-left and bottom-right** — the pair is swapped, which is + a mirror in one axis. +* which axis is settled by the town's **asymmetric border**: the collision grid + has extra gaps at rows 1–2 only, never at rows 29–30. The screenshot has them + along the **bottom**, at image y 700–820, which back-projects to rows ~1–3 — + a vertical flip. Under a horizontal flip the same pixels land on rows 26–29, + where the grid is solid. +* all four of the map's **warps** sit on the bottom row of their own house's + footprint, which is where a door is; the door model (`build_model` 67, placed + four times) sits at `z = house z + 13`. So `+z` is south in the grid's terms + as well as the cartridge's, and the mailboxes at (13,11) and (18,21) sit + beside their doors on the same rows. +* `Gen4Ground:heightsAt` **already** reads tile y as `+z`. The height lookup and + the picture disagreed about which way south was. + +One character: `0, 0, -1/half, 0` became `0, 0, 1/half, 0`. + +The second half of the report follows from the first. *"Maps arent properly +aligned nor walkable areas"* is what a mirrored ground **is**: the collision +grid was never flipped, so a house visible at the bottom of the screen had its +walls at the top and its door on the wrong side. Nothing about collision itself +was wrong. + +A vertical flip also reverses triangle winding. That costs nothing here because +`Gen4Model:draw` sets cull mode `"none"` — noted in the file so that turning +culling on for the ground does not quietly break it again. + +### The Poké Ball was the exact inverse of a Poké Ball + +The intro's ball step was composing member 40's tilemap against the 16-tile +sheet at member 32/33/34 **with no tile base**, and a tilemap indexes VRAM +rather than the member it shipped beside. + +Decoded, member 40 is 32×24 cells of which **736 are tile 0** and 32 are +`0x20..0x2F` — sixteen indices for a sixteen-tile sheet, with `flipX` doing the +ball's right-hand side, and palette row 2 (which is why `paletteFirst = 32` was +already right). The app loads those sixteen tiles at **tile 32** of the +background's character base. + +Composed without the base, tile 32 begins at byte 1024 of a 512-byte sheet, so +`Gen4Graphics.compose`'s own bounds check left every ball cell transparent — +while all 736 empty cells drew the sheet's tile 0. The result on disk was a +full screen of one repeated glyph with a 48×48 hole punched where the ball +goes, **identical across all three frames**, which is what shipped and what the +screenshot shows. + +`Gen4Graphics.compose` now takes a `firstTile`, and this was checked for every +other pairing in the archive rather than assumed: the five backdrops run to +tile 121 and the figures' tilemap to 127, both inside their own 128-tile +sheets. **Only the ball is loaded high.** + +Re-composed with the base, the three frames are a 42×42 button centred on the +screen: pale blue, pressed, then **yellow** — the ball's button being pushed in +and lighting up, which is precisely what `Gen4RowanIntro`'s three-picture +animation was written for. + +That also corrected a reading in the screen. `drawScene` drew the ball picture +and **returned**, on the argument that it "owns the screen because on the +cartridge it owns a screen". That argument only survived because the picture +was a full-screen field of garbage; the real picture is a small button on +transparency, and drawn alone it is a black screen with a button on it. It now +draws **last, over Rowan and his backdrop**. + +`CACHE_FORMAT` goes to `rom-cache-v348:` so the three ball PNGs are rebuilt. +The ground flip is runtime-only and needs no re-import. + +### "Flat 2d" is not a bug, and here is what it would take + +`Gen4Ground` bakes each 32×32-tile chunk **orthographically, top down, once**, +into a 512×512 canvas, and the map draws canvases. That is a deliberate choice +and the file says so: the geometry does not change, so rasterising it every +frame arrives at the same picture. It is also why walls and house fronts are +invisible — an orthographic top-down view of a 3D town shows roofs. + +The DS draws Sinnoh with a **tilted perspective camera**. Getting there is not +a fix to this file so much as the next stage of it, and the meshes are already +in the cache: + +1. a per-frame camera (`Gen4Model.perspective` + `lookAt`, both of which exist) + in place of `topDown`, drawing the visible chunks' models directly rather + than their baked canvases +2. sprites billboarded into that camera instead of blitted at tile positions, + which is where the player and every NPC currently live +3. the cache's own `lighting` value per map (already extracted — `T01R0202` + carries `lighting = 3`), which is what makes an indoor room read as indoors + +Until then the bake is the flat case of the same data, and it is now the right +way up. + +### The bedroom in a green field + +The second screenshot — a small room drawn in the corner of a large green +field — was measured too, and it is **not** a collision fault. + +`T01R0202` is the player's bedroom. Its chunk's permission grid is 1,024 tiles +of which only the top-left 12×12 carry anything: 936 tiles read `0x0000` +(walkable, behaviour 0) and the 83 that carry `0x8000` form a **sealed** box — +rows 0–3 solid, `x = 0` and `x = 11` solid down the sides, row 11 solid across +the bottom. The player cannot leave the room. That is the cartridge's own +arrangement, not a gap in the import. + +What is wrong is that **nothing knows the map is smaller than its chunk**. The +map def is 32×32 because the matrix is 1×1 and a chunk is 32 tiles; the mesh +covers only the room; the stand-in fills the other 900 tiles with its +walkable-behaviour colour; and the camera is free to show all of it. The fix is +a camera clamp and a void fill derived from the terrain's own coverage rather +than from the grid's declared size — filed rather than guessed at. + +## Platinum's field camera, transcribed + +`overlay005/field_camera.c` holds the whole thing: **seventeen** cameras, one +per `CAMERA_TYPE_*`, each `{ distance, cameraAngle, projection, verticalFov, +near, far }`. `MapHeader.cameraType` — the byte at offset 21, which +`Gen4MapHeaders` has parsed since it was written and which nothing has ever +carried into a map def — picks one. + +Over all 593 headers in this cartridge: + +| Camera | Headers | | Camera | Headers | +|---|---:|---|---|---:| +| `INTERIOR_ORTHOGRAPHIC` | 300 | | `SPEAR_PILLAR` | 4 | +| `DEFAULT` | 189 | | `SLIGHTLY_ZOOMED_OUT` | 3 | +| `CAVE` | 60 | | `OREBURGH_GYM` | 2 | +| `ZOOMED_IN` | 19 | | `HALL_OF_ORIGIN` | 2 | +| `IRON_ISLAND_CAVE` | 6 | | `LAKE_ACUITY` | 2 | +| | | | six others | 1 each | + +**Three hundred of the 593 are orthographic on the cartridge** — every +ordinary room, the player's bedroom included. So "flat" was never the mistake. +*Flat and straight down* was: all seventeen are **tilted**, between 40.6° and +78.4°. + +### One world unit is one screen pixel, and that is the cartridge's number + +`Camera_ComputeProjectionMatrix` builds the orthographic box as +`top = tan(fovY) × distance`. For `INTERIOR_ORTHOGRAPHIC` that is +`tan(3.5211181640625°) × 1563.537841796875 = 96.209` — **half of the DS's +192-row screen**. So `verticalFov` is the *half* vertical FOV, and the scale is +1:1 at the target plane. + +Run the same product over all seventeen and fifteen of them land between +94.815 and 96.228 — within 1.3% of one pixel per unit. (`STARK_MOUNTAIN_ROOM_2` +is 115.1 and `UNUSED_16` is 91.6; both are the cartridge's own values.) That +independently confirms the `pixelsPerUnit = 1` the terrain import already +assumed. + +`Camera_AdjustPositionAroundTarget` puts the camera at +`target + (sin(y)·d·cos(x), sin(−x)·d, cos(y)·d·cos(x))`, and `y` is zero for +all seventeen — the camera is always due **south** of its target and above it. +For `DEFAULT`: 571.97 up, 342.98 south. + +### What this port draws, and how far off it is + +The engine's world is a grid of 16-pixel tiles, and *everything* else in it — +collision, warps, sprites, the tile window, encounters — is laid out in those +pixels. A true perspective camera moves the ground relative to that grid, so +adopting one means projecting every sprite through it as a billboard in the +same pass. That is the right end state and it is not this change. + +What `Gen4Ground` does now is an **oblique** projection at the cartridge's own +pitch: + +``` +screenX = x screenY = z − y · cot(pitch) +``` + +The ground plane maps **one to one**, so the tile grid, the collision and every +sprite stay exactly where they were and nothing above that file changes. +Height leans up the screen, which is the whole point: a house shows its front, +a cliff shows its face, a bridge stands off the path beneath it. At pitch 90° +the lean is zero and this is byte-for-byte the straight-down bake that came +before — which is why the OPTIONS row can offer that as one of its values +without a second code path. + +**How far that is from the cartridge, measured:** + +* the cartridge scales ground depth by `sin(pitch)` and height by `cos(pitch)`; + this scales them by 1 and `cot(pitch)`. That is the cartridge's own picture + stretched vertically by `1/sin(pitch)` — **16.7%** for `DEFAULT` — and the + stretch is the price of keeping a tile 16 pixels tall. +* the remaining difference is the perspective itself, and on this cartridge + that is small: a half-FOV of 8.09° at 666.9 units is a very long lens. + Ray-traced against the ground plane, `DEFAULT` sees from **101.87** units in + front of the target to **120.86** behind it — 222.73 units of ground over 192 + rows, against **223.87** for an orthographic camera at the same scale. That + is **0.51%** on the total and **+9.9% / −7.4%** on the two halves: the near + and far *edges* of the screen are wrong by about a tenth, the centre is right. +* for the 300 `INTERIOR_ORTHOGRAPHIC` headers there is no perspective error at + all — the cartridge is orthographic there too. + +The chunk canvas grows **upwards** by `leanPx = ceil(cot(pitch) × 384)`, +because that is where the leaning geometry goes; the blit takes the same +`leanPx` back off, so the ground lands where it always did. Chunks are drawn +north to south, which was already the loop's order and is the order that has to +hold once one chunk's towers overlap the next one's ground. Terrain that climbs +more than 384 units inside one chunk loses its top rows rather than drawing +over its neighbour. + +### The OPTIONS row + +`CAM TILT` cycles `CARTRIDGE / 90° / 80° / 70° / 60° / 50° / 40°`. `CARTRIDGE` +is the map header's own type — the answer this work exists to give. Changing it +ticks `Gen4Camera.generation`; `Gen4Ground:draw` compares the counter and drops +its bakes, because the pitch is baked *into* them. One frame's hitch, then +nothing. + +Held in the module rather than read from the save, for the reason +`TileRenderer.setTileAnim` is: `MapLoader.load(data, mapId)` is never handed a +game. Two callers take the saved value up — the OPTIONS list as it is built, +and `OverworldState:enter`, which is the one place every path into the world +goes through (boot, a warp, the map editor's Play). The second is what makes +the choice survive a restart. + +### Whoever is standing on the ground has to lean with it + +The tile grid is untouched by the tilt — that is the whole reason the oblique +projection was chosen — but a character on a hill, a bridge or a flight of +steps was still drawn at the *grid's* height rather than the terrain's, so they +walked through what they were meant to be standing on. + +`Gen4Ground:rise(px, py)` is that lift in pixels: the BDHC height under a tile, +times `cot(pitch)` — the same `z − y·cot(pitch)` the chunk bake uses, applied +to one point. It takes **map pixels** rather than tiles so a sprite's own +position gives an answer that changes as it walks. Heights are cached per tile, +and the *raw* height is cached rather than the lifted pixels, because the tilt +can change under it and the terrain cannot. + +Applied as a **shifted camera** rather than a shifted position, which is what +keeps it to one seam: every sprite path in this engine ends in `py − camY`, so +`e:draw(cam.x, cam.y + riseOf(e))` lifts the sprite and nothing else has to +learn about elevation. Zero on every Gen 1/2/3 map and on any Gen 4 map drawn +straight down — `gen4Ground` is nil in the first case and `lean` is 0 in the +second — so it costs one nil test a frame for everything that is not Sinnoh. + +Measured, the heights are sane world units: Twinleaf's chunk runs 8–16, the +player's bedroom −24–0 (the floor sits below the chunk origin, and the mesh +moves down with it, so the two stay together), and across all 666 chunks +−96–480. + +It is still a **step per tile** rather than a ramp. The BDHC's plates are per +region, so the ground itself steps there too; a true ramp wants the plate's own +plane evaluated at the exact point, which `Gen4Bdhc` can already do and this +does not ask for yet. + +#### ...and it found a bug that had never fired + +`Gen4Ground:heightsAt` sorted its results with `table.sort(list, a > b)`, and +`Gen4Bdhc.heightsAt` answers `{ index, height }` **records**, not heights. Lua +will not order two tables, so that call raises — and `heightAt` was handing its +caller a record where it promised a number. + +Neither had ever gone off, because nothing called either function: the movement +grid still reads elevation 0 (#98). The moment something did, it would have +raised on the first tile with two surfaces under it. Sampled over 42,624 tiles +spread across all 666 chunks, **652 of them (1.5%) have more than one plate** — +which is every bridge in Sinnoh, and therefore exactly where a player walks. +`heightsAt` now returns plain numbers, highest first. + +## Sinnoh was wearing one palette + +Reported: *"much of the tileset didnt seem properly colored especially when +outside."* It was outside, and only outside, and the reason is one line. + +`Gen4Terrain.textures` decoded **every** texture against **palette 1**. The +note beside it said why: *"a texture's own palette is named by the material +that wears it, which is a fact about the chunk rather than about the picture, +and the great majority of these sets pair one palette with one texture +anyway."* The first half is true. The second half is false. + +Twinleaf Town's set (member 6) holds **78 textures and 74 palettes**. Palette 1 +of that set is `apeak` — a mountain top. So `ngrass`, which is grass, came out +in browns; so did the sand, the flowers and the lake. Every outdoor map in the +game was painted in one palette. + +**The join was already in the cache and always was.** Every chunk shape carries +`material`, `texture` *and* `palette`, because `Gen4Nsbmd` reads the material +that names both — `ngrass` is worn with palette `grass`. Nothing used it. The +*model* path never had this problem; it has always looked `shape.palette` up in +the member's own dictionary, which is exactly why the buildings were the right +colour standing on ground that was not. + +**Name-matching is not a substitute**, which is worth saying because it is the +obvious shortcut. Over all 7,547 chunk shapes there are 1,149 distinct texture +names, and **553 of them are worn with a palette of a different name** — +`ngrass02` wears `grass02`, `bf_symbol2` wears `symbol`, `ginga2` wears +`dun26_092`. Across the archives as a whole, 1,293 of 3,130 map textures and +2,104 of 3,063 building textures have no palette of their own name at all. + +Twenty-nine textures are worn with **more than one** palette (`wcliff` is +`criff` on 76 shapes and `wcriff` on 7); those get a second picture filed under +`#`, and `Gen4Ground:modelFor` asks for that key first. Every +other texture is filed once, so the file count barely moves — set 6 goes from +78 pictures to 88. + +### ...and 355 textures were not decoded at all + +`Gen4Models.DECODABLE` listed formats 2, 3 and 4. Counted over both texture +archives, that left out **259 a3i5 and 96 a5i3** pictures — 238 of them in the +map sets. Those are the textures with soft edges: cliff tops, tree canopies, +the lake surface. The holes were exactly where a map most looks wrong. + +Neither format is a paletted texture with an alpha bit bolted on; the texel +*is* a pair — three bits of alpha over a five-bit index, or five over three — +and the alpha is the texel's own rather than the palette's. `transparent0` does +not apply to either: index 0 is an ordinary colour there, and treating it as a +hole punches the middle out of every soft edge. + +Recounted with both added: 4,306 `palette16`, 1,484 `palette4`, 48 +`palette256`, 259 `a3i5`, 96 `a5i3`, and **nothing else** — no 4×4-compressed +and no direct colour. Set 6 now decodes 88 of 88 with none refused, against 9 +refused before. + +## Sinnoh's scripts had no handlers at all + +Reported: *"npcs are appearing for the events but not triggering, npcs are +still missing text."* The log says it outright: + +``` +[warn] script: unknown command 'g4_lock_all' (skipped) +[warn] script: unknown command 'g4_face_player' (skipped) +[warn] script: unknown command 'g4_buffer' (skipped) +... show_text ... +[warn] script: unknown command 'g4_wait_button' (skipped) +[warn] script: unknown command 'g4_check_flag' (skipped) +[warn] script: unknown command 'g4_compare_var_value' (skipped) +``` + +`Gen4ScriptVM` has lowered Platinum's bytecode into 61 distinct `g4_*` rows +since it was written, and **nothing has ever registered a handler for one**. +Every row went through `ScriptRunner`'s unknown-command path, which logs and +steps over it. + +The box opened and shut without waiting; the name placeholders stayed empty; +and — the one that matters most — every `checkflag` and `comparevar` left the +comparison register untouched, so each branch after it read whatever the +*previous* script had put there. A script that is 90% correct and branches at +random is not 90% of a conversation. + +`src/script/Gen4Commands.lua` is the other half. It follows `Gen3Commands`' +shape — add `g4_*` functions to the shared `Commands` table, register them in +`Commands.registerInto` — but the two share no state: Hoenn's vars live in +`save.gen3Vars` and Sinnoh's in `save.gen4Vars`, because one save can hold +both. + +The comparison register is the cartridge's, transcribed. `Compare` (`scrcmd.c` +:879) returns 0 for `<`, **1 for `==`** and 2 for `>`; `ScrCmd_GoToIf` (:1065) +indexes `sConditionTable[condition][result]`: + +``` +// < == > +{ TRUE, FALSE, FALSE }, // 0 < { TRUE, TRUE, FALSE }, // 3 <= +{ FALSE, TRUE, FALSE }, // 1 == { FALSE, TRUE, TRUE }, // 4 >= +{ FALSE, FALSE, TRUE }, // 2 > { TRUE, FALSE, TRUE }, // 5 != +``` + +`ScrCmd_CheckFlag` writes the **flag's own value** into that register rather +than a comparison (:1105), which is what makes `checkflag` then `gotoif 1` read +as "if the flag is set". Vars are ids from `0x4000` (`VARS_START = 16384`) and +anything below is a literal — the same rule Hoenn uses. + +### Eight lowering entries that named opcodes the decoder never produces + +Cross-checking the lowering table's keys against `Gen4ScriptOps`' 840 opcode +names turned up **eight dead entries** — code that has never once run: + +| Written as | The cartridge's name | +|---|---| +| `giveitem` | `additem` | +| `takeitem` | `removeitem` | +| `lock` / `release` | `lockobject` / `releaseobject` | +| `bufferpokemonname` | `bufferpartymonspecies` / `bufferpartymonnickname` | +| `message2` | `messageinstant` / `messagenoskip` / `messagesynchronized` | +| `trainerbattle` | `starttrainerbattle` | +| `closemenu` | *(no such opcode)* | + +**No item gift in Sinnoh was being lowered at all.** The dead-key count is now +zero, and six more buffers (`buffermovename`, `buffernumber`, +`buffercounterpartname`, `messagevar` …) are lowered while the names were +being checked. + +### ...and four that dropped operands + +* **`warp` is five operands, not three.** `ScrCmd_Warp` (`scrcmd.c`:3566) reads + `(mapHeaderID, unused, x, z, direction)`. The lowering passed args 1–3, so + the runner got the header, the *padding* and the X coordinate as if they were + map, x and y. Every scripted warp went somewhere wrong or nowhere. It also + names a **map header**, not a map, so `g4_warp` builds the header→map-id + index once from the defs and looks through it. +* **All four bag commands are `(item, count, destVar)`** (`scrcmd_item.c`) and + every one writes whether it succeeded into that var, which the script then + compares. The destination was being dropped. +* **`showyesnomenu ` puts the answer somewhere.** The row asked the + question and threw the answer away, so every yes/no in Sinnoh branched on + stale state. +* `starttrainerbattle` takes two operands and was passed one. + +### What is honestly not built + +Sixteen verbs are registered as named, log-once no-ops rather than left to the +unknown-command path: the trainer-battle handoff, the shop screen, the sign-box +state machine, the scripted menu, the common-script archive. Each says once per +session what system it is waiting on. The remaining 45 do the cartridge's work. + +Verified by construction: every one of the 61 `g4_*` verbs the VM emits now has +a handler, and every one of the 83 lowering keys matches a real opcode. + +## Half the maps had no collision at all + +Reported: *"collisions dont seem to be correct in the overworld."* Not a wrong +grid — an absent one. + +Gen 4 splits its collision the way Gen 3 splits its layouts. A grid is built +once per **matrix** and a map that does not own its matrix outright references +it instead, because the 30×30 Sinnoh overworld is 921,600 cells and inlining it +once per building standing on it took the extraction from 4 seconds to 22. The +extractor writes both halves: the shared grids into `map_layouts`, the +reference as `def.layout`. + +**Nothing ever read the other half.** `Map` reads `def.blocks` and nothing +else, and at run time `data.map_layouts` is touched by exactly one Gen 3 script +command. Counted over the cache: **291 of the 593 Gen 4 maps have no `blocks` +string**, including `T01R0201` — the player's own house, the map in the +screenshot. + +`MapLoader.resolveBlocks` is the missing read: when a def has no blocks, take +them out of `map_layouts[def.layout]`, cropping to `originX/originY` and the +map's own width and height when the map is a region of a bigger grid. Done +where the def and the dataset are both in hand, and written **back onto the +def** rather than onto a copy — half the engine compares `map.def` against +`data.maps[id]` by identity, and a copy would quietly become a second map. It +is idempotent, so a reload costs nothing. + +Checked against the cartridge rather than assumed: all 291 resolve, none comes +out the wrong size, and the resolved grid for `T01R0201` matches that chunk's +own permission bytes in `gen4_map_permissions` cell for cell. + +### One thing that is *not* a collision bug + +With the camera tilted, a wall's base is drawn at its collision cell and its +top up to `height × cot(pitch)` pixels higher. Bumping into a wall whose top is +drawn well above the player reads as "collision is wrong" and is the oblique +projection working: the cell you cannot enter is the one under the wall's +**foot**, not under its cap. Setting `CAM TILT` to 90° puts them back on top of +each other, which is the quickest way to tell the two apart. + +## The checkerboard was the stand-in, and the ROM has no border + +Reported: *"i still see it surrounded by checkerboard instead of what the rom +uses as a border."* + +The checkerboard **is** the stand-in tileset. `Gen4Tileset.metatiles` lays every +cell down as a two-by-two checker of a class colour and a darkened copy of it, +deliberately, so a map drawn in flat colours still shows its 16-pixel grid. And +behaviour 0 — ordinary ground — is 940 of Twinleaf Town's 1,024 cells. Once the +mesh draws the map, the only stand-in left visible is the part the mesh does +*not* cover: the void around it, in green. + +**The cartridge has no border block.** Gen 1–3 tile a border metatile outside +the map; Sinnoh's overworld is one seamless matrix and its interiors are sealed +rooms inside a 32×32 chunk, so what the DS shows beyond the geometry is the +backdrop and nothing else. + +So `TileRenderer:drawWindow` now fills the viewport with the stand-in palette's +own colour 0 — `{24, 26, 34}`, the backdrop every class colour was always drawn +against — and lays the ground over it, instead of drawing the tile window +underneath. The stand-in still runs when there is no ground (a cache imported +before the terrain stage), which is the case it was written for. It used to +draw underneath because a chunk bakes over several frames and the screen would +otherwise be blank; a flat backdrop covers that just as well, and is what a map +transition looks like on the cartridge anyway. + +## Every warp in Sinnoh was one door early + +Reported: *"warps seem to be bugged, when i go through them im coming out in +the wrong place."* The log says it without ambiguity — `map: T01 at (20,11)` +into Twinleaf's third house, `map: T01 at (20,21)` on the way back out, and +that is the **second** house's door. + +`Warp.lua` indexes `destDef.warps[warpDef.destWarp]`, which is a Lua array. +Gen 3's extractor writes `destWarp = rom:u8(o + 5) + 1` and says why in a +comment at `RomExtractorGen3.lua:11652`; Gen 2 clamps at one. The Gen 4 +extractor wrote the cartridge's zero-based anchor straight through. + +Checked against the cache, all four of Twinleaf's houses: + +| Interior | `destWarp` | lands on | its own door is at | +|---|---:|---|---| +| `T01R0101` | 0 | *nothing* (`warps[0]` is nil) | (9, 11) | +| `T01R0201` | 1 | (9, 11) | (20, 21) | +| `T01R0301` | 2 | (20, 21) | (20, 11) | +| `T01R0401` | 3 | (20, 11) | (10, 21) | + +With `+ 1` every one lands on its own door exactly. + +**This is also why the player kept appearing outside the rooms.** `T01R0201 → +T01R0202` is `destWarp = 0`, which resolved to `warps[0]` — nil. Arriving at a +nil warp puts the player at an undefined position, and a bedroom is a sealed +12×12 box inside a 32×32 chunk: land outside the seal and you can roam the void +freely, which is the screenshot. + +## Blank text boxes: the bank was never carried + +Reported: *"talking to people brings up a text box but its blank."* + +A Gen 4 `message` command names an **entry**, not a string. The bank is the map +header's `msgArchiveID`, and the pair is what addresses a line — which is why +the extractor keys `data.text` as `TEXT_Bnnnn_nnnnn` rather than by a single +id, and says so where it writes it. + +The lowering emitted `{ "show_text", }` on the reading that "the row +carries the entry and the runner resolves the bank". **Nothing resolved it.** +`Commands.show_text` looked a bare index up in `data.text`, found nothing, and +degraded to an empty box — which is exactly the behaviour it documents for an +unusable id, and exactly what was on screen. + +Two halves, both missing: + +* the map def never carried `messages` (`h.messages` has been parsed since + `Gen4MapHeaders` was written and was dropped on the floor); +* `message` now lowers to **`g4_message`**, which joins the def's bank to the + row's entry through `Gen4Text.label`. It has to be its own verb because a + Gen 1–3 `show_text` takes a whole id and a Gen 4 one takes half of one. + +Falls back to the bare id when a map has no bank, so a cache imported before +this gets the same empty box it had rather than a crash. + +## A Gen 4 map has no border to walk on + +Reported: *"im able to walk outside of the boundaries still."* + +Gen 1–3 tile a border metatile outside the map and let the player stand on it +for a step, because that is how a map **connection** works — you walk onto the +border and the seam hands you to the neighbour. Gen 4 has neither. +`Gen4Maps.mapDef` sets `borderBlock = 0` with the note *"every cell outside the +map reads as void, which is what the border is"*. + +Block 0 is not void. On the stand-in tileset it is ordinary ground, and +ordinary ground is walkable — so the player walked off the edge of Twinleaf and +kept going, over terrain the ground renderer draws quite happily (it draws +whole chunks of the shared matrix, not the map's 32×32 crop) and nothing owns. + +`Map:isWalkableCell` now refuses an out-of-bounds cell on a Gen 4 map, beside +the `patchedImpassable` test that already guards that function for the same +class of reason. + +**That was the edge, not the destination** — and the destination is below. + +`CACHE_FORMAT` goes to `rom-cache-v349:` for the warp and the message bank; the +boundary is engine-side. + +## Sinnoh's overworld is one grid, and a map is a window onto it + +84 headers name matrix 0, the 960×960 overworld, and each takes a rectangle out +of it. There are **no warps between them**: the player walks from Twinleaf Town +to Route 201 and the header changes underfoot. Nothing in this port knew that, +so sealing the boundary — correct in itself — turned every town into a box. + +**The engine already has the machinery.** Gen 1–3 maps connect at their edges, +and `OverworldState:checkEdgeExit` walks the player across a seam with no warp, +reading the neighbour's own collision as it goes. It runs *before* the ordinary +walkability test, which is why the sealed boundary does not block it. A +connection is `{ map, offset }` where `offset` is how far the neighbour is +shifted along the seam, and the landing is `destX = cellX − offset`. + +For two windows onto the same grid that offset is exactly the difference of +their origins, which makes this a derivation rather than a guess: + +``` +up / down offset = destOriginX − originX +left / right offset = destOriginY − originY +``` + +**Adjacency is tested, not assumed.** `connectionLanding` puts an upward step on +the destination's *bottom* row, which is only right when the destination's +bottom edge **is** this map's top edge — so two regions that overlap, or sit +corner to corner, get no connection rather than a wrong one. +`Map:connectionFor` already narrows a multi-neighbour edge to the one covering +the player's position along it, using the same origin arithmetic, so a long +route with three maps down its side needs nothing extra. + +Measured over this cartridge: **40 layouts are shared, 69 maps come out with at +least one connection, and there are 140 edges between them.** Spot-checked +against Sinnoh's actual geography: + +| Map | Connections | +|---|---| +| Twinleaf Town | `up → Route 201` (offset 0) | +| Route 201 | `down → Twinleaf`, `left → Verity Lakefront` (−32), `right → Sandgem Town` | +| Jubilife City | `right → Route 203`, `left → Route 218`, `down → Route 202` (32), `up → Route 204` (32) | + +One supporting fix: the seam reads the *neighbour's* collision, and a Gen 4 map +that references a shared grid has no `blocks` until it is loaded — so +`connectionLanding` now runs `MapLoader.resolveBlocks` on the destination +before testing it. Idempotent, and a no-op on every other generation. + +## `callcommonscript` is a real call now + +`g4_common` was the one verb still logging *"decoded and lowered but the +common-script archive is not built yet"*, and it is the busiest thing on that +list: **668 call sites** in this cartridge — 652 into `scripts_common`, 14 into +`pokedex_ratings`, 2 into `pokemon_center_2f_common`. + +The lowering left it as an unexecutable row on the reading that common scripts +*"are not in the map's own member, and inlining would need the whole common +archive resolved before a single map could lower"*. Both halves of that turned +out to be already done, or nearly: + +* `extractScripts` walks **every** member of `scr_seq.narc` — all 1,124 of them + — decoding each entry point and every block a `goto` reaches. Member 211, + `scripts_common`, already has **231 blocks in the pool**. The common scripts + were never missing; nothing could name them. +* a band id is an index into that member's **entry-point list**, exactly as a + map's own script id is into its own — and `extractScripts` already builds + that list. Only the thirty band members' lists are written into the pool, so + it costs nothing. +* which member a band's file is comes from `Gen4Archives.names`, so + `scripts_common` → member 211 is *derived*, not written down. + +So `callcommonscript` lowers to the ordinary call it always was — +`g4_call ` / `jump