Skip to content

v0.3: special battle menus, NPC lists on top, steady band, L+R fix, macOS and Linux patchers - #4

Merged
yabi4 merged 17 commits into
mainfrom
claude/determined-pascal-ch0ri8
Oct 22, 2026
Merged

yabi4 merged 17 commits into
mainfrom
claude/determined-pascal-ch0ri8

Conversation

@yabi4

@yabi4 yabi4 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Everything for v0.3: payloads v26 to v36, the standalone patcher for macOS and Linux, and the release workflow. CHANGELOG.md has the details; docs/release-notes/v0.3.md is the text the release will carry.

What changes for players

Battles

  • The Safari Zone, the Bug Contest, Pal Park and the first rival battle draw their own commands on the scene (BALL / BAIT / MUD / RUN, BALL for BAG, FIGHT alone), chosen by battle type as the game does. A double battle's second Pokémon shows CANCEL.
  • The selected band keeps the theme's colour in battles and after an evolution: it is written into the palette buffer the scene copies every frame, and the battle's message-icon blink is held still while a menu is up.
  • The cursor position comes from the decompilation instead of being learned, and the hook no longer reads the command menu after the bag, the party or the naming screen has freed it.

Menus and lists

  • NPC lists the patch does not draw itself open in the game's own top-screen list window (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.
  • The X menu is drawn in the Safari Zone, the Bug Contest, Pal Park and the Union Room in each one's own layout, read from the touch menu's own data; RETIRE keeps the world on top.
  • The Pal Pad and the rankings open on top; the Union Room's group drawing stays on the touch screen. All 19 screens that inherit their routing were checked against the decompilation (docs/AUDIT.md).

Oak's speech

  • The band behind his answers no longer flashes (the palette blink is held under his prompts), and the ends of his longest lines are no longer erased.

L + R

  • The swap no longer leaks into the game. OneScreen_ReadKeys wraps ReadKeypadAndTouchpad at 0x02000DB4 and 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

  • Windows, macOS (Apple silicon and Intel) and Linux builds of the window and of the command line (tools/build_exe.py). The window no longer fails to open on a Tk 9 Python.
  • The old-style NPC lists option (--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.yml rebuilds the payload, checks it matches the committed one and runs the tests on every push. release.yml builds every patcher for a v* tag and drafts the release with docs/release-notes/<tag>.md as its text and the files' SHA-256.
  • tools/testsaves/ builds play-test saves with PKHeX.Core.

Testing

  • payload CI passes on this branch.
  • Play-tested on French SoulSilver in melonDS: Oak's speech, ordinary battles, evolutions, the X menu, saving, shops, the PC, drink machines, the day-care, field Yes / No, the Pokédex, PC boxes, Fly / Dig / Escape Rope, the Pokégear, the opening cinematic, L + R, and the macOS patcher app.
  • Not played yet: the Safari Zone, Bug Contest, Pal Park and first-rival battle menus, double battles, the X menu in those zones and the Union Room, the Battle Frontier lists and tutors, the Game Corner, Mom's savings, the Pal Pad and the rankings. The release notes say so.
  • The Windows and Linux builds and release.yml have not run yet. It can be run by hand from the Actions tab after the merge, before tagging.

After merging

Tag main as v0.3 and push the tag; release.yml builds everything and drafts the release for review.

🤖 Generated with Claude Code

claude and others added 17 commits October 15, 2026 14:29
…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>
@yabi4
yabi4 merged commit f8f693a into main Oct 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants