16 KiB
Gold port: what is still open after the 2026-08-09 asm parity runs
Two multi-agent runs diffed the Gold port against the pokegold disassembly
(../pokegold) and fixed what diverged. Round 1 confirmed 51 defects across 14
paired clusters; round 2 confirmed 41 more across 13, including one crash. A
third round (2026-08-10) closed the mod-API boundary: see "The 2026-08-10
mod-API round" below for what that opened and closed.
Every tier is green at the end of all three: run_engine 131/131, run_tests
ALL TESTS PASSED, run_modkit 6/6, run_save_editor_tests 650/0,
run_link_tests ALL PASSED.
This file records only what is NOT done, so nobody re-derives it. It is a
working handoff doc in the shape of KANTO-CONTINUE.md, not user documentation.
Do this first or none of it is visible
Re-import the Gold cache. Seven fixes live in the extractor and the manifest
tool, so they do nothing until the ROM is re-imported. This was verified still
outstanding on 2026-08-10: the default identity's
gold/data/generated/menu_gfx.lua has no trainerPics key at all, so a trainer
battle still opens on the mon rather than the trainer even though the Lua half
is fixed and correct.
- trainer frontpics (
TrainerPicPointerswalk inextractMenuGfx), which is the missing trainer intro image at the start of a trainer battle. The Lua side is now right and re-import is the ONLY thing left:Trainers.lookupreturns a numericclass(36) beside aclassId(BUG_CATCHER), and bothmenu_gfx.trainerPicsandpalettes.trainersare keyed by the constant, so the lookup had to move toclassId. On a cache that carries the pics the frontpic renders in the class's own palette. - egg assets:
battle/front/egg.pngandmenu/egg_hatch.png, feeding the hatch cutscene and the summary screen's egg page. A fallback landed in the meantime (ICON_EGGframe 0, which resolves to a real 16x16 frame), so the egg page draws SOMETHING on a stale cache;tools/rom_manifest_gold.jsoncarriesEggPicat[20, 31363]and that stream decompresses to exactly 400 bytes (a 5x5-tile pic), so a fresh import takes the real path. - the Pokegear phone icon.
_LoadFontsExtra(engine/gfx/load_font.asm:8-15) putsFontsExtra_SolidBlackAndUpArrowGFXat$60/$61andPokegearPhoneIconGFXat$62, ON TOP ofFontExtra's BOLD_A/B/C.src/import/RomExtractorGen2.lua:342-356readsFontExtrastraight in and then blits onlyFramesover$79-$7E, so$62is still a bold "C" -- which is what the new caller box draws where the telephone glyph belongs. Fix is two files: add the two GFX symbols totools/rom_manifest_gold.json, then blit them at$60/$61/$62in the loop that already handlesFrames. PREDEFPAL_UNOWN_PUZZLE = 76, which is why the Ruins of Alph puzzle rendered grayscale instead of brown- the tilemap pad
0x7f -> 0x4finreadTilemapRLE/readFlatTilemap - held-item icons (
HeldItemIconsin the manifest,out.heldIteminextractIcons), which is the held-item marker in the party menu - the NPC-trade rows'
itemfield, which is what made the Violet City Onix's Bitter Berry print as "83"
Open: real unknowns, each needs a cache and a driver run
-
Magnet train renders a blank field. Pushing
World:magnetTraindirectly shows nothing for 90+ frames (see shot03-magnet-train.png). The screen's surround is correct now; what was never investigated is whetherdrawBackgroundneeds setup that the direct push skips. This cannot be settled by reading source. -
The NPC that introduces Unown never appears. Round 1 could not produce a port ref that diverges from the asm, so it is filed
cannot_locate, not fixed. The cart chain is: solve a chamber puzzle, thensetmapscene RUINS_OF_ALPH_INNER_CHAMBER, SCENE_RUINSOFALPHINNERCHAMBER_STRANGE_PRESENCEfrommaps/RuinsOfAlphKabutoChamber.asm. Walk that chain live rather than statically: the static read came back clean twice. -
Two pic sites still resolve an Unown off the species.
src/ui/gen2/PhotoStudio.lua:102andsrc/ui/gen2/EvolutionAnim.lua:460usedef.spriteFrontwithout going throughUnown.formSprite. For EvolutionAnim this is almost certainly dead (Unown does not evolve). For PhotoStudio it is genuinely unclear: the cart'sengine/events/print_photo.asmdoes not callGetUnownLetterat all, so the port may already match. Check before changing. -
Unown dex registration on obtain paths.
engine/pokemon/evolve.asm:310runsGetUnownLetterthencallfar UpdateUnownDexafterSetSeenAndCaughtMon, guarded bycp UNOWN. That is dex bookkeeping, not rendering, and it was never audited across every path that gives the player a mon.GetUnownLetterhas 20 call sites on the cart; the port covers the pic ones.
The 2026-08-10 mod-API round
Six lanes closed the Gen 1 / Gen 2 mod-API boundary and a batch of reported gameplay bugs. What that round closed, so nobody re-opens it:
- Fifteen seams that had a Gen 2 site but no entry in the parity gate are now
listed and asserted: the four
intro.oak_speech.*events plus theintro.oak_speech.buildhook,battle.overlay,battle.low_health_alarm,battle.catch_exp,pokemon.sprite,input.step,input.pointer,render.zones,render.compose,render.letterbox,render.hud. Five more (intro.boot.*) are listed as Gen 2-only.tests/engine/gate_gen2_mod_api.luais 893/893. - Two registries un-gated on Gold after their consumer landed:
battle_sprite_scales(src/ui/gen2/BattleState.lua:imageScale) andrender_pipelines(src/core/Game2.lua:loadinstallssrc/render/Pipelines.luaon the merged dataset aftermods:load). Both keep the SHARED Gen 1 target, so one mod record serves both games. 40 of the 46 registries are now available on Gold. hook:render.zonescame off thegate_meta_coverage.luaDEBT ledger, which is now four entries, all M7/M12 link and give-mon seams.
What that round left open, each verified on 2026-08-10:
-
render_pipelines'drawWorldhalf is inert on Gold. Gold's overworld draws straight to the window rather than into a canvas the waysrc/world/OverworldController.luahands one toPipelines.drawWorld, so a drawWorld-only pipeline renders nothing. It is not left switched on pretending:Game2:loadretires a restored level for one and re-applies Tilt from the option the exclusion just cleared, leavingoptions.pipelinesuntouched so the mode returns the day Gold grows a world canvas. Related:Pipelines.rowsis read only fromsrc/ui/OptionsMenu.lua, so a pipeline on Gold has a hotkey and no OPTION row. -
transitionsstays gated and should.src/ui/gen2/BattleTransition.luakeysSTYLESas a boolean SET of the four cart wipes (spin,speckle,zoom,sine), not the{ frames, draw, sound, flash }record the registry carries, and there is no styleDef lookup a mod id could reach -- a registered style would fail the membership test and fall back to vanilla. Un-gating it before that changes would be the silent no-op the routing table exists to prevent. Same forrulesets,field,text_pointers,link_fieldsandmap_scripts;docs/mod-api-gen2-compat.mdcarries the per-registry reason. -
src/core/Game2.lua:joystickremovedis half of Gen 1's.src/core/Game.lua:867doesself:recoverInput("joystickremoved", joystick)ANDTouchControls:joystickremoved(); Game2 does only the pad half, so a controller unplugged mid-hold leaves Gold's Input state stranded and a held direction walks forever. It was a total noop before, so this is not a regression -- it is a seam finished halfway. One line. -
Game2noops five joystick callbacks.joystickpressed,joystickreleased,joystickaxis,joystickhatandjoystickaddedare assignednoop, so a stick with no SDL game-controller-database entry reaches neither Input norGamepadMap.RAW_BUTTON_BINDINGSon Gold. Gen 1 handles all four (src/core/Game.lua:764-797). A DualSense is SDL-recognized and takes the gamepad path, so this is not the reported SELECT bug -- it is the raw-stick fallback. -
Game2:gamepadpressedhas no SELECT-held guard. Gen 1 (src/core/Game.lua:686-694) suppresses the shoulder GAME SPEED cycle while SELECT is held, because Select+L is a display chord on NX. Harmless today (Gold has no display chord) but the two paths have diverged. -
hideCallerBox's fallback path skips the screen seams.src/script/gen2/CallAsm.lua:217-231pops throughstack:popwhen the caller box is top (which it always is on a normal call, sincePhoneRingrunsclosetextbeforeInitCallReceiveDelay) but falls back to a baretable.remove(states, index)otherwise, which raises neitherexitnorscreen.popped. Only a mod screen pushed over the box can reach it, and then a listener that sawscreen.pushednever sees the pop. -
src/core/gen2/Roamers.lua:338says the wrong thing. Its comment says "the sharedencounter.specieshook still runs downstream and is where a mod changes what appears". It does not:World:tryWildEncountercallsstartBattleon a roamer hit and RETURNS before it reachesWorld:rollEncounter. The doc's partial-coverage list is right and the comment is wrong; correct the comment, not the doc. -
src/world/gen2/World.lua:4318still calls the pack's SEL row something "this port has not built". The pack submenu now offers SEL. Stale comment.
Three partial coverages that are documented and still true, repeated here
because "the hook exists" is not "the hook sees everything":
encounter.roll / encounter.species are not wired into World:tryHeadbutt,
World:rockMonEncounter or the roamer path; src/ui/gen2/BattleState.lua
builds a flat opts for Catching.attempt with no data in it, so a
mod-registered ball is readable through Catching.recordFor but is not
resolved at the real throw site; and no Gen 2 UI file reads
Battle.statusRecordFor(data, status).hudLabel, so a mod status shows no label
in the battle HUD, the party menu or the summary page.
Open: cleanups declined on purpose
These were skipped with reasons during the wiring pass. They are listed so the reasons survive, not because they are pending work.
-
Vm:showRaw(body, stay)was proposed and declined.showRawHeldinSpecials.luaalready answers the text lookahead correctly, so this trades one working shape for another. -
tests/drivers/gold_egg_hatch.luasummary push was declined as redundant:gold_egg_hatch_shots.luaalready photographs both the cutscene and the summary page. -
Two
Battle.luadivergences were filed as notes by the file's own owner rather than fixed, because no reported symptom drives either: text printed for an already-statused target, and Future Sight taking STAB, type effectiveness and weather. Both are real divergences from the cart. Fix them when something actually depends on them.
Reported, investigated, and NOT defects: do not re-chase these
Each of these came in as a bug report and came back disproved against the asm. Re-opening one costs another full investigation, so the reasoning is kept here.
-
SFX pointer table misalignment. The table is aligned end to end. 188
dbarows with no skips or padding, 188 constants, verified through the manifest scraper, the extractor's*3stride, and 8 ids spot-checked against the live cache. Two things that look like misalignment are how the cart is built:SFX_GET_EGG_UNUSEDandSFX_GET_EGGboth point atSfx_GetEgg, andSfx_ReadText/Sfx_ReadText2share one header address. Do not repoint, re-stride, or add an offset to the SFX, music, or cry tables. -
Bind letting you pick other moves. This is Gen 1 behavior and does not hold for Gen 2.
BattleCommand_TrapTarget(effect_commands.asm:5568-5605) writes only the TARGET's wrap count and trapping move. It touches nothing on the user, so the user is free to switch moves. The port is correct. The real Gen 2 move locks (Rollout, Thrash, Petal Dance) were genuinely missing and have been added. -
The Slowpoke Tail salesman not blocking progression. He is Route 32's
FISHER4at (7,70), not an Azalea Town object, and the cart's refusal arm only prints._OfferToSellSlowpokeTailrunssetscene SCENE_ROUTE32_NOOPfirst, so the coord event never fires again. There is no pushback to port. -
The rival being named BLUE. Not reproducible. The whole chain is intact: special 36 resolves to
H.NameRival,NamingScreen:accepthands the typed string through, and BLUE is the Gen 1 default that Gold never reaches. A separate real defect on this seam (the rival being pre-named SILVER before the officer asks, and the blank-entry fallback) was found and fixed. -
Not Very Effective dealing 0 damage, status not shown in the battle UI, and the Pokedex missing from the Start Menu all came back
already_fixed: the damage floor isMIN_DAMAGEadded after the cap and before the type multiply, the HUD prints the status tag where the level goes, and the menu row is gated onsave.engineFlags[11]correctly.
Standing risks, not breakage
-
Four Gold test files are wired into no runner, so a regression in them will not turn
run_tests.luared:gold_flag_names_test,gold_route_validate_test,gen2_pokegear_unlock_test,gen2_save_export_test. All four pass standalone. The first two are the pre-run route validators documented inKANTO-CONTINUE.md; the last two need aGOLD_CACHEand are excluded by an in-file comment. This is deliberate, but it is a coverage hole and worth knowing. -
mods/must stay clean. The 2026-08-10 round left temporary probe mods behind mid-run (mods/tmp_menus,mods/zz_verify_seams); both are gone now andmods/holds onlyexample_mew_starter,example_silly_oak,examplesandnuzlocke. A straymods/tmp_*loads on every Gold boot and changes what a driver measures.mods/example_silly_oakcarries only a.modkitignoreand warns "manifest.json does not exist" on every boot; that is pre-existing (27 Jul) and unrelated. -
tests/drivers/gold_opaque_surround.luais stale. It readsgame.stack._items;src/core/StateStack.luahas only.states. That branch is covered instead bygold_center_pcand the letterbox-per-frame count ingold_frame_seams. -
luacon this machine is Lua 5.5, soluac -pis a weak proxy for the LuaJIT/5.1 semantics this engine targets. Useluajit -b <file> /dev/nullfor a real syntax gate. -
docs/rfcs/0001-surfing-pikachu-sprite.mdand0002-screen-render-visible.mdare deleted in the working tree, and both are still referenced by name from live modkit case files that pass. Those deletions predate both parity runs. Worth resolving before a commit.
The failure mode that actually bit, worth remembering
Round 1 reported the Yes/No dialogue fix as landed. It was not. Two of its three
lanes wrote their half, and the third left the hook closure at
World.lua:828 declared as function(body, onDone), so the third argument
Vm:resume passes was silently discarded and the entire stay implementation
in World:showText and World:askYesNo was dead code. Lua drops extra
arguments without complaint, so nothing failed and every suite stayed green.
Round 2 caught it only because an investigator probed the boundary empirically instead of reading both sides and assuming they met. When work is split across agents by file, the seams between the files are where the bugs live, and a green suite does not prove a seam is connected.