The SBC and RG34XXSP ports hand LÖVE a source *directory*
(`love <dir>`), so `love.filesystem.isFused()` is false there and
`Boot.run` bailed on its first line. The launcher still offered
"Update vX.Y.Z" and "Restart to update", so an update downloaded,
verified, and then was silently ignored forever.
- Boot.canUpdateInPlace() replaces the fused-only gate: a packaged
build updates whether it is fused (AppImage, Flatpak game.love) or
unpacked, and only a dev / source checkout (engine "0.0.0-dev") is
excluded. Fails closed when the host cannot be established.
Prelaunch.updateAllowed now delegates to it, so the boot gate and the
`--update` gate cannot disagree.
- Check.hostPort() reads the release-target marker, with
POKEPORT_HANDHELD as a legacy fallback (the SBC launcher has always
exported it, so packs predating this change still identify
themselves). The marker was read from the environment but never
exported, so a full-package fallback on a handheld resolved to a
desktop AppImage that cannot run there.
- A handheld now fetches its own package ("Download port update",
reusing the worker's cross-platform download_full) instead of
offering a URL it has no browser to open, and reports "Update package
ready" once it is in the save directory for a manual re-extract.
- Launchers export POKEPORT_PORTMASTER / POKEPORT_RG34XXSP.
An in-place update only ever mounts a payload over the running source,
so relaxing the gate is less invasive than the fused path it joins.
A runtime bump still needs a full package, which the minShell gate
already reports as needs_full.
Tests: tests/engine/update_boot_host_gate.lua (new, 10 checks) pins the
gate; update_check_tests.lua pins both port asset names and hostPort().
scripts/test.sh --quick: all tiers passed; scripts/lint.sh: 0 errors.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Most map sections never showed a Location Preview because the preview
artwork and the region-map GUIDE text were hand-authored placeholders:
only a handful of sections had art and none of it came from the ROM.
Read the real tables out of the dump instead.
Extraction (new):
- src/import/gba/region_map_tables.lua locates and validates
sMapPreviewScreenData. The pinned offset from versions.lua is tried
first and a bounded +/-0x8000 scan is only a guard. Validation is
structural (mapsec in 88..196, type 0/1, three in-range ROM pointers,
and the shipped invariant that the palette block is the first 0x40
bytes of the tile block) so all 28 entries must hold.
- src/import/gba/map_preview_extract.lua bakes 21 deduplicated 240x160
RGBA artworks (28 entries reuse art: Pattern Bush and the six Tanoby
chambers), 109 mapsec names verified against sRegionMapSectionIdToName,
and the 19 sDungeonInfo entries. Palette banks 13/14; bank-0 tilemap
refs only occur in off-screen padding columns.
- src/import/gba/bg_bake.lua holds the shared 4bpp BG baking helpers;
berry_pouch_extract.lua is refactored onto them (-94 lines).
Runtime:
- src/ui/game3/map_preview_screen.lua ports
Task_RunMapPreviewScreenForest: 120-frame first-visit hold, 40-frame
revisit, 48-frame fade-out, artwork and name window composited into one
canvas so they fade together. Only MPS_TYPE_FOREST (8 sections) takes
over the screen; the 20 MPS_TYPE_CAVE sections only change the warp
fade colour, matching pret.
- src/core/game3/map.lua gives a changed section with a forest preview
precedence over the map name popup (overworld.c state 12).
- src/core/game3/warp.lua routes every fade through warpFadeModes,
applying WarpFadeOutScreen/WarpFadeInScreen: a section change into a
cave-preview map forces FADE_TO_BLACK, otherwise MapTransitionIsEnter/
IsExit decide white vs black on MAP_TYPE_UNDERGROUND.
- setworldmapflag feeds the transient visit flag; the HUD ticks and
dismisses the screen; the dataset installs the cache on hydrate.
- region_map.lua's GUIDE panel uses the retail geometry, tint ramp and
delayed text, and ROM names/dungeon text overlay the hand-authored
fallbacks via ensureGenerated().
Two retail quirks are tolerated: MPS_ROCKET_WAREHOUSE and Berry Forest
share flagId 2231 (as do the six Tanoby chambers), and the Tanoby mapsec
is spelled MAPSEC_DILFORD_CHAMBER.
CACHE_VERSION 99 -> 100.
Verified: tests/game3_map_preview_extract_test.lua (new; ROM-gated, all
28 entries, 8 forest / 20 cave, 21 artworks, 109 names, 19 dungeons, the
shared flag, Pattern Bush art reuse), tests/game3_town_map_test.lua,
tests/run_engine.lua 587/587, scripts/test.sh --quick all tiers,
luacheck src 0 warnings / 0 errors.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Collision.installWarps force-marked every map-header warp cell walkable. That
is right for the outdoor MB_WARP_DOOR tiles extract can leave solid, but it also
opened cells that only carry a dead warp event. PalletTown_PlayersHouse_1F has
one at (3,9): the wall tile directly left of the door mat, mid 26, coll 1,
behavior 0x00 MB_NORMAL. pret never lets the player stand there, so the warp
never fires; the forced COLL_DOOR let the player walk out of the house through
the wall and land in PalletTown.
The repair now fires only when the behavior is unreadable (tileset attrs stopped
before that mid, so Collision.behavior returns nil) or is a behavior pret would
actually warp on. Collision.isWarpMetatileBehavior covers 0x60-0x6F plus 0x71,
matching field_control_avatar.c: the arrow warps (TryArrowWarp), the directional
stair warps (IsDirectionalStairWarpMetatileBehavior), and everything
IsWarpMetatileBehavior accepts (cave door, ladder, fall warp, regular warp,
Lavaridge 1F warp, warp door, escalators, union-room warp).
Warp indexing is unchanged: a dead warp on a wall is still registered, only the
grid write is gated.
tests/engine/firered_house_wall_warp_bug2297.lua is ROM-free: it embeds the pret
13x10 map.bin grid and the `building` metatile attributes, translates them with
the real ScriptingCollision.fromCell, binds the real Versions.WARPS table, and
asserts the wall stays solid, the four warp cells still behave, the repair still
applies for unreadable attrs and live warp behaviors, and an exhaustive
0x00-0xFF table pins the predicate. It fails 6 checks against the pre-fix file.
luajit tests/run_engine.lua: 585/585 suites passed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add ROM-backed contextual L/R help with article navigation and safe pause/resume. Record recent field scenes and gameplay events for the skippable Continue recap, with playback isolated from live progress.
Integrate extraction and cache validation, preserve bundled script flags during import, and cover navigation, persistence, event recording, playback isolation, and both ROM revisions with tests.
RFC 0021 gave a mode back the bag on the link cable: opts.items makes an
item the turn, and `{ type = "action", kind = "item", item, index, move }`
rides the wire so the peer -- and a spectator -- can apply the same effect
to their copies before the moves. The peer never got the item.
Wire.sanitize rebuilds every inbound message field by field from
SCHEMAS[type] and drops anything the schema does not name. SCHEMAS.action
names kind, slot and index; `item` and `move` are not among them, so they
were stripped on the way in. Added beside the others, clamped the same
way (a string id, a move slot in 1..MAX_MOVES). SCHEMAS.spectate calls
the same sanitize, so a spectator is fixed by the same line.
Why the tests did not see it. tests/engine/link_items.lua -- and every
in-process link test -- pairs the two sides with Net.loopbackPair, which
hands the table straight over; sanitize only runs on the way through
Session, which is every REAL transport. So the failure needed two
machines to show up, and when it did it was the quietest kind: the peer
receives an item action with no item in it, LinkItems.apply returns
nothing, the turn is still spent, nothing is printed, and the two
simulations part by one heal with nothing on either screen to say so --
until a hash several turns later blames the wrong turn. Found by a
downstream mod's two-client harness on its first real duel: the guest
healed 12 -> 18 and the host watched its copy of that mon go 12 -> 6.
link_items.lua now pins the schema directly (an item action through
sanitize, through a spectate wrapper, and a move action carrying no
item), which is a check a loopback pair cannot make. docs/rfcs/0021
gains the section saying a new field on an existing message type is
invisible until SCHEMAS knows about it.
tests/run_link_tests.lua green; the engine tier matches stock's reds
(572/584, the same twelve).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A link battle runs under cable rules and LinkBattle keeps them: openItems
prints "Items can't be used in a link battle!" and the wire knows move,
struggle, locked, switch and RUN. Right for the Cable Club, wrong for a
mode that is not the Cable Club -- a battle royale played over
LinkState.newFromSession fights with real, damaged parties, and the
potions and X items on the ground are its whole economy; against a bot
the bag works, against a person it says no. The mode cannot fix it:
submit, resolveLockstep and the decoder are closures, and there is no
action that means "nothing" to spend the turn on.
opts.items = true on newHost/newGuest (off by default, set on both
machines like turnLimit): the bag is BattleState.openItems -- the vanilla
BagMenu against this battle, whose picker already offers the clamped
copies -- and the effect is ItemEffects.use as in any fight. What
changes is what spending the turn means: itemUsed puts { kind = "item",
item, index, move } on the wire as the turn's action instead of running
the AI's reply. Both machines and a spectator resolve it before switches
and moves: the user's side is already applied (the bag did it), the other
side applies the same effect to its own copies of that side through
src/link/LinkItems.lua -- ItemEffects.use behind a battle whose player is
the user's battler and a save whose party is the user's copies, nothing
consumed -- and prints "<name> used <ITEM>!" plus the effect's lines.
Every effect reachable in a battle is deterministic, so the per-turn
hash still agrees. BagMenu hands itemUsed the item, target and move it
used; a local battle ignores them. A ball on the cable is refused
(ItemEffects), the way the doll is in any trainer battle.
tests/engine/link_items.lua: cable rules by default; opted in, the host's
POTION heals the guest's copy before the moves, both print the lines,
one turn, hashes agree; a spectator fed the same two messages heals its
copy too. docs/modding.md and docs/rfcs/0021-link-battle-items.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Align CacheContract firered overrides with pure ROM extraction
- Fix door animation extraction tile sizes, palettes, and strides
- Update storage chrome, pokedex, summary, and battle chrome extractors
- Ensure 100% self-contained ROM extraction without external dependencies
Kanto Companion's Edit Mode can't run during battle -- there's no way
to stop a D-pad press from also driving the battle menu, so the only
option today is pausing the game outright. This adds the same input
precedence a mod had before the sandbox changes: vanilla is the whole
existing callback body, and the hook fires before any of it runs.
input.wheel stays a plain observer like input.pointer, since nothing
depends on suppressing it.
GEN2_STATUS_IDS (psn/brn/frz/par/paralysis/slp -> Gen 2's own registry ids)
was declared verbatim in both PartyMenu.lua and SummaryMenu.lua; move it to
Status.GEN2_ID_ALIASES so a future status alias fix only has one copy to
update.
SummaryMenu.TYPE_NAMES was left behind after this branch switched its two
former internal uses to the shared TypeChart.displayName/DISPLAY_NAMES; it
has no remaining callers anywhere in src/ or tests/.
- Introduced the `POKEPORT_IDLE_AFTER` and `POKEPORT_IDLE_FPS` environment variables to manage presentation rates on static screens, allowing game logic and audio to maintain full speed.
- Updated the vsync handling to keep it enabled across all platforms, including KMSDRM handhelds, to leverage PresentSync for improved cadence and pacing.
- Enhanced FrameCap logic to ensure proper handling of performance caps in handheld environments, preventing unnecessary software pacing when hardware capabilities are sufficient.
- Adjusted documentation to reflect these changes and their impact on power efficiency and performance.
Migration and Backward compatibility were two separate sections
saying the same thing about encounter.roll/encounter.species being
untouched, on top of a third mention in "The decision it extends" --
RFC 0014 and 0015 both fold this into one "Migration and
compatibility" section, so this does the same and drops the
redundant third restatement.
Also fixes the chance bullet's "the same reasoning that scoped
fishing out of this RFC" -- that reasoning was never actually stated
anywhere in the doc, just referenced as if it had been. Reworded to
stand on its own: encounter.fishing is a real, separate, existing
hook this RFC doesn't touch, which is reason enough on its own.
src/ui/gen2/PokedexMenu.lua reads its KIND label and both description
pages from data.gen2Pokedex.entries, loaded straight from disk before
mods:load runs -- a separate table from data.pokemon, the `pokemon`
registry's own merge target. mod.content.pokemon:patch(id, { dexEntry =
... }) therefore validated but never reached the screen. Adds
src/core/gen2/PokedexText.lua to project a patched dexEntry onto the
#DEX table after the merge (Game2:load, alongside the other Gen 2
post-merge registries), and a text2 field to the dexEntry schema for
the entry's second description page, which the screen already reads
but the registry had no field for. Also routes the OPTION/SEARCH panel
titles (PokedexMenu.lua) through Strings(), the same literal-wrapping
pattern already used elsewhere in this screen and its siblings.
A Pokémon's level is printed on four Gen 1 surfaces -- both battle
healthboxes, the party rows, and both status pages -- and every one of them
prints it unconditionally. There is no seam, so a mode that wants the number
gone has two options today and both are bad: paint over the engine's own
pixels from render.hud (four rectangles, a background shade to match, and
the palette flashes and healthbox slide to survive), or monkey-patch the
render modules from inside the sandbox, which works and is exactly what
CONTRIBUTING-mods.md tells mods not to do.
The motivating case is a battle royale that scales every party to a shared
rung rising with its fog: the number is the same for everyone, it changes on
a clock, and it reads as a threat it is not -- a Lv37 opponent looks
dangerous to a player who has not worked out that their own team is Lv37
too. A randomizer keeping an encounter unreadable, a challenge run that
forbids level-checking and a blind Nuzlocke want the same switch.
New hook `pokemon.level_visible`, taking the shape the presentation
predicates on the battle screen already use -- battle.status_hud_visible,
battle.bottom_ui_visible, battle.caught_marker_visible: consulted behind
Runtime.wantsHook, default visible, only an explicit false suppresses. It is
not named battle.* because a level is not a battle-only readout, and it
carries the surface that asked (battle.enemy / battle.player / party /
summary) so a mode can hide an opponent's level and keep its own.
src/ui/LevelDisplay.lua holds the one definition of "visible", so the four
call sites are a one-line guard each rather than four copies of the same
five lines that can drift apart.
No layout moves. Each site keeps its own hand-rolled PrintLevel rule
(home/pokemon.asm:335-345), it just asks first. Two details are deliberate:
a status condition still replaces the level on a healthbox exactly as in the
cart, so hiding a level never hides PSN or BRN (the guard is an elseif on
the existing status branch); and on status page 2 the <to> arrow is hidden
with the level it points at, because an arrow with nothing after it is half
a sentence.
Gen 1 only. The Gen 2 screens and the Gen 1 PC box list -- where the level
is part of a row label rather than a drawn field -- keep their own readouts
and do not consult the hook. Both are stated as follow-ups in the RFC and
beside the hook in docs/modding.md, so a mod author reads the limit before
depending on it.
Verification: tests/modkit/cases/pokemon_level_visible.lua covers the
contract through the public mod API; gate_hooks picks the hook up on its own
because it walks the live catalog; gate_meta_coverage is satisfied by the
change that introduces the seam, so it never enters the DEBT ledger.
tests/run_modkit.lua 33/33, tests/run_engine.lua 327/331 -- the same four
audio/hostshell suites fail unchanged on dev without this branch.
Co-Authored-By: Claude <noreply@anthropic.com>
Lets a mod ask what a map's wild-encounter odds look like right now,
composed with any live encounter.table wrapper, without rolling the RNG.
encounter.roll/encounter.species can only transform one draw; this is the
missing read side. Guarded query method (mod.world:effectiveEncounters),
no changes to the real roll path, docs and a public-API modkit case.
Add a ubuntu-24.04 linux-flatpak job that runs build_flatpak.sh from the
shared love payload, wire it into release staging/upload, and mirror a
path-gated Flatpak build in ci.yml when flatpak packaging changes.
Drop the pointless zip wrapper around the x86_64 AppImage, harden HostShell
so bundled curl keeps APPDIR libs while host curl scrubs LD_LIBRARY_PATH /
Steam LD_PRELOAD, and add a Flatpak channel with --device=all, bundled
curl, and AppStream releases metadata. Portable mode now probes writability
with a unique temp file and falls back soft on RO mounts / Flatpak.
Two decisions the OPTION screen and the cart make for the player, made
hookable so a game mode can make them instead: whether a faint offers the
SHIFT free switch, and whether a catch asks for a nickname. Guarded call
sites, file-local vanilla links, docs and a public-API modkit case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1DVpYXGabigHqMwviDoKV