CLOSES#576
The launcher skipped oaks_pc.asm's whole session -- the access text and
the 'Want to get your #DEX rated?' YES/NO -- and played the Pokedex_Rating
jingle the moment the entry was picked, before any text printed. Now the
access text types out, the YES/NO pops, and only once the completion line
and the rating tier have printed does the jingle sound (DisplayDexRating
-> PlayPokedexRatingSfx, auto.wait hands the box to the A/B path), then
the 'Closed link to PROF.OAK's PC.' tail closes the session.
The battle intro slide ran at 4px/frame over 40 frames -- twice the
original speed -- and drew the pics in their normal palette instead of
as black silhouettes.
SlidePlayerAndEnemySilhouettesOnScreen scrolls SCX from $90 to 0 at
2px/frame (~72 frames) under the %11100100 silhouette palette, only
running SET_PAL_BATTLE once the pics land. Match that: introSlide starts
at 80 with a 2px/frame offset (80 frames over the full 160px width), and
picImage bakes both pics through PAL_BLACK while introSlide > 0, exactly
like the evolution movie (#279).
parity_battle_intro_chrome waited a hardcoded 45 frames for the slide to
land; bumped to 85 to cover the slower slide.
Expose a generic seam so a mod can drive a second screen without the
engine owning any dual-screen layout policy:
- render.compose hook in Renderer:endFrame hands a mod the finished
world + UI canvases, their SGB zones, the frame metrics,
Renderer:blitCanvas (lifted from the internal blit closure) and the
SecondScreen bridge. Return true to take over the window; no wrap (or
calling next) runs the normal single-window composite byte-for-byte.
- SecondScreen.lua + the Android Presentation bridge (love_android_
secondary_* in common/android.cpp, GameActivity secondary display)
as the optional physical-second-display transport.
No battle-render changes: a mod lays out the two screens (including any
battle split) itself. Ships with a unit test, no-mod parity via
gate_hooks, and docs/modding.md (D14).
Layout interactive UI against love.window.getSafeArea so notch, Dynamic Island, and home-indicator insets no longer clip controls, while keeping the game framebuffer edge-to-edge.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pressing Import ROM on iOS takes the whole app down:
src/import/RomImporter.lua: attempt to call field 'pickFile' (a nil value)
love.system.pickFile is a NATIVE BRIDGE, not part of LOVE. It exists only on
builds that compiled one -- Android, and iOS builds patched by
mobile/ios/patch_love_src.py -- so on a build without it the field is simply
nil. RomImporter:546 routes iOS down the same path as Android
(`mobileOS == "Android" or mobileOS == "iOS"`), and all three mobile pick
sites called the field unguarded.
That is why the reports say "any version": nothing about it is version
specific. Red, Blue and Yellow all reach the same call.
Every one of those call sites already handles a device with no document
picker -- Choose falls back to "No picker available, copy your ROM into:"
plus the save directory, and the mod / save rows have their own notices --
and love.system.createFile at its single call site was already guarded this
way. These three were not, so the fallback that was written for exactly this
case could never be reached.
Route them through one small helper that answers false when the bridge is
absent. A build without a picker now degrades to the copy-into-the-save-folder
flow, which on iOS is a working path: the Files app exposes the app's
Documents folder and GRBootstrap sweeps what lands there into the save dir.
tests/rom_importer_no_picker_test.lua covers Import ROM, Import mod and
Import save with the bridge missing, and asserts the picker is still used
when it is present. Reverting the fix reproduces the reported error exactly.
Reported in #482 (confirmed by three people) and #512.
love.system.syncHealthSteps() now exists on Android, matching the iOS
Health bridge merged in #452 and using the same JNI route as the SAF
picker (wrap_System.cpp -> System.cpp -> common/android.cpp ->
GameActivity over JNI):
- GameActivity.syncHealthSteps: one-shot read of the hardware
TYPE_STEP_COUNTER sensor (cumulative since boot, counted by the OS
whether or not any app runs). The reading is anchored in
SharedPreferences so a walk is never credited twice; a reading below
the anchor means the phone rebooted, which re-anchors without
crediting. Deltas (50k clamp) merge into steps_pending.json in the
save identity dir - the same contract as the iOS GRHealthBridge, so
the Pokewalker mod works unchanged on both platforms.
- ACTIVITY_RECOGNITION declared in the app manifest (Android 10+
runtime prompt on first sync; granted -> the sensor read runs
immediately via onRequestPermissionsResult). The build script's
permission trim leaves it alone.
- Nothing in the base game calls the new seam; without a consumer mod
the only cost is one dormant manifest permission.
- build_android.sh: shadow-build from a space-free temp dir when the
checkout path contains spaces - ndk-build is GNU make underneath and
cannot cope with paths like "xCode Projects".
- mobile/ANDROID.md: step-bridge dev notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce a tuned .luacheckrc and scripts/lint.sh so the engine has a
standing static-analysis baseline -- the tool that would have caught both
bugs in the previous commit before they shipped.
The config is high-signal by design: it keeps the categories that catch
real defects (undefined globals/locals, unused values, unreachable code)
and mutes the cosmetic ones the codebase deliberately lives with (a self/dt
an interface requires but a method ignores, documented empty fall-through
branches, long lines). It marks `love` mutable (games assign callbacks onto
it) and teaches it LuaJIT's table.unpack.
.luacheckrc is tracked via a .gitignore exception, matching how .github and
.gitignore opt out of the blanket dotfile ignore.
`luacheck src` now reports 7 benign warnings and 0 errors, down from 185.
Also drop one dead `require` (ItemEffects loaded src.pokemon.Pokemon and
never used it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6bFAiQyZ5jDmewsbB4LG9