Second wave of fixes from the Gen 3 codebase review (210 findings), the
regressions the full-suite sweep surfaced, and a T6 tier so the top-level
tests/game3_*.lua suites run inside ./scripts/test.sh instead of outside it.
Battle: Knock Off and Thief/Trick item persistence follow the ROM, not the
review - the party slot is never written through during battle, and the
knocked-off mask suppresses the item on later send-outs
(pokefirered/src/battle_script_commands.c:2731 MOVE_EFFECT_KNOCK_OFF with the
STICKY_HOLD guard at :2732, opponent-steal guard at :2610-2622, mask use at
:4489). Rapid Spin keeps its one-per-use chain order (:8435-8474); Growl
keeps pret's target (src/data/battle_moves.h, include/battle.h:63).
Scripting: operand layouts match pret/asm/macros/event.inc - givemon 15 bytes
(:989-997), comparestat {B,W} (:1573-1576), setptr/loadbytefromptr/setptrbyte
take a word pointer (:118-137). 18 of the 20 E10 ops are wired per
src/scrcmd.c (per-op citations in docs/game3/e10-opcode-spec.md), money and
random and the warp family read their operands as VarGets
(src/scrcmd.c:1798-1830, :455-461, :719-731), and the day-care party-full
guard is in place (src/daycare.c:525,:1081).
Field and UI: fishing counts rounds as the ROM does
(src/field_player_avatar.c:1740-1765), an unresolved map section no longer
reports Pallet Town (src/region_map.c:3782), and a font provider sits behind
a FireRed capability/profile seam.
Save data: profile-driven Options.block, a save version round-trip, and slot
id validation that blocks path traversal (slotDiskPath("firered", "../evil")
returns nil).
Importer: the parallel import path now writes both completion markers,
object kind/clone bytes decode as pret defines them
(include/global.fieldmap.h:110-130), and the HM table drops Whirlpool
(include/constants/items.h:411-418).
Tests: six new scenario suites (battle_ai, capture, event, menu, move,
overworld) and eight engine suites (profile, capabilities, cache paths, font
provider, options block, save version round-trip, version dispatch,
versions_game). T6 runs every top-level game3 suite at GAME3_JOBS default 8
with per-suite logs, failure cause classification and a KNOWN_GAME3_FAILURES
ceiling of 23 as a shrinking guard.
Docs: the working notes under docs/game3 (triage ledger, pret citation audit,
e10 opcode spec, RSE seams, sweep v3/v113, test baseline, artifact
conversions, merge trial).
Full gate at this snapshot: exit 0, all tiers passed - engine 624/624,
T6 279/279 (0 known, 0 fresh failures), gen2 146/146, modkit 37/37, luacheck
clean, privacy gate 3/3 over a 9,711-file publication set.
bootGame's require("src.core.Game") was resolving to Gen2Compat's Game
facade after a Gold session, so the next Gen 1 boot got an empty proxy
whose rawget(load) is nil (iOS: "src.core.Game missing load after
reload"; Android: Game:load called on a nil value).
Two independent causes, both needed:
* callerIsMod decided "not under src/" meant "a mod", and main.lua is
not under src/, so the engine's own require was gated as a mod's.
Root chunks main.lua and conf.lua are now matched exactly, which a
mod's own mods/<id>/main.lua cannot collide with.
* devShim.generation was only ever set, never cleared, so the
generation Gold declared outlived Gold's session. Loader.endSession
drops it from SessionLifecycle.endMountedSession, and the facade
gate now requires generation == 2 rather than ~= 1.
Only platforms that return to the launcher in-process (Android, and iOS
since #1808) keep the shim alive across sessions; everywhere else the
process restart cleared it.
A custom cart pairs an identity (title, shell colour, label art) with a base
game, a list of mods pinned to exact builds with their option values frozen,
a load order, and a seal. It ships no code of its own: every mod it names is
a separately published mod, which is what keeps a cart auditable before it
runs and reproducible after an author's repo disappears.
Format and storage:
- src/carts/CartManifest.lua parses and validates cart.json, canonicalises it
for hashing and reads/writes the .g1rcart bundle. The bundle is a data-only
serialised table read through SaveSerializer, so an imported cart can never
execute code. Canonical strings are length-prefixed because option keys and
values are author-controlled and could otherwise forge a record boundary and
collide two different carts onto one hash.
- Pins name a public source: a GitHub release with its sha256, a GameBanana
file id with its md5, or "local" for a capture that only exists on this
install. A local pin is unpublishable by construction, which is what makes
"build it here, publish later" possible without inventing a hash.
- Label art rides alongside the manifest rather than inside its identity, so
re-arting a cart does not tell every player their run is out of date.
src/core/Base64.lua decodes it; strict, with no whitespace tolerance.
Saves:
- Cart playthroughs live in the cart's own slot namespace (saves/cart_<id>/),
so a cart's file never sits beside a vanilla one and uninstalling a cart
never orphans a save. Every save records the cart build it was made under.
The seal:
- A sealed cart loads its pinned list, in its order, with its options, and
nothing else. A pinned mod with no frozen options gets an empty bucket so
unfrozen keys fall to schema defaults, identical for everyone; otherwise two
players on one cart quietly run different games.
- A sealed cart refuses to load when a pin is missing or installed at another
version. Playing a subset of the cart is the exact dishonesty the seal
exists to prevent, so the refusal loads nothing at all.
- Breaking the seal is permanent, marked per save slot, and downgrades that
playthrough to open behaviour. It cannot be cleared through any public API.
Launcher:
- A game's page carries a Custom Carts control and a picker; choosing a cart
turns the page into that cart's page, with its own cartridge, title and save
slots. The rail of five games never grows and a cart id never reaches
imp.tab or imp.panelVersion.
- Loader.planCart runs before boot so a refusal is visible on the page instead
of being discovered as an error after launch.
- Save as cart captures the enabled mods for a game and names, before the
player confirms, every mod that could only be pinned to this install and
whether the result can be shared at all.
Authoring:
- tools/cartkit.py scaffolds, validates, pins and packs a cart, and installs a
release workflow. Its writer is byte-identical to the engine's serialiser.
The sandbox blocks love.system and love.filesystem, which orphans the
native step bridge (#452, #489): its one consumer can no longer call
syncHealthSteps or read steps_pending.json (#1186).
Adds a "steps" manifest permission (shown to the player like the
others) gating a mod.steps facade: available() probes the bridge
quietly, sync() forwards the async refresh, poll() hands the mod its
copy of a delivery. The engine owns the pending file -- mods never name
a path and receive only { steps, from, to }. Without the permission the
acting calls name it, following the network gate. No new events, hooks
or registries; nothing removed.
RFC 0009. Tests: tests/modkit/cases/steps_bridge.lua (no-mod cold
bridge, permissioned sync/poll, per-mod copies, contract-field
filtering, malformed-delivery drop, unpermissioned refusal, bridgeless
build).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A platform-specific launcher wrapper (a native shell embedding this engine,
owning its own UI around the game window) needs to pause the simulation
while its own UI is on top, live-reload options it wrote outside any Lua
UI, and veto main.lua's "closing the window returns to the Lua launcher"
behavior when it owns that job itself. Implementing this by hand-patching
main.lua's love.update/love.quit directly ties every such integration to
editing the one file every other engine change also touches, guaranteeing
merge conflicts. No existing hook covers "should the per-frame simulation
step run" or "should closing the window return to the Lua launcher."
Adds two generic, additive hooks (src/core/PlatformHooks.lua): core.update
and core.quit_to_launcher, replacing what would otherwise be inline
main.lua special-casing. Also adds Manifest.force_enable_env, letting a
mod that cannot function disabled on the one build where its env var is
set (a platform-bridge mod bundled only with that build) re-enable itself
regardless of a saved disable.
RFC 0006 status: Proposed.