trainers.battleTheme validated and merged onto the trainer record but was
never read: battle music came solely from data.audio.battle[kind] where
kind is computeMusicKind()'s final/gym/trainer/wild. Route both battle-
theme start sites through a single choke point:
- BattleState:playBattleTheme() cues Music.playBattle with the override
(self.trainer.battleTheme via battleTheme()), defaulting to the kind
when unset, so vanilla fights and #782's non-gym Giovanni are unchanged.
- BattleState:enter() and OverworldController:pushBattle() both call it.
- Music.playBattle gains an optional 4th song arg that overrides the kind
default, and real call sites now populate the music.select trainerId.
- Victory jingles stay kind-based: a custom battle theme has no derivable
win-variant.
New ROM-free T2 suite tests/engine/trainer_battle_theme_bug945.lua covers
mod load, override resolution, the choke point, and the nil-override
parity gate.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.
* Resolve Find Mods stats from each mod's GitHub repo when the feed lacks them
A FIND MODS row now shows download/date stats even when its feed publishes
none: the row fetches the mod's own GitHub releases through the same
cached ModUpdate.fetchReleases the MODS tab uses (six-hour options cache,
so an installed mod's repo is instant). Feed-published stats still win
when present; otherwise one repo is fetched per frame -- the thumbnail
budget pattern -- so opening the tab never stalls for the whole listing.
ModUpdate.statsForReleases is the shared resolver.
* Fix crash opening the Find Mods tab: rename the stats cache field
The resolver stored results in self._findStats, which collides with the
method of the same name: self._findStats resolves through the metatable to
the function, so the or {} guard never fired and indexing it crashed the
launcher the moment the panel built. State now lives in _findStatsCache.
* Fix Find Mods crash: require ModUpdate in the find panel
buildFindPanel called ModUpdate.statsLine without a local require --
only buildModsPanel had one -- so opening the tab indexed a nil global.
* Retry Find Mods stats after failed repo fetches
A failed repo fetch (hourly GitHub API rate limit, transient network error)
was memoized as resolved, so a rate-limited first visit left those rows
empty for the whole session. Failures now schedule a 60s retry; a 404 is
still permanent so a renamed or vanished repo is fetched once.
* Add the MODS tab sort options to the Find Mods tab
the launcher runs before Game:load, so #767's Strings hooks had nothing
filling the catalog and a restart could not help - the ordering is the same
every launch. reads lang/strings.lua from enabled mods only, data not the
entry chunk, sandboxed.
plus a plain pixel fallback on the ui faces, or the kana draw as tofu.
lets a translation keep chosen characters on the rom tiles instead of the ttf.
needed for japanese: sizing the font for kana makes latin narrower, which
knocks the party menu numbers out of line.
A mod index can now publish three optional per-entry fields -- downloads
(total across every release), first_release and last_release (ISO days) --
which the FIND MODS listing shows in the same gold line the MODS tab uses.
The fields are additive by design: feeds that carry them stay readable by
every build that predates them (schema_version stays 1), and feeds that do
not render exactly as before. ModUpdate.statsLine builds the shared line;
the MODS tab reuses it. Parser, formatting, and parse coverage are tested.
The MODS tab now shows each installed mod's total GitHub downloads
(summed asset download_count across all releases), its first and latest
release dates, and a Sort row (Name / Popularity / Release date /
Last updated) persisted in options.modSort.
The launcher already fetched per-repo release lists for update checks, so
the data rides the same cached fetch: parseRelease keeps download_count
and published_at, writeCache persists them, and a cache entry written
before the fields existed is treated as stale and refetched once instead
of hiding the line behind an old cache.
Bring feat/switch-nx up to date with origin/dev (72 commits). Resolve
Input/RomImporter conflicts by keeping GamepadMap (NX face remap + dual-path
gate) while adopting upstream joyBindings rebinds (#632) and Enable-all mods
(#647). Gate shoulder GAME SPEED hotkeys when Select is held so Select+L
display chords still work.
Co-authored-by: Cursor <cursoragent@cursor.com>
A manifest whose name, version, description, or category carries invalid
UTF-8 (a BOM, Latin-1 bytes) crashed the launcher's MODS panel, since
love.graphics.printf raises on invalid UTF-8. Manifest.validate now drops
invalid bytes and a leading BOM from those strings, in place so the
badge's raw.category read agrees.
Mac OpenMTP leaves ._*.zip sidecars that fail PhysFS mount and hide a good install; prefer FileData mount on Horizon and keep success notices when a sibling fails.
Co-authored-by: Cursor <cursoragent@cursor.com>
Makes several UI spots reachable for translation mods without forking the
engine. All behavior-neutral for the base game: Strings(x) is the identity
function when no catalog is loaded, and the metric dex fields are opt-in.
- ListMenu: draw the title via Strings() (covers bag/PC/box/fly/move lists)
- Menu / (see follow-up ChoiceBox): grow the box to the widest label so longer
localized labels don't overflow the frame
- OptionsMenu: wrap toggle values (ON/OFF, SET/SHIFT, WIDE/OG, text speed) and
the ruleset display name in Strings()
- Schemas: add optional dexEntry.heightM / weightKg
- DexEntryMenu: render metric height/weight when those fields are present,
otherwise unchanged (ft/in + lb)
love.filesystem looks for "mods/" in two places: the save directory, and
-- portable installs only -- the game folder, which CacheFs mounts. So a
player who unzips a mod next to the executable of an ordinary install,
which is where very nearly every other game would want it, gets no error
and no mod. The panel just comes up empty, with nothing on screen to
suggest the files are sitting in the wrong folder twenty centimetres away.
That is a hard failure to self-diagnose, and it is worse behind a
launcher: the install lives somewhere the player never opens, so "the
game's mods folder" is a guess to begin with.
The mods panel now looks in those folders before its first listing and
copies what it finds into the tree the game really reads, reporting what
it took in the notice line. It happens on open rather than behind a
button because the failure being fixed is one where nothing suggests
there is anything to press.
Looking is scoped: CacheFs.withMounted puts the folder on the read path
at its own mount point, runs the scan, and takes it straight back off.
Nothing a stray folder contains can shadow a game file or change what the
running game resolves, which is what makes it safe to point at a folder
whose contents nobody has validated. Adoption skips ids the game can
already see, so it is idempotent and never nags twice, and it leaves the
loose folder alone -- deleting files outside the save directory on the
player's behalf is not this code's call to make.
Which strays are worth taking is pure (LauncherMods.pickStrays), matching
how deriveList and locateRoot are already split out, so the engine tier
covers the rules without needing love. SaveData.gameFolders is the old
detectPortable candidate list lifted out unchanged -- portable mode is
just the case where one of those folders holds the marker.
Claude-Session: https://claude.ai/code/session_01JvEthuoNBPfxpvHUD9Pd4N
# Closed issues
CLOSES#17: Incorrect Character Visuals (Only with GBC filter)
CLOSES#23: Could you allow the player to change the order of the moves
CLOSES#24: Evolution music not playing during evolution
CLOSES#26: Standing on door glitch
CLOSES#27: Battle Intro text automatically continues
CLOSES#29: Visual bug when zoomed out
CLOSES#32: Team Rocket recruiter doesn't battle with you unless you speak with him first
CLOSES#33: Developer/Debug Console
CLOSES#35: Professor Oak's introduction Inaccuracies
CLOSES#36: Missing Pokemon Dex entries when picking starter + Rival Pathing issues
CLOSES#39: Guy who stops player from skipping brock doesn't bring you to brock's gym + doesn't leave once you've beaten brock
CLOSES#40: Bill cutscene is broken
CLOSES#41: Ticket guy failing to be a Ticket guy
CLOSES#42: S.S. anne odd behavior + Missing sailing away animation
CLOSES#43: Dig Attack animation appears to be glitched
CLOSES#44: Pokeball flashing doesn't appear to be accurate
CLOSES#45: Inaccurate Cut Animation
CLOSES#46: Dugtrio i caught in diglett cave has two of the same move
CLOSES#47: Rival ignores player in Lavender Tower
CLOSES#48: Healing pad in lavender tower does not function
CLOSES#49: Incorrect dialogue with parched security guard
CLOSES#50: (Game Breaking!) Rocket grunt guarding poster refuses to move
CLOSES#51: Badges showing up as items I can deposit in PC
CLOSES#52: Visual bug on Celadon Department Store roof (Red Filter)
CLOSES#54: Visual issue on route 15 + Fuchsia City
CLOSES#56: Running animation missing
CLOSES#57: Safari Zone does not display steps while you are inside of it
CLOSES#58: (Game Breaking!) Softlock at cycling road gate
CLOSES#59: Bike visual issues
CLOSES#60: Cycling road not forcing you to get on your bike
CLOSES#61: No keycard doors in Silph Co.
CLOSES#63: Missing teleporter animation
CLOSES#64: Inaccurate spinning
CLOSES#65: Reimplement unused Silph Co. Chief and Professor Oak trainer battles