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.
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>
Source:getChannelCount could skip the #626 widen on love-nx, and keeping
8-bit depth into a stereo buffer still sounded wrong on audren. Decode the
file via newSoundData, always emit 16-bit stereo like ChipSynth, and write
fresh pika-cry WAVs as stereo at extract time so re-imports skip the hop.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Yellow boot suite only recorded newSource, so a missing sound.newSoundData
wrap (the silent hole that motivated the full-surface overlay) could still go
green. Force mono on the Source stub, record newSoundData, and assert the cry
re-read lands on yellow/. Also pin the three NX suites in scripts/test.sh T0
and fix the seed_chunk global leak in the fallback suite.
Co-authored-by: Cursor <cursoragent@cursor.com>
The overlay only wrapped the five loaders the boot path needed, leaving a
silent-failure hole: any future state (or current code like Sound.lua's
widenMono, which re-reads the pika-cry WAV via love.sound.newSoundData
with the caller's bare path) could load a generated asset through an
unwrapped API and silently degrade on hardware.
NxAssetOverlay now wraps every read-side love function that accepts a
filesystem path (filesystem.read/load/lines/newFileData/getInfo,
graphics.newImage/newFont, image.newImageData, audio.newSource,
sound.newSoundData, font.newFontData), so new states and mods fall inside
the Blue/Yellow fallback with zero per-call-site work. Write-side
functions stay stock, proven by identity assertions in the fallback
suite. The static guard's forbidden-literal list covers the same APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Three layers, all running without a ROM:
- nx_yellow_boot_test.lua: drives the real Yellow and Blue boot states
(TitleState, YellowIntro + IntroMovie pre-roll for Yellow, IntroMovie
direct for Blue, Sound.playPikaCry) against a broken-mount filesystem
where generated art exists only under yellow|blue/, and asserts no bare
assets/generated path ever reaches the raw love loaders. This is the
runtime complement to the static literal guard: data-driven manifest
paths and formatted paths (cry_%02d.wav) are exactly what a source scan
cannot see. love_stub gains Image:setFilter/getFilter so IntroMovie
constructs headless.
- CI path gate: switch-changes now also triggers on the NX runtime
(NxAssetOverlay, Platform, GameVersion, CacheFs) and the NX engine
suites, so src-side NX regressions rebuild the fused NRO instead of
slipping through with green headless-only checks.
- switch-selftest runs the three NX engine suites headlessly on the
fork-safe ubuntu runner, giving PR feedback before the self-hosted Mac
build. The content gate and switch-build.md docs were updated in sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
The scattered per-call-site prefix rewrites were a parallel track that any
future newImage("assets/generated/...") would silently bypass. Replace
them with NxAssetOverlay: installed once from love.load on NX only, it
wraps newImage / newImageData / newSource / filesystem.read / getInfo so a
missing assets/generated path falls back to the active version's
blue|yellow copy. Call sites return to plain love loader calls, and
Assets.resolve goes back to being the platform-free mod-override point.
Two deliberate exceptions remain: the chip-audio worker (separate Lua
state) keeps receiving the prefix explicitly via audio.programPrefix, and
data/generated module loads keep using CacheFs.readActive.
A new guard test (tests/engine/nx_generated_guard_test.lua) fails CI on
any direct love loader call with a literal assets/generated path, so the
class of bug cannot regress by accident. scripts/test.sh --quick is
green across all tiers.
Co-authored-by: Cursor <cursoragent@cursor.com>
Yellow music was still silent because the background worker thread loads
ChipSynth.lua in a fresh Lua state with no GameVersion/Platform context.
The main thread's prefix never reached it.
ChipAudio.slimAudio now resolves the versioned cache prefix on the main
thread and includes it in the audio payload as `programPrefix`.
ChipSynth.loadBanks prefers `audio.programPrefix` when present, falling
back to its own NX detection for the sync path. Blue and Yellow are
handled the same way.
Tests cover the worker prefix hand-off and Blue's programs.bin path.
Co-authored-by: Cursor <cursoragent@cursor.com>
The previous NX gate only rewrote image paths that go through Assets.resolve.
Pokemon Yellow still had no sound and a blank title screen because:
- ChipSynth reads programs.bin directly via love.filesystem.read, bypassing
Assets. On NX the unprefixed path is missing when the mount overlay fails,
so the engine never built and every song/SFX was silent.
- Sound.playPikaCry loads pika_cries WAVs with love.audio.newSource, also
bypassing Assets.resolve.
- TitleState, YellowIntro, and IntroMovie call love.graphics.newImage
directly on unprefixed assets/generated paths, so the Pikachu title and
intro atlases failed to load.
Fix: apply the same NX-only prefix rewrite in those four places.
Desktop/Android keep the existing mountVersion overlay behavior.
Also add ChipSynth._loadBanksForTest and tests covering the new paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture resolve paths and newImage open results for Yellow/Blue art
triage without enabling switch-debug.txt.
Co-authored-by: Cursor <cursoragent@cursor.com>
Desktop and Android keep mountVersion as the overlay; only love-nx
resolves assets/generated to yellow|blue/ save-dir paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
NX fused mount often cannot expose assets/generated; open the real
yellow|blue/assets/generated file with newImage instead of FileData.
Co-authored-by: Cursor <cursoragent@cursor.com>
Probe generated canaries after mountVersion and always readActive for
prefixed caches so sprites are not blanked by empty PhysFS stubs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Disable mouse-yield on NX where stick/touch moves the system pointer between sparse axis events, clamp pad dt, pixel-snap the overlay, and soften FlexLove GC so the launcher cursor stays steady.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only resize when width/height change; love-nx flag mismatches were
recreating the EGL surface every frame.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror Data:load's versioned CacheFs read in Assets so Yellow-only NX
Play survives intro without needing a Red root cache mask.
Co-authored-by: Cursor <cursoragent@cursor.com>
Unlock love-nx SDL dock/undock resizing and sync via NxDisplay so
booting docked is not stuck on the conf 720p hint.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip per-frame mouse warps and FlexLove perf sampling on Switch, feed pad coords through a getPosition bridge, and park that shim before the save editor so desktop paths stay unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
A shared imports/ inbox with Red+Yellow was starting Red from the Yellow tab; match by GameVersion.forSha1 for the selected game and document the tab-scoped rescan.
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>
scanInbox never consulted ready, and mobileFileBridge already mirrors
android on Android/iOS, so the dual guard was a no-op.
Co-authored-by: Cursor <cursoragent@cursor.com>
platform_nx_* and rom_importer_nx_* run with the love stub and must execute
in CI's ROM-free lane via tests/run_engine.lua, not only T3.
Co-authored-by: Cursor <cursoragent@cursor.com>
Split Import/Export paths into imports/saves/{red,blue,yellow}/ and
exports/{red,blue,yellow}/ so MTP destinations match each launcher tab.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
apply() returned false on its first line for mobile, so the option did
nothing there. A phone has no window to resize, so the lock caps the
render scale instead: the largest whole multiple of 160x144 the display
holds, centred, black around it.
Two parts beyond that. The scale is read off the display rather than from
the desktop's 1X-4X ladder, which named a different fraction of every
device and left the useful levels off the list; mobile shows ON or OFF.
And the world pass, which expands to cover the whole display so letterbox
becomes more map, is now sized against the locked viewport, so the lock
reaches the overworld instead of showing more of it.
Pixel perfect throughout, whole multiples only. Desktop and OFF are
unchanged. Renames the row to FAITHFUL RATIO on both platforms; the saved
key stays faithfulRes so existing settings carry over.
The row assertions called Data:load(), which needs data/generated/. The
T1/T2 tier runs without a ROM in CI, so the suite died on the import
rather than failing an assertion. Use T.fixtures.load() like the other
engine suites do.
Verified by moving data/generated aside and re-running: 20/20 with no
imported data present.
Edge docking and zoom-linked UI scaling shipped as unconditional
behaviour. Both are departures from how the port composed the screen, so
they become a setting instead: UI LAYOUT = CENTERED (the default) or
DYNAMIC.
CENTERED is a fixed letterbox. Elements stay where they were drawn in the
160x144 canvas, and the UI does not follow the survey zoom, so screen
furniture neither moves nor resizes under the player. That is what the
pre-anchoring builds did. DYNAMIC is the current behaviour, unchanged.
Both halves matter together: gating only the anchoring would stop the
dialogue box moving but leave it resizing with the zoom, which is the same
complaint in a different form.
Gated at Renderer:setUIAnchor and Renderer:uiScale rather than at each
caller, so one switch covers the dialogue box, its YES/NO, the START menu
and anything anchored later, and no caller knows the option exists.
Game.dynamicUI answers true only for an explicit "dynamic", so a save
written before this keeps the layout it already had.
Independent of it, deliberately: BATTLE SIZE still works under either mode
(uiFill overrides the scale later, in endFrame), and a battle still holds
its own prompts inside its screen under DYNAMIC.
Also includes the Oak intro fix (previously #674): the speech fills white
over the UI canvas while its dialogue box docks to the window edge, so
under DYNAMIC black showed between the two. letterboxWhite closes it, and
the shrink beat's replica box rides the same anchor as the real box it
stands in for.
The faint slide was shortened from 30 to Timing.FAINT_SLIDE (14) frames
in the timing-parity pass, but fxFaintOffset still computed the offset
with a stale (30 - frames) * 2. With frames starting at 14 the sprite
teleported 32px down on the first frame and only slid the remaining
28px, cutting the animation short.
SlideDownFaintedMonPic drops the pic one 8px row per 2-frame step, so
the offset advances Timing.FAINT_SLIDE_STEP (4px) per frame at 1x and
covers the full 56px PIC_HEIGHT over the 14-frame budget.
The starter balls' scripts showed the received-mon text but never played
the sound_get_key_item fanfare that the text carries in the original
(scripts/OaksLab.asm OaksLabReceivedMonText / OaksLabRivalReceivedMonText).
Add play_sound Get_Key_Item before each received text, mirroring the
Yellow starter port.
BATTLE SIZE "fixed" draws the battle as a discrete letterbox rather than
filling the window, and BATTLE BG "world" composes it over the live map.
Everything the battle then opens broke out of that composition, because
each piece of the frame's geometry was read off a fact about THIS FRAME
instead of about the battle:
* Renderer:uiScale follows the survey zoom only while a world is behind
the UI, gated on worldActive -- this frame's world pass. PartyMenu and
ListMenu are opaque, so pushing one makes StateStack:visibleBase skip
the map, no world pass runs, and the menu loses the step-down and blits
a whole integer scale larger than the battle it just covered. Held
with uiWorldHold, the same whole-stack rule uiFill and the battle dim
already use. ("fill" hid this: it overrides the scale outright.)
* Game:draw started the frame at visibleBase, so that same opaque menu
cut the overworld -- and the world pass with it -- out of the frame
entirely, collapsing a "world" backdrop to endFrame's flat black clear.
A world-bg battle now keeps the frame starting from underneath itself
(drawBaseInStack). Only the START of the draw moves; the clear stays
keyed to the real visibleBase, so the menu still gets its opaque canvas
and draws exactly as before.
* worldZones was keyed to that same clear base, so it came out nil for a
frame whose world pass HAD run -- dropping endFrame's world blit onto
the UI zone list instead, smearing the party menu's own HP-bar palettes
across a world-canvas-sized image. Keyed to whether the map drew.
* endFrame's letterbox clear read letterboxWhite off visibleBase alone,
so an opaque menu over a BG "white" battle flipped its surround to
black the same way. Same whole-stack hold.
* ChoiceBox bottom-anchored unconditionally, docking it to the WINDOW
edge. That is only right when it rides the dialogue box beneath it,
which is anchored there too; TextBox now passes the anchor and nothing
else does, so the battle's switch offer and the shop/PC confirms stay
over the screen that pushed them.
* TextBox anchors likewise: a battle is a self-contained SCREEN, not the
window, and pokered prints its text box in the same 160x144 tilemap as
the HUD. The caught-mon nickname prompt was landing a whole letterbox
below the blanked battle field it is printed on. BattleState.holdsUI-
Anchors holds setUIAnchor off while a battle is in the stack; the
overworld's own dialogue box still docks to the screen edge.