Repository navigation
v0.3: special battle menus, NPC lists on top, steady band, L+R fix, macOS and Linux patchers - #4
Merged
Merged
Conversation
…M overrun The Windows devkitARM path is now only the last resort, so the payload can be rebuilt anywhere binutils are installed. Rebuilding v25 with stock binutils 2.42 reproduces onescreen/payload/hook.bin byte-for-byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
…mpilation
Renames only - the assembled payload is byte-identical to v25. Constants
that described a guessed role now carry the struct and field the full
decompilation (ctrl-cheeb-del/pokeheartgold @77565981) gives them:
FIELD_MENU_OFF (+0x1C) -> FieldSystem.savedBottomScreenAppId
FIELD_TOUCH_TASK_OFF -> FieldSystem.bottomScreenTask
FIELD_OUTER_* -> BottomScreenAppManager.{appId,state,...}
FIELD_CHILD_* / +0x394 -> TouchscreenChoiceMenuData.{state,...,selection}
LIST_MENU_OFF (+0x3A0) -> TouchscreenChoiceMenuData.selectionCallbackArg
and the comments say why +0x39C held a code address (selectionCallback is
MenuExec_PrintItemHelp). Every offset was recomputed from the decomp's own
headers and agrees with the value measured from savestates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
The full decompilation (ctrl-cheeb-del/pokeheartgold @77565981) gives the compiler's own struct layouts. Checked against it, and against what the hook had measured from the game: Battle - menuCursor is BattleInput+0x6DC. The hook learned it at runtime from a start of 0x6D4 (scrollXEnd); the learner is gone, and with it a read of freed memory while the bag was open. - BattleInput is freed whenever BattleSystem.graphicsMode != 0 (bag, party, catch naming screen). Every read now goes through OneScreen_BattleInput, which checks it and range-checks both pointers. bt_system is only published while Battle_Run is in BSTATE_BATTLE_MAIN. - Menu ids 5..8 are the first rival (FIGHT only) and Safari (BALL/BAIT/MUD/ RUN) menus, which were drawn as FIGHT/BAG/POKEMON/RUN. They now swap. Menus - The list menu is read from TouchscreenChoiceMenuData.menu (+0x0C), not the callback argument at +0x3A0 that the move tutor never sets. - OneScreen_ListMatch also checks a hash of every predictable entry, so the Pokeathlon Dome's BUY/INFO/SEE YA no longer matches the shop. labels.py computes the same hash; list records grow to 56 bytes, the header to 528. - The X menu's normal layout is told by (inhibit & 0x190) == 0x180, not the RETIRE bit alone, which also passed the Union Room (drawn wrong). - The field / task / start-menu walk is range-checked at every link. Tooling and docs - tools/emu_hook.py + tools/test_hook.py run payload routines under unicorn against synthetic structs, including labels.py's records end to end. - docs/DECOMP.md cross-references every offset; AUDIT, FINDINGS, DEVELOPMENT and README corrected (overlay 5 is not the party menu, ov10, the cursor). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
The Safari Zone, Bug Contest, Pal Park, Union Room, Colosseum and Battle Tower partner room each arrange the overworld menu differently, and the variant lives in the touch menu app's own data, which this patch had called out of reach. The full decompilation names it (TouchscreenMenuData, src/touchscreen_menu.c) and the path to it is the bottom-screen manager the field prompts already walk. OneScreen_StartMenu now reads, from the live touch app: +0x51C layout:4 which of the seven grids is up +0x470 icons[i].shown which slots exist +0x14 selection the selected slot all four confirmed in pret's retail assembly of overlay 27. Cells are slots, so the panel matches the touch screen, highlight included, in every context. The inhibit-flag reconstruction stays as the fallback for frames when the touch app is switching. labels.py emits one strip row per layout from the game's own table (sTouchscreenMenuLayouts), after checking that table is really in the ROM's overlay 27, plus CHAT/LOG strips for the Union Room when they fit the existing cells. A layout it cannot label is marked undrawable and swaps, as before. OneScreen_BottomApp / OneScreen_TouchMenu validate every link of the walk, including bottomScreenType, since type 6 parks a photo viewer in the same slot. tools/test_hook.py drives the whole routine against synthetic touch-menu data and checks the tilemap it writes, cell by cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
…d v28) Review of the full decompilation (start_menu.c, Task_StartMenu_HandleSelection_ Retire) found that RETIRE in the Safari Zone frees the menu and jumps straight to the script runner, an ARM9 task. The latch stayed up, OneScreen_StartMenu returned 0 because the task was no longer the menu, and Poll swapped - hiding the question the script prints on the world before it takes the bottom screen. Poll now only swaps for an undrawable menu while Task_StartMenu is still the field's task (the save box); otherwise it wipes the panel and keeps the world. The live layout is also cross-checked against the start menu's own inhibit flags (sm_layout_class); if they disagree, the flag-based fallback decides. Fixes the gSystem.newKeys comment (+0x48, not +0x44). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
…d v29) HGSS kept the top-screen list window it inherited from Diamond and Pearl. The full decompilation shows script command 752 (ScrCmd_MenuExec, the touch list, 146 sites in 49 scripts) and command 71 (ScrCmd_ScriptListMenuExec, ScriptMenu_ShowList on MAIN_3) read the same ScriptMenu, built by the same MenuInit/MenuItemAdd commands. The patcher now points table entry 752 at OneScreen_MenuExec. A list the patch draws itself (shop, PC) or cannot vouch for (two-line or formatted entries, the choice app not idle, a pointer it cannot follow) goes to the original command. Anything else is shown by ScriptMenu_ShowList, with OneScreen_WaitList as the native wait: it maps ListTask's B (0xFFFE) to the last entry's value, which is what the touch list returns and what scripts like the apricorn juice stand and the fossil menus expect. x is pulled left if the window would run past column 31, and a stale themed panel is wiped before the game's window takes tiles from 0x3D. regions.resolve_native_lists finds gScriptCmdTable by shape and decodes the calls and literal pools of entries 752 and 71, cross-checked against ARM9 addresses that do not move between regions and ShowList's retail prologue; any mismatch leaves the table alone. --touch-lists (CLI) / 'Old-style NPC lists' (GUI) opts out. tools/test_hook.py: MenuExec's guards and fallback, WaitList's B mapping, the x clamp, and the finder against assembled synthetic Thumb code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
… text (payload v30)
The full decompilation shows why the band behind Oak's selected answer
flashed. OakSpeech_InitMsgPrinter starts a PaletteBlink on top-screen BG
palette 6 to animate the bottom-screen icon (colours 11-14). It runs on the
main SysTask queue after the exec trampoline, and on ticks 0 and 24 of its
cycle writes all sixteen colours of palette 6 from its own copy - so the band
(colour 12) went back to the game's 0x62FF, then to white, about once a
second. Moving the band to another index would not help: every colour is
rewritten.
OneScreen_OakBlink pauses the blink (state 2) in states 65/69/98, where the
icon it animates is suppressed anyway, and restarts it (state 0) when they
end - only if it paused it, never END. The struct is validated first: main
RAM, palette 6, top screen, no PaletteData, apply == 0x020165A5
(PaletteBlink_LoadMainBgPltt, ARM9, region-independent).
The same research found the hook blitting all-paper over the dialog's
columns 24-26 on every non-prompt frame. With the US text five of Oak's lines
run past x = 192 and lost their last letters, and the tutorial's own icon was
painted out. The {YESNO} control rewrite already handles the icon, so the
per-frame wipe is replaced by a one-shot take-down of our own box.
Tests: the hold/release cycle and every validation guard, the state sequence
through OakExec, and a monologue frame leaving the text tiles untouched
(confirmed to fail against the old wipe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
Mom's savings menu opens a 16x8 savings box at (1, 1) on MAIN_3, drawn from tile 1, then asks for a list at (1, 1). The game's top-screen list window would sit on top of it and take tiles from 0x3D, inside the box's own - harmless on the touch screen, where that list used to go. OneScreen_MenuExec now sizes the window the way ScriptMenu_ShowList will (anchors included) and asks OneScreen_ListRoom, which reads MAIN_3's tilemap buffer: any cell inside the list's rectangle or its frame, or any cell using a tile the window would be given, keeps the touch list. A themed panel left from the previous menu is cleared before the check. Payload v31. New emulated test covers the savings box, the money box, the dialogue box, frame edges, tile reuse, anchoring, a missing buffer and a held panel; each check was mutation-tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
The CLI, the window title and the frozen executable's name all take it from onescreen/__init__.py. The README banner says what has and has not been played: v0.2b in full, v0.3's changes only under emulation so far. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
A review of every doc claim against the code and the full decompilation found 26 inaccuracies; each was checked again by a second reader and in the scripts before changing it. - NPC lists: the Name Rater has no list, the move tutors build theirs with command 653 (not redirected), and the day-care's withdraw list has a CURSOR_X level column, so none of them uses the game's own top-screen list. Say which lists do, and every case that still swaps: two-line or formatted entries (about 40 sites, not 29), a box in the way, the tutors. The Pokeathlon marts now get the native list rather than swapping. The B remap is justified by the juice stand and fossil menus testing for the last value; the rankings never reach it. - README: v0.3 is emulation-tested only; the CLI's theme default; US HeartGold is tested; Pal Park battles swap too; the party menu was never asm. - USAGE: Oak's band takes the theme's colour; four inheriting apps were wrong, not three; where the tested SHA-1s live. - AUDIT: newKeys is +0x48; the helper is 0x02022D3C; the hook already writes screensFlipped for the PC box. - DEVELOPMENT: the hook's other entry points, a 23-word config probe, what build.py leaves out, and that new apps go in regions.APP_TABLE. - FINDINGS: the two script-command families, the bag/party freeing BattleInput, the special-zone caveat, the day-care's one list. Two hook comments reworded; the payload is byte-identical. The patcher's "untested dump" note now names IPKE as tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
An independent check of the previous commit against the code, the decompilation and the other docs found 14 more places to fix: - The BUY / INFO / SEE YA mart is scr_seq_0003_056/_060, which nothing in the decompilation calls (std 2056/2060). The Dome's live counters are PRIZE EXCHANGE / INFO / SEE YA!, whose French text is unknown, so the docs no longer claim the Dome was drawn as the shop. - Of the 19 inheriting apps only Oak's speech was wrong; the evolution scene, fly map and move relearner are outside that list. - README: the intro's list exceptions match USAGE; "checked under emulation ... not in an emulator" says what was actually run. - FINDINGS: the candidate-menus section predates the hash and the native list; the Name Rater and tutors are not list menus. - Comments in hook.s, regions.py and labels.py that still carried the old claims (config size, bag/party curMenuId, Oak's band, the command 71 family, APP_TABLE's tuple). The payload is byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8h8BWTPs3RVMEyUGXnBN1
… and the tutors on top (payload v33) The selected band (v32). Battles and the evolution scene keep a PaletteData with autoTransparent set, and their VBlank callbacks push the main-BG transparent buffer over palette RAM every logic frame, so a band written only to 0x05000178 was overwritten one video frame in two. Both scenes also run a PaletteBlink on palette 11 that rewrites colours 11-14. OneScreen_BandBuffer writes the band into the pushed buffer as well and puts the game's own colour back from the opaque copy when the box comes down; the battle's message-icon blink is held paused while the commands or a yes/no box are up, unless the bag or party has taken over. The evolution scene pauses its own blink at whatever point in its cycle it happens to be, which a quarter of the time left colour 12 white behind Oui / Non; the buffer write covers that. Lists (v33). Command 752 now measures each entry the way the text printer lays it out - CURSOR_X sets an absolute x - folds two-line entries onto one line, and opens the window with ScriptMenu_OpenListWindow directly. Command 653, the Battle Frontier move tutors, is redirected to OneScreen_TutorExec. The patcher finds OpenListWindow, FontID_String_ GetWidth and command 653 by shape; the config block is 24 words. Checked against French SoulSilver: BattleSystem.palette +0x28, msgIcon +0x1B0, EvolutionTaskData +0x14 / +0x58 and the PaletteData buffer layout are what the retail code uses, and every new list finder resolves. tools/savestate.py's variable list caught up with the hook's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…load v34) The 19 applications that set no POWCNT1 of their own were each read in the full decompilation. "Sets nothing" missed the commonest way an app routes itself - screensFlipped = TRUE and the shared GfGfx_SwapDisplay helper, which sends engine A to the touch screen and which the static flips must never touch. Two of them draw every window on engine A that way: the Pal Pad (overlay 43) and the rankings viewer (overlay 121). Both now have app-table entries, matched on the VBlank callback each installs, found by masked search in its overlay (one match each in IPGF). Both set the flag earlier in the same call, so the one-shot swap lands after it. The rest are covered, correct as they are, or online-only; docs/AUDIT.md has the verdict for each. - An unverified app-table entry gets callback 1, which never matches, instead of ending the table - so the entries after it, and the slot the hook reads by name, keep working. - regions._find_masked missed a match ending exactly at the end of an overlay, which could let a duplicate through the one-match rule. - Overlay 37 is the Union Room's group drawing, a stylus canvas; it stays on the touch screen like the trainer-card signature. - The patch log names overlays after main.lsf; 57, 59, 70 and 72 were mislabelled as battle code. - .github/workflows/payload.yml rebuilds the payload, checks it matches the committed one, and runs the emulated tests on every push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… menus (payload v35) BattleInput_CreateMainMenuObjects picks the command words by battle type, not menu id: BALL / BAIT / MUD in the Safari Zone, BALL for BAG in the Bug Contest, and CANCEL for RUN when BattleInput.cancelShown is set - a double battle's second Pokemon. Pal Park's menu (9/10) and the first rival's (5/6) are the same objects with words hidden. OneScreen_BattleImage reads BattleSystem.battleType (+0x2C) and cancelShown (+0x68F) and picks the matching set; it is the one place both AutoBattle and BattleDraw ask, and a set the blob does not hold - or no blob at all - now swaps instead of drawing nothing. Room: a set was four 1792-byte images and the blob had under 1 KB spare. A selected image is the plain one with the paper inside the selected word's rectangle turned to band, so a banded set stores one plain image and four rectangles, and OneScreen_BandRect lays the band in VRAM after the copy, a word at a time (0xF -> 0xC clears a nibble's low two bits). The command set went banded too. The extra records live after the start-menu plans (OneScreen_GeoRecord); the blob is 22284 bytes of a 22912 reserve, with 72 bytes of ITCM left. labels._banded_set refuses any set whose banding does not reproduce _render exactly, and tools/test_hook.py compares every image the hook draws with _render byte for byte; with French SoulSilver's own font and words, all 21 match. The new test caught a set left out (no images) passing an unsigned bounds check. tools/testsaves builds play-test saves from a complete one with PKHeX.Core, then puts back the RTC date PKHeX rewrites and re-checksums the game's way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/build_exe.py builds for the system it runs on - PyInstaller cannot cross-build: a one-file .exe on Windows, an .app bundle and a .zip of it on macOS, a one-file binary and a .tar.gz of it on Linux. --cli builds patch.py the same way, as a command-line program with the same arguments. On macOS the .icns is made from the committed PNG with sips and iconutil, Info.plist gets the version, and the bundle is signed ad hoc again after that edit. The .tar.gz archives keep the executable bit, which GitHub's artifact downloads drop. .github/workflows/release.yml runs it on Windows, Ubuntu and macOS on both Apple silicon and Intel, checks the command-line build starts, and for a v* tag drafts a release with every build. requirements-build.txt pins the versions the macOS build was made and checked with. patch-gui.py no longer fails to open when tkinterdnd2 imports but its library cannot load - it is compiled for Tcl 8, so any Tk 9 Python hit this. The window now loads tkdnd into an ordinary root and opens without drag and drop if that fails. Checked on macOS 13 (Intel) with python-build-standalone 3.12.12, Tk 8.6: the .app's signature verifies, it launches and quits cleanly, drag and drop loads, and the command-line build patches French SoulSilver into a ROM byte-identical to `python patch.py`. The Windows and Linux builds and the workflow itself have not run yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two shoulders of the L+R swap rarely go down in the same frame, so the game acted on the first one alone: a bag pocket or a Pokedex page, and under HGSS's L=A button mode an A - which in a battle chose FIGHT before the screens swapped (play-test D8 and I7). OneScreen_ReadKeys now wraps the main loop's `bl ReadKeypadAndTouchpad` at 0x02000DB4. A lone L or R is held back for three frames; if the other arrives it is the swap, and neither key reaches the game until both are up; if not, the press goes through then, or on release for a quick tap. Only the cooked key words, and the raw edge words, are rebuilt; heldKeysRaw is left as the pad has it for the next frame's edges and the soft reset. ApplyButtonModeToInput runs again on what was rebuilt, and frames with the lid shut, when the game leaves the raw words stale, are left alone. inject.patch_key_read checks both the hooked call and the button-mode call at 0x0201A556 before patching. OneScreen_Poll no longer reads L+R. Room: the yes/no sets and the gender row are banded like the command box, one plain image and a rectangle per choice, which brings the label reserve down to 20864 bytes and leaves 1936 bytes of ITCM. The old-style NPC lists option - --touch-lists and the window's checkbox - is gone; it was never in a release. A ROM whose list functions cannot be vouched for still keeps its lists on the touch screen. tools/test_hook.py: 58 tests, 9 new - the shoulder filter in both of HGSS's button modes (NORMAL and L=A; START=X and X/Y swap are unreachable there), quick taps, the lid, other keys passing at once, the hook-site check, and every two-choice set drawn byte for byte against _render. Removing the lid check, the button-mode re-apply or the window each fails a test. The patched French SoulSilver's calls at 0x02000DB0 and 0x02000DB4 land on OneScreen_Frame and OneScreen_ReadKeys. Play-tested on v36: L+R on the battle menu, in the bag and Pokedex, and elsewhere. Quick taps in NORMAL mode are still to be checked in game. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/release-notes/v0.3.md is the text for the v0.3 release, in the shape of v0.2b's: getting started with the file for each system, what is new, what was played and what was not, and the known limitations. For a v* tag, release.yml now checks out the repository and drafts the release with docs/release-notes/<tag>.md as its text and every file's SHA-256 under it, titled "1ScreenHGSS <tag>"; a tag without a notes file still gets GitHub's generated notes. The step's script was run locally with a stand-in gh for both cases; the workflow itself has not run yet. README: v0.3 has been played on French SoulSilver, so the banner and Status say what was played and what was not; the battle and X-menu paragraphs mention the special zones and CANCEL; Development mentions the emulated tests, CI and the release builds. USAGE: macOS 15 needs Open Anyway in Privacy & Security rather than Control-click. CHANGELOG: V0.3 is no longer marked unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything for v0.3: payloads v26 to v36, the standalone patcher for macOS and Linux, and the release workflow.
CHANGELOG.mdhas the details;docs/release-notes/v0.3.mdis the text the release will carry.What changes for players
Battles
Menus and lists
ScriptMenu_ShowList), including formatted lists, price columns and the Battle Frontier move tutors. A list that would land on a box the script already has up keeps the touch screen.docs/AUDIT.md).Oak's speech
L + R
OneScreen_ReadKeyswrapsReadKeypadAndTouchpadat0x02000DB4and holds a lone L or R back three frames for its pair; the chord is hidden from the game until both keys are up.Patcher
tools/build_exe.py). The window no longer fails to open on a Tk 9 Python.--touch-lists), added after v0.2b, is removed: lists always use the game's top-screen window when the patcher can vouch for it.Development
tools/test_hook.py: 58 tests that run the hook's routines under unicorn against synthetic game structures, including every drawn label checked byte for byte against the patcher's renderer..github/workflows/payload.ymlrebuilds the payload, checks it matches the committed one and runs the tests on every push.release.ymlbuilds every patcher for av*tag and drafts the release withdocs/release-notes/<tag>.mdas its text and the files' SHA-256.tools/testsaves/builds play-test saves with PKHeX.Core.Testing
payloadCI passes on this branch.release.ymlhave not run yet. It can be run by hand from the Actions tab after the merge, before tagging.After merging
Tag
mainasv0.3and push the tag;release.ymlbuilds everything and drafts the release for review.🤖 Generated with Claude Code