Silver: derived import manifest (tools/make_silver_manifest.py re-resolves
the Gold manifest's symbols from pokesilver.sym), silver GameVersion row,
generation-keyed extractor routing, required-files override, edition save
stamping (a Silver playthrough no longer writes into the Gold save),
checkver-driven edition data, SILVER/KAMON/OSCAR/MAX presets, GOLD rival
default, edition credits banner, Lugia title screen (OAM layouts, bob,
trail, palettes as title.lua data keys with Gold defaults so old caches
need no re-import), packaging for every build target, docs, and tests.
Launcher: the installed-mods list is one continuous scroll (rows culled to
the viewport) instead of a pager with an inner scroll viewport; the pad
cursor's edge-scroll no longer runs it to the bottom. The game dropdown
shows just the initial and caret. Find-tab behavior unchanged.
Title tempo: a sprite-anim frame shows duration+1 ticks
(engine/sprite_anims/core.asm GetSpriteAnimFrame), which locks both
editions' 64-tick wing beat to the 64-tick sine bob; the title screens no
longer run fast and out of phase.
Several hand-ported scripts carry pokered dialogue as inline English
literals instead of reading game.data.text, because the real ROM
label was never reachable from data/generated/text.lua:
ViridianCityYoungster2OkThenText/CaterpieAndWeedleDescriptionText,
TMNotebookText, the SS Anne kitchen cook's three dish lines, and the
Viridian fisher's pre-gift line (data/scripts/story5.lua's gift()
already read t[label] here, just had a stale comment and a missing
fallback).
Traced the actual cause carefully -- there are two independent,
differently-behaved label scanners in this codebase:
- tools/extract/text.py's parse_text_file() requires a label to
start with "_" to be collected. This is a real bug (confirmed
against a real pret/pokered checkout), but this function has no
callers anywhere in the tree and no __main__ entry point -- it
looks like dead code left over from an earlier version of the
pipeline.
- The function that actually produces the shipped label list is
text_metadata() in tools/make_rom_manifest.py, which feeds
manifest["text"]["labels"], which build_rom_data.py's
extract_text() iterates to decode each label straight from the
ROM. text_metadata() already uses the permissive regex (no "_"
requirement) since commit 0f581e2f.
So the actual blocker is that the committed tools/rom_manifest.json
was stale relative to text_metadata()'s current code, not a source
bug. Verified by rebuilding pret/pokered from source with RGBDS
(reproducible -- the resulting pokered.gbc/pokeblue.gbc hash to the
same canonical SHA-1s gen1recomp already pins, so no cartridge dump
was involved anywhere here) and running the real, unmodified
make_rom_manifest.py against it: 2595 labels against the committed
manifest's 2585, a clean superset containing everything these
scripts need. SilphCo2FSilphWorkerFPleaseTakeThisText is the one
exception already in the manifest -- confirmed by commit 0f581e2f
("so many bugs i cannot even breathe") that it was hand-patched in
exactly this same targeted way, for issue #393.
Fix:
- tools/extract/text.py: relaxed parse_text_file()'s regex to match
text_metadata()'s, for consistency (no effect on what ships, since
nothing calls this function, but no reason to leave a legacy copy
of the same scanner out of sync).
- Four scripts read the real label first (t[label] or fallback, the
established pattern): celadon_eevee.lua, ss_anne_kitchen.lua,
viridian_city.lua, story5.lua (comment/fallback only, lookup was
already correct).
- tools/rom_manifest.json and tools/rom_manifest_blue.json:
regenerated for real -- both files are the direct, unedited output
of running make_rom_manifest.py/make_blue_manifest.py against a
real pret/pokered checkout at POKERED_REVISION, not hand-assembled
or reverse-engineered to match. Only safe because of the two fixes
below, which exist specifically so a real run doesn't regress
anything the previously-committed files had. Diffing a real
make_rom_manifest.py run against that previous file (1143 lines
out of 45253) found exactly what a naive "just regenerate against
whatever pokered HEAD is handy" would have silently broken:
- pret/pokered commit 079d1cc92fc3b0ec82bc1418c2b4045bfca84620
(PR #596, 2026-08-06) renamed
_SilphCo10FGiovanniILostAgainText/_SilphCo10FPorygonText to
_SilphCo11F... (they live in text/SilphCo11F.asm, Giovanni's
floor). data/scripts/victories.lua:183 still hardcodes the old
name, and extracting under pokered's new name would silently
blank Giovanni's "I lost again!?" rematch line. Fix is the pin
below: POKERED_REVISION is pinned to the last commit before this
rename, so today's generator output matches victories.lua
natively, no engine code touched, no generator-side workaround
either. Advancing the pin past this commit is a real, welcome
future upgrade -- it just needs victories.lua's labels (and
anything else's) fixed up in the same change.
- trainerPartyOverrides.OPP_CHIEF (Giovanni's Celadon gym team)
wasn't produced by any code under tools/ at all. Traced why:
pret/pokered's data/trainers/parties.asm has "ChiefData: ; none"
-- the Celadon Chief's battle is unused/cut content in the
original game, and RomExtractor.lua's own comment confirms
gen1recomp reimplements it as a real fight using a hand-authored
party for exactly that reason -- no pokered commit, old or new,
will ever produce this data. Added a TRAINER_PARTY_OVERRIDES
constant to make_rom_manifest.py so this survives every future
regeneration automatically; verified it reproduces the committed
value byte-for-byte and flows through to Blue/Yellow for free
via their existing derive-from-Red path.
- trainerHeaders.MtMoonB2F's Super Nerd slot is pre-existing
fabricated data, not pokered drift: his event name,
EVENT_BEAT_MT_MOON_3_SUPER_NERD, has never existed in pokered at
any point in its history (the real name,
EVENT_BEAT_MT_MOON_EXIT_SUPER_NERD, has been stable since 2015),
nor in gen1recomp's own event_flags.lua; trainerDefeated()
checks defeatedTrainers[npc.id] first, the same pattern already
used for the Fighting Dojo's Karate Master, so this entry is
very likely already inert. field.seafoam also differs from a
fresh regeneration (two showObject boulder-toggle IDs in the B3F
puzzle, a live gameplay system nobody has verified either value
against), and field.tradeArt is new content a fresh extraction
produces that was never shipped. None of those three are this
PR's problem to fix, but a real generator run has to do
something with them regardless -- so make_rom_manifest.py gets a
new apply_known_nonreproducible_overrides(), called right after
text_metadata()/field_metadata(), that pins MtMoonB2F and
seafoam back to what was already shipped and drops tradeArt,
each with a comment explaining why and what the real fix looks
like (MtMoonB2F needs a Data:seedMtMoonB2FSuperNerd()-style
engine seed, not manifest data). Verified this override function
was complete and correct -- diffed a real run against the
previously-committed file first, empty -- before trusting it to
write tools/rom_manifest.json/_blue.json directly; both are now
literally that generator's output, not hand-assembled.
tools/rom_manifest_yellow.json isn't touched by this PR at all:
it already had all ten labels, and make_yellow_manifest.py has
no matching override yet for its own field.oldManBattle outlier,
so regenerating it for real isn't safe the same way yet.
- tools/make_rom_manifest.py: added a POKERED_REVISION pin --
cf621a76d4941c93c078eb38e0880fe8db48ef40, the last pret/pokered
commit before the Silph Co rename above, chosen deliberately
rather than current HEAD -- and a check_pokered_revision() guard
main() calls before generating: fails loudly if --pokered isn't
at that commit instead of silently absorbing whatever upstream
renames or restructures since, with an explicit
--allow-revision-mismatch escape hatch for a deliberate pin bump.
That's the intended way this pin moves forward: diff a fresh run
against the committed manifest, fix up whatever engine code
depends on by exact name, and bump POKERED_REVISION in the same
change -- a conscious, reviewable decision instead of a silent
contributor default. Wired the same guard into
make_blue_manifest.py and make_yellow_manifest.py for their own
--pokered/--pokeyellow checkouts; make_yellow_manifest.py gets its
own POKEYELLOW_REVISION (e6ba56989b0f2694f393e6924820be11dcc1fbb8,
verified here). The pin and its guard live entirely in the
generator source, not also embedded as a field in the shipped
manifests -- that would be redundant with the .py constant next to
it in the same commit, for no protection the guard doesn't already
give.
- tools/make_rom_manifest.py, make_blue_manifest.py,
make_yellow_manifest.py: switched json.dump(..., ensure_ascii=
False, ...) to ensure_ascii=True to match how the committed
manifests were actually encoded (escaped \uXXXX rather than
literal UTF-8). Purely cosmetic -- json.load parses both
identically -- but needed so that a fresh make_rom_manifest.py run
at POKERED_REVISION now produces tools/rom_manifest.json
byte-for-byte (plain diff empty), not just content-equal. One
small, pre-existing cosmetic mismatch remains and wasn't chased: a
single nested dict, field.cardKeyDoors.doors, has its
SILPH_CO_10F/11F keys in natural floor order in the committed file
instead of the sort_keys=True lexicographic order everything else
in the file uses -- same content either way.
- tests/rom_manifest_generator_test.py: new ROM-free unit tests (same
style as the existing tests/build_rom_data_cli_test.py, wired into
scripts/test.sh as a T0 tier) for check_pokered_revision() and
apply_known_nonreproducible_overrides() -- matching/mismatched/
bypassed/unresolvable-checkout revision cases, and that the
MtMoonB2F/seafoam/tradeArt overrides land correctly (including
alongside a populated map entry, and without erroring when
tradeArt is already absent). Doesn't replace the manual real-ROM
verification above, which needs an actual pokered/RGBDS toolchain
-- but a future typo or logic slip in either function now fails
immediately instead of only surfacing next time someone happens to
redo that manual check.
- Left data/scripts/flavor/silph_co_9f.lua's nurse dialogue (labels
also added to both manifests here) untouched code-wise: static
command table, not a function, needs its
face_player/heal_party/fade state machine restructured to use
t[label] safely, and show_text's un-resolved-label fallback prints
the label name literally rather than English -- not safe without
interactive testing.
Checked Yellow's equivalent case (Melanie's House) since it looked
like the same shape: it isn't actually broken.
tools/make_yellow_manifest.py's YELLOW_EXTRA_TEXT_LABELS already
force-includes those eight labels, and a real built
dialogue_yellow.lua already has correct French translations for
them. tools/rom_manifest_yellow.json also already carries all ten
labels this PR adds to Red/Blue. No label changes needed there.
Tested: patched parse_text_file() against a real pret/pokered
checkout (+11 labels, 0 removed, all clearly dialogue-shaped);
rebuilt pokered.gbc/pokeblue.gbc with RGBDS at current pret/pokered
HEAD (confirmed hashes to gen1recomp's own canonical SHA-1s) and
diffed a fresh make_rom_manifest.py run there against the committed
manifest to map out what a full regen at HEAD would need; re-checked
out the same pokered checkout at cf621a76 (POKERED_REVISION),
rebuilt both ROMs again (same canonical hashes); with
apply_known_nonreproducible_overrides() and the ensure_ascii fix in
place, ran both generators to a scratch path first and diffed
against the then-committed manifests -- empty -- before running them
again writing tools/rom_manifest.json/_blue.json directly, so both
files are now the generator's literal, unedited output (git diff on
that final write: 18 lines moved in rom_manifest.json, exactly the
pre-existing SILPH_CO_10F/11F ordering quirk; zero lines changed in
rom_manifest_blue.json); rebuilt pokeyellow.gbc the same way, confirmed its
canonical hash, and confirmed make_yellow_manifest.py's symbols/
text/trainerHeaders/trainerPartyOverrides also come out byte-for-
byte identical to the committed tools/rom_manifest_yellow.json; ran
the real build_rom_data.py --only text against all three rebuilt
ROMs and confirmed every added label decodes from real ROM bytes
matching the English fallback literals exactly; verified
check_pokered_revision() actually raises on a deliberate mismatch
before relying on it to gate the runs above; python3
tests/rom_manifest_generator_test.py: 9/9 pass; luajit
tests/run_engine.lua: 250/250 suites pass.
A fixed cutoff (e.g. "<=200 is ink") only makes sense for sprites with a light background to split against; a mostly-opaque 16x16 icon has almost no pixel above that cutoff, so every such icon collapsed onto the same "all ink" hash and was flagged as a near-duplicate of anything else that also collapsed -- which was most of them, boulder.png included. Thresholding against the image's own mean keeps the split meaningful (and roughly balanced) no matter how light or dark the source is.
bindGoldData points gen2Palettes, gen2Icons, gen2Pokedex, gen2Landmarks,
gen2Roofs and gen2Sprites at the extractor's own Gold tables through
loadGen, but never gen2Constants -- despite Schemas.GEN2 routing
`constants` to that same namespaced-and-differently-shaped category
palettes and icons are in. A save editor boot left data.gen2Constants
unset, so mod.content.constants:get(...) read an empty table instead
of the cart's ordered name lists, misreading the generation and
rejecting every record a mod shaped off it.
data.gen2Constants now goes through the same loadGen("constants") path
the other five already use, falling back the same way they do when no
ROM cache is active.
The save editor now routes Gold vs RBY through a generation adapter so boxes, items, events, and maps write the right fields. The launcher layout is shuffled a bit, and patch notes can come from the updater, a packed file, or the iOS sidecar.
Expose a non-blocking TLS socket API to mods (WSS clients) without bundling
any game-specific multiworld content.
Co-authored-by: Cursor <cursoragent@cursor.com>
subprocess.run(..., capture_output=True, text=True) with no explicit
encoding falls back to locale.getpreferredencoding(False) -- the OS
default codepage. On Windows that's a legacy single-byte codepage
(e.g. cp1252), never UTF-8. When the LuaJIT dump contains a byte with
no mapping in that codepage, subprocess's internal _readerthread
crashes with an uncaught UnicodeDecodeError in a background thread; the
thread dies silently and the caller gets back stdout=None instead of a
string, crashing one line later with
AttributeError: 'NoneType' object has no attribute 'splitlines'.
Concretely, the Yellow-side imported dataset contains:
"_ColosseumHeightText" -> "...6’8” tall!"
The right double quotation mark (U+201D) encodes in UTF-8 as E2 80 9D;
0x9D has no defined character in cp1252, so decoding as cp1252 fails
outright. Verified against the real imported dataset: the Red/Blue-only
dump has zero bytes outside cp1252's defined range; the Yellow dump has
exactly one, at this row.
UTF-8 is the actual encoding these dumps are produced in -- the driver
Lua sources are read/written as UTF-8 throughout this file, and LuaJIT
writes those source strings' bytes back out verbatim -- so passing
encoding="utf-8" explicitly at the three affected call sites
(run_loader, check_data_dump, dump_dataset) is a no-op on platforms
whose default codepage is already UTF-8 (Linux/macOS) and a correctness
fix on Windows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yellow's field.oakSpeech manifest carried only shrink frames, so
OakSpeech.lua's `oakGfx.demoSpecies or "NIDORINO"` fallback fired and
the opening speech showed Nidorino's sprite and cry instead of the
player's Pikachu.
- Stamp demoSpecies "PIKACHU" in the Yellow import manifest (source of
truth for fresh ROM imports and developer builds).
- Stamp it in make_yellow_manifest.py so regeneration keeps the value.
- Repair stale Yellow caches in Data:applyVersionedFieldData() with a
fill-if-absent block, matching the #617 oldManBattle RATTATA pattern.
- Add parity test (manifest carries PIKACHU; stale cache filled;
pre-stamped value left alone).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The mon inspector now has a NICKNAME section: a text field with Set / Clear, commit-on-Enter and discard-on-Escape.
Input is gated at the field instead of refused at commit. Only glyphs the game
font can actually draw AND that round-trip through a .sav are accepted, capped
at the naming screen's 10 glyphs, so a bad keystroke or paste never appears and
Set always succeeds. Chars like "@" (the Gen1 string terminator), "#" and the
dakuten kana have codec entries but no font tile so they are blocked rather than silently mangled.
This fixes the nil value returned when running validate or pack on Fedora 43. Since Love isn't running, luajit calls on an empty table. Providing a stub table resolves the nil error.
The DEX grid now defaults to Pokedex-number order with an A-Z view, set
from two chips in the header. Sorting is view-only: it never dirties the
save, resets the scroll, and no-ops on a re-click. Ops.dexList builds the
order deterministically and sorts partial mod records last. Tests pin the
orderings against the real generated data.
The launcher spent ~9ms per frame building and drawing, and the Find Mods
tab could hang the window for minutes. Both had the same root cause: a
retained UI tree rebuilt every frame, and blocking curl calls made from the
draw path.
Replace the vendored FlexLove engine (28.5k lines) with src/ui/kit/ (Kit,
Theme, Layout, Loader). The kit caches Text objects and all measurement,
allocates nothing in the steady state, and draws flat. Build+draw is now
under 1ms at every window size and on every tab (POKEPORT_LAUNCHER_PROF).
Move every network call off the render thread onto a love.thread pool
(src/net/Fetch.lua): mod index fetches, per-mod release checks, find-tab
stats, thumbnails and mod installs. Mod indexes prewarm at boot so the
Find Mods tab is populated before it is opened.
Paginate every list -- mods, find, save slots, settings, release notes,
versions -- with the page size derived from the real viewport height, so a
500-mod index costs what a 10-mod one does. Scrolling is gone.
Anything that waits now raises a non-dismissable loader; per-row background
work shows an inline spinner instead. The in-app updater moves to the top
right beside the settings gear and pulses when an update is waiting.
Theme is black with white outlines, no gradients or glows, and solid
colour-coded embossed buttons with bold labels. The game tabs keep their
cartridge colours. Everything is 1.3x larger. The save editor shares the
theme, and adding an item there is now a searchable pop-up like adding a
Pokemon.
Also:
- Reset rebinds, in Settings and under Touch Controls. Rebinds are additive
(Input:applyBindings layers them over the defaults), so there was no
in-game way to undo one.
- Launch options: --game red [--slot N] / POKEPORT_GAME boots straight into
a game for shortcuts and frontends, falling back to that game's tab when
its ROM is not imported.
Fixes found while porting:
- Ellipsis and letterspacing truncated bytes, not codepoints, so a
multi-byte mod name crashed the first frame on a Japanese index.
Measurement no longer throws on malformed input either.
- The new font set missed UiFont's kana fallback, rendering translated
builds as tofu.
- Fetch workers idle in Channel:demand() and LOVE waits for live threads at
exit, so the process outlived the window; quitting mid-download also
waited on curl's 300s ceiling. Shut the pool down in love.quit and bound
its transfer timeouts.
- In one column the save-slot card drew below the fold, over the footer,
with no scrollbar left to reach it.
The two FlexLove engine tests guarded a scroll manager and an auto-height
propagation bug that no longer exist; replace them with a kit suite covering
page bounds, viewport sizing and UTF-8 truncation, and retarget the NX test
to assert the dependency is gone rather than that its perf guards are set.
Opening Touch Controls dropped the launcher virtual cursor and swallowed
gamepad input while touch still worked. Share PadCursor with the save
editor, forward pad events, and centralize overlay handoff/resume so both
hosts park and re-arm the pointer cleanly.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring FlexLove launcher UI ("big ui moment"), iOS picker dismiss, and Metal updates into the Switch NX branch. Keep pack_love (with libs/), NX inbox/rescan paths, SwitchDiagnostics, and Save Editor PadInput; port Scan again labels into LauncherView.
Co-authored-by: Cursor <cursoragent@cursor.com>