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.
33 KiB
E10 opcode spec — engine-unwired opcodes vs pret
Scope: the engine opcodes that have no handler branch in src/core/game3/scripting/ops_a.lua
and therefore fall through to the generic skip path (ops_a.lua:1956-1975).
Pret ground truth: pret/pokefirered @ c75f3523 ("Merge pull request #770 from Deokishisu/patch-8"),
canonical team clone /Users/shanemcgovern/dev/pokefirered. Every behavioural claim below cites
pokefirered/… file:line. Nothing here is a fix instruction — the Finisher/applier verifies each against the
cited pret body and engine seams before wiring (C1 lesson: report suggestions are evidence, not instructions).
Engine snapshot: /Users/shanemcgovern/dev/gen1recomp working tree at time of writing.
1. Recount — 26, not 63
The v3 review said "63 defined opcodes have no handler" (E10, marked verified=true at review time). That no
longer reproduces against this tree:
| Measure | Count |
|---|---|
Opcodes declared in opcodes.lua Opcodes.TABLE |
213 |
Pret ScrCmd_* functions in pokefirered/src/scrcmd.c |
213 (full parity) |
Engine ops with an op == "…" branch in ops_a.lua |
189 |
| Engine ops with no branch (this spec) | 26 |
The review's named examples are all wired now, most with pret citations in the code:
callstd_if/gotostd_if (ops_a.lua:371), applymovementat/waitmovementat/addobjectat/removeobjectat
(:1058), bufferitemnameplural (:804), setmonmove (:823), addpcitem/checkpcitem (:945),
setdooropen/setdoorclosed (:1085), vmessage (:913), vgoto/vcall/vgoto_if/vcall_if (:885).
Only createvobject/turnvobject from that list remain unwired.
Also note: goto_if_set has a return false stub branch in ops_a.lua but is not an opcode in
opcodes.lua — it is a composite pret macro (event.inc), so that branch is dead code, not a coverage gap.
2. Classification summary
| Class | Count | Ops |
|---|---|---|
| A — pret FRLG no-op (engine skip is behaviour-equivalent; add explicit no-op branches) | 16 | initclock, dotimebasedevents, adddecoration, removedecoration, checkdecor, checkdecorspace, drawbox, drawboxtext, showcontestpainting, setberrytree, startcontest, showcontestresults, contestlinktransfer, getpokenewsactive, addelevmenuitem, showelevmenu |
| B — pret yields (ScriptContext_Stop) | 1 | choosecontestmon |
| C — real work, wiring needed | 9 | setmysteryeventstatus, gotonative, gettime, setobjectsubpriority, resetobjectsubpriority, createvobject, turnvobject, loadhelp, unloadhelp |
Pret-side grounding: 26/26 cited (0 [pret-unverified] here). Engine-side readiness differs per op and is
called out in the table: 20 wired-ready (16 explicit nops + gettime + the two subpriority ops +
choosecontestmon), 6 need an engine backend or seam decision (setmysteryeventstatus, gotonative,
createvobject, turnvobject, loadhelp, unloadhelp).
Operand-layout audit result: 24/26 declared layouts match pret. Two are wrong and already desync the
decoder (not just unwired): 0xa8 setobjectsubpriority and 0xa9 resetobjectsubpriority — see §3.1.
3. Per-op spec
Layout column: pret byte sequence from asm/macros/event.inc (operands after the opcode byte; map expands
to 2 bytes per asm/macros/map.inc:4-7). Engine column: declared op(name, size, args) from opcodes.lua.
3.1 Real work — wiring needed
| Op | Pret fn (file:line) | Pret behaviour | Layout pret vs engine | Engine state | Wiring requirement |
|---|---|---|---|---|---|
0x0e setmysteryeventstatus |
src/scrcmd.c:269-273 |
SetMysteryEventScriptStatus(ScriptReadByte(ctx)) — stores the mystery-event script status byte |
B (2 total) vs 2 {B} ✅ |
unwired | Needs a session save field + writer mirroring SetMysteryEventScriptStatus; check pret readers before choosing the field (grep GetMysteryEventScriptStatus). No engine field found → [engine-unverified: seam choice] |
0x24 gotonative |
src/scrcmd.c:92-97 |
SetupNativeScript(ctx, func), returns TRUE: replaces the running script with the C function, resuming the script when it returns TRUE |
W (5 total) vs 5 {W} ✅ |
unwired; overlaps Q10 (Natives.callnative has no native: registrations) |
Requires a native-function registry + dispatch (Q10). Without it the op can only skip/halt. [engine-unverified: dispatch design] |
0x2e gettime |
src/scrcmd.c:673-681 |
FRLG: writes 0 to VAR_0x8000/0x8001/0x8002 (RTC lines commented out) | — (1 total) vs 1 {} ✅ |
unwired | Set the three special vars to 0 (pret does not leave them stale). Trivial; wired-ready. |
0xa8 setobjectsubpriority |
src/scrcmd.c:1122-1130 |
localId = VarGet(u16), mapGroup = ScriptReadByte, mapNum = ScriptReadByte, priority = ScriptReadByte; SetObjectSubpriority(localId, mapNum, mapGroup, priority + 83) |
H, map(2), B = 5 operand bytes / 6 total vs engine 5 {B,B,B,B} = 4 operands ❌ under-reads 1 byte |
unwired; engine declares wrong layout | Fix layout to {H,B,B,B} size 6, then map to the object's OAM subpriority (oam.lua:292 Oam.setSubpriority-style path; objects.lua may need a setter). |
0xa9 resetobjectsubpriority |
src/scrcmd.c:1133-1140 |
localId = VarGet(u16), group/num bytes; ResetObjectSubpriority(localId, mapNum, mapGroup) |
H, map(2) = 4 operands / 5 total vs engine 4 {B,B,B} ❌ under-reads 1 byte |
unwired; engine declares wrong layout | Fix layout to {H,B,B} size 5, then reset the object sprite's subpriority. |
0xaa createvobject |
src/scrcmd.c:1171-1181 |
graphicsId(B), virtualObjId(B), x=VarGet(u16), y=VarGet(u16), elevation(B), direction(B); CreateVirtualObject(...) — sprite-only NPCs beyond the object limit |
B,B,H,H,B,B (8 total) vs 8 {B,B,H,H,B,B} ✅ |
unwired | No virtual-object backend found in src/core/game3/ → needs a design addition (sprite registry keyed by virtualObjId, reused by turnvobject, cleaned on map unload). [engine-unverified: backend missing] |
0xab turnvobject |
src/scrcmd.c:1184-1190 |
virtualObjId(B), direction(B); TurnVirtualObject(id, direction) |
B,B (3 total) vs 3 {B,B} ✅ |
unwired | Depends on the createvobject backend. |
0xc8 loadhelp |
src/scrcmd.c:1274-1280 |
Reads a word (msg), falls back to ctx->data[0] when NULL, DrawHelpMessageWindowWithText(msg) |
W (5 total) vs 5 {W} ✅ |
unwired; engine has the L/R context Help (help_system.lua:133 Help.show(game,context)), not a text help window |
Seam decision: does the engine's Help display script text, or should this be a no-op like signmsg? Pret behaviour is the cited text window; [engine-unverified: seam choice] |
0xc9 unloadhelp |
src/scrcmd.c:1285-1289 |
DestroyHelpMessageWindow_() |
— (1 total) vs 1 {} ✅ |
unwired | Pair with loadhelp; if loadhelp maps to Help.close(), wire this to the same. |
3.2 Pret FRLG no-ops (engine skip is equivalent; make it explicit)
All of these are return FALSE in pret with the real implementation commented out (FRLG removed the feature).
Wiring = one explicit return false branch (no silent skip), so mods/scripts relying on the pret-contract see a
documented no-op. None consume their operands in pret beyond the script reader skipping them, which the
decoder already does via their declared sizes (all ✅ against event.inc).
| Op | Pret fn (file:line) | Pret body | Layout check |
|---|---|---|---|
0x2c initclock |
scrcmd.c:658-664 |
RTC lines commented; return FALSE |
H,H (5) vs 5 {H,H} ✅ |
0x2d dotimebasedevents |
scrcmd.c:667-671 |
commented; return FALSE |
1 ✅ |
0x4b adddecoration |
scrcmd.c:526-532 |
reads var, DecorationAdd commented | H (3) vs 3 {H} ✅ |
0x4c removedecoration |
scrcmd.c:534-540 |
commented | 3 {H} ✅ |
0x4d checkdecor |
scrcmd.c:550-556 |
commented | 3 {H} ✅ |
0x4e checkdecorspace |
scrcmd.c:542-548 |
commented | 3 {H} ✅ |
0x72 drawbox |
scrcmd.c:1464-1472 |
window draw commented | B×4 (5) vs 5 {B,B,B,B} ✅ |
0x74 drawboxtext |
scrcmd.c:1505-1516 |
commented | 5 {B,B,B,B} ✅ |
0x77 showcontestpainting |
scrcmd.c:1543-1552 |
reads 1 byte, commented | B (2) vs 2 {B} ✅ |
0x8a setberrytree |
scrcmd.c:1989-1999 |
commented | B,B,B (4) vs 4 {B,B,B} ✅ |
0x8c startcontest |
scrcmd.c:2018-2024 |
commented | 1 ✅ |
0x8d showcontestresults |
scrcmd.c:2026-2032 |
commented | 1 ✅ |
0x8e contestlinktransfer |
scrcmd.c:2034-2040 |
commented | 1 ✅ |
0x96 getpokenewsactive |
scrcmd.c:2002-2008 |
commented | H (3) vs 3 {H} ✅ |
0xb1 addelevmenuitem |
scrcmd.c:2178-2187 |
commented | B,H,H,H (8) vs 8 {B,H,H,H} ✅ |
0xb2 showelevmenu |
scrcmd.c:2189-2194 |
commented | 1 ✅ |
3.3 Pret yields
| Op | Pret fn (file:line) | Pret behaviour | Layout | Engine state | Wiring requirement |
|---|---|---|---|---|---|
0x8b choosecontestmon |
scrcmd.c:2010-2016 |
ChooseContestMon() commented, but ScriptContext_Stop(); return TRUE; are active — the script halts awaiting context resume |
1 ✅ |
unwired → engine continues instead of yielding | Wire as a yield (vm pause equivalent / return true in the dispatch contract), matching pret's stop. Contest UI itself is absent in FRLG. |
4. Counts for the lead
- Unwired opcodes (current tree): 26 (review's 63 is stale; all named examples except
createvobject/turnvobjectnow have cited handlers). - Pret-grounded: 26/26 — 0
[pret-unverified]on behaviour; all bodies insrc/scrcmd.c+ layouts inasm/macros/event.inc. - Wired-ready (trivial, no backend needed): 20 — 16 explicit no-ops +
gettime+choosecontestmon+setobjectsubpriority/resetobjectsubpriority(layout fix + existing OAM subpriority seam). - Needs an engine backend or a seam decision: 6 —
setmysteryeventstatus,gotonative,createvobject,turnvobject,loadhelp,unloadhelp(each marked[engine-unverified]above). - Pre-existing decoder defects found while auditing: 2 (
0xa8/0xa9under-read 1 byte — scripts containing them already desync all later rows, same class as E3).
5. Seam decisions (Architect, 2026-09-22)
Design + evidence only; no src/ edits. Engine anchors are this tree, pret anchors
are the canonical clone ~/dev/pokefirered @ c75f35230 (plus pret/pokeemerald and
pret/pokeruby over raw.githubusercontent for the cross-game checks, all fetched
2026-09-22).
5.0 Cross-game table (why "FRLG-only" is the wrong axis here)
| Op | pokefirered macro + handler |
pokeemerald macro + handler |
Usage in FRLG data/ + src/ (0 = never emitted) |
Verdict |
|---|---|---|---|---|
0x0e setmysteryeventstatus |
asm/macros/event.inc:98 + src/scrcmd.c:269 |
macro present, data/script_cmd_table.inc:32 → ScrCmd_setmysteryeventstatus |
0 | shared FRLG+E, absent from R/S |
0x24 gotonative |
event.inc:268 + src/scrcmd.c:92 |
event.inc present, data/script_cmd_table.inc:54 → handler |
0 | shared FRLG+E+R/S (R/S table data/script_cmd_table.inc:39) |
0xaa createvobject |
event.inc:1346 + src/scrcmd.c:1171 |
data/script_cmd_table.inc:188 → handler |
0 | shared FRLG+E, absent from R/S |
0xab turnvobject |
event.inc:1357 + src/scrcmd.c:1184 |
data/script_cmd_table.inc:189 → handler |
0 | shared FRLG+E, absent from R/S |
0xc8 loadhelp |
event.inc:1551 + src/scrcmd.c:1274 |
data/script_cmd_table.inc:218 → ScrCmd_nop1 |
0 | FRLG-only in effect (Emerald no-ops it) |
0xc9 unloadhelp |
event.inc:1557 + src/scrcmd.c:1285 |
data/script_cmd_table.inc:219 → ScrCmd_nop1 |
0 | FRLG-only in effect (Emerald no-ops it) |
None of the six is FireRed-only by macro presence — but none is used by any script
in the FRLG ROM (0 hits for each across data/ and src/, excluding the command
table and handler). They are reachable only from external Mystery Event payloads
(e-Reader / Wonder Card), which are not in the ROM. That drives the priorities:
all six are low/medium, and only setmysteryeventstatus has a plausible runtime
source.
The Emerald 0xc8/0xc9 → ScrCmd_nop1 mapping is also a concrete input for the
opcode overlay design in rse-seams.md §3.5: an RSE overlay must re-declare both
slots as size 1 (nop1), not the FRLG size-5/size-1 pair, or every later row in
an RSE script desyncs.
5.1 0x0e setmysteryeventstatus — handoff, small: Mystery Event status slot
- Decision: give the engine a real status slot mirroring pret's
sMysteryEventScriptContext.data[2], written by this opcode from the normal VM and read back by the Mystery Event runner. No UI, no capability gate. - pret: writer
SetMysteryEventScriptStatussrc/mystery_event_script.c:92-95; readerMEventScript_Runsrc/mystery_event_script.c:75-80(status out-param at:78) /RunMysteryEventScript:83-88; failure pathSetIncompatiblesets3src/mystery_event_script.c:46-50; opcode handlersrc/scrcmd.c:269-273; consumerssrc/mystery_gift_client.c:257,296(FUNC_RUN_MEVENT); headerinclude/mystery_event_script.h:6-7. Note pret has a separate ME-script commandMEScrCmd_setstatus(src/mystery_event_script.c:129) ingMysteryEventScriptCmdTable— that is a different opcode space and is out of scope here. - Engine anchors: opcode declared
src/core/game3/scripting/opcodes.lua:27; dispatch skip pathsrc/core/game3/scripting/ops_a.lua:1977-1979; storage candidatessrc/core/game3/mystery_gift.lua(no status field today) andsrc/core/game3/scripting/natives_gift.lua:100-123. - Wiring: one
elseif op == "setmysteryeventstatus"case indispatchwritingctx.mysteryEventStatus = row[1]plusrequire("src.core.game3.mystery_gift").setStatus(row[1]); agetStatus()mirror. - Status value meanings (0/1/2/3):
[pret-unverified]— settling filesrc/mystery_event_script.c(the ME command handlers that read the slot). - Tests: new
tests/engine/game3_mystery_event_status_test.lua(VM row → write → readback; unknown id no-op); regressiontests/game3_gift_delivery_test.lua,tests/game3_gift_model_test.lua,tests/game3_gift_menu_test.lua. - Effort: small (~15 lines + test). Owner: handoff —
ops_a.luaandmystery_gift.luaare Finisher-owned.
5.2 0x24 gotonative — handoff, deferred: keep documented safe-skip, seam = symbol map
- Decision: do not emulate pret's ROM function-pointer jump (impossible
without executing ROM). Wire the op to a symbol resolver:
gotonative <word>looks upNatives.resolveNative(addr)→["native:" .. symbol]; unmapped logs once and skips (the current behaviour, made explicit). Leave the registry empty until an extracted symbol table exists. - pret: handler
src/scrcmd.c:92-97(SetupNativeScript(ctx, ScriptReadWord(ctx)));SetupNativeScriptsrc/script.c:70, declinclude/script.h:28— the word is abool8 (*)(void)native C function pointer, resumed by the script context. - Engine anchors:
opcodes.lua:52(declared, size 5{W}); no dispatch case (ops_a.lua:1977-1979skips it); disassembly special-cases itsrc/core/game3/scripting/disasm.lua:128; thenative:namespace has zero registrations —Natives.callnativenatives.lua:883-891always misses (review Q10). - Wiring: (1)
resolveNativeseam added to the natives registry, (2) oneelseif op == "gotonative"case, (3) later, the extractor emitsaddr → symbolfrom the ROM (T6.x, needsversionsoffsets). - Tests: new
tests/engine/game3_gotonative_test.lua(unmapped → skip + one log; registered symbol → handler invoked, returns TRUE = yield); regressiontests/game3_special_ids_test.lua,tests/game3_script_verbs_subset_test.lua. - Effort: seam small, symbol extraction medium (importer + ROM-gated).
Owner: handoff (
ops_a.lua,natives.luaFinisher) + extractor ticket. Priority low: 0 in-ROM users;[pret-unverified]whether any e-Reader payload emits it (settling file:src/mystery_event_script.cpayload format + RSE needs the pokeemerald clone).
5.3 0xaa createvobject — handoff, medium: virtual-object registry
- Decision: add a virtual-object registry (id → graphicsId, x, y, elevation,
direction) owned by a new module, rendered through the existing OW-sprite path with
no collision, no interaction and no script callback; cleaned up on map unload.
turnvobject(§5.4) mutates the same row. - pret: opcode handler
src/scrcmd.c:1171-1181; backendCreateVirtualObjectsrc/event_object_movement.c:1719(builds a real sprite withSpriteCB_VirtualObject, storessprite->sVirtualObjId/sVirtualObjElev); lookupGetVirtualObjectSpriteIdsrc/event_object_movement.c:9236; teardownDestroyVirtualObjectssrc/event_object_movement.c:9225; declinclude/event_object_movement.h:96. In pret these are sprites, not object events — they never collide or talk. - Engine anchors:
opcodes.lua:192(declared,8 {B,B,H,H,B,B}— layout matches pret ✅); no backend exists —grep -i virtual src/core/game3/objects.lua→ 0 hits; draw/collision owners aresrc/core/game3/objects.lua(Finisher) andsrc/core/game3/ow_sprites.lua(Finisher, L1/L4), withfield_view.lua(Refactor) in the draw path. - Wiring: new
src/core/game3/virtual_objects.lua(spawn(vObjId, graphicsId, x, y, elevation, direction),turn(id, direction),clear(),list()), consumed by the object draw pass; twodispatchcases inops_a.lua. Map-unload reset hooks alongside the existing object teardown inobjects.lua. - Engine ownership rule:
x/yareVarGethalves in pret (src/scrcmd.c:1174-1175), so the handler must resolve vars like the existingvar_gethelper (ops_a.lua:46-52) — do not read the raw halves. - Tests: new
tests/engine/game3_virtual_objects_test.lua(spawn keys the id, turn mutates direction, duplicate id replaces, clear empties, unknown id no-ops, vars resolved); regressiontests/game3_objects_perm_reset_test.lua,tests/game3_npc_player_collision_test.lua(must stay collision-free), plus an in-game visual check (headless tests render nothing). - Effort: medium (registry + draw hookup). Owner: split — new registry file is
A-lane-buildable;
objects.lua/ow_sprites.lua/ops_a.luawiring is handoff (all Finisher). Priority low (0 in-ROM users).
5.4 0xab turnvobject — paired with §5.3, handoff
- Decision: implement as
VirtualObjects.turn(id, direction); a missing id is a logged no-op (pret:GetVirtualObjectSpriteIdreturnsMAX_SPRITESandTurnVirtualObjectdoes nothing —src/event_object_movement.c:9248-9257). - Engine anchors:
opcodes.lua:193; no dispatch case (ops_a.lua:1977-1979). - Wiring: one
elseifcalling the §5.3 registry. - Tests: covered by
tests/engine/game3_virtual_objects_test.lua; add the missing-id no-op row there. - Effort: trivial (subset of §5.3). Owner: handoff (same wiring files).
5.5 0xc8 loadhelp — handoff, small: dedicated help message window, not the L/R Help system
- Decision: implement a help message window backend (a
Windowtemplate +Window.printat pret's fixed position, opened/closed by the opcodes). This is notsrc/ui/game3/help_system.lua(the L/R context browser) and is not capability-gated byhelpSystem— that flag maps topokefirered/src/help_system.c, whereas pret's window lives insrc/new_menu_helpers.cand is also used by the start menu (src/start_menu.c:332,440), so the primitive is shared UI plumbing. - pret: handler
src/scrcmd.c:1274-1280(msg = ScriptReadWord(ctx); if NULL falls back to(const u8 *)ctx->data[0]); backendDrawHelpMessageWindowWithTextsrc/new_menu_helpers.c:701-705(LoadHelpMessageWindowGfx→PrintTextOnHelpMessageWindow(text, 2)); window id fromGetStartMenuWindowId()src/new_menu_helpers.c:677. Emerald maps this slot toScrCmd_nop1(data/script_cmd_table.inc:218), so R/S+Emerald scripts never ask for it. - Engine anchors:
opcodes.lua:222(size 5{W}✅); skip pathops_a.lua:1977-1979; text resolution already exists —resolve_text(vm, ptr)ops_a.lua:36-45maps a pointer throughOpcodes.key→vm:getText(key), and the NULL/0 fallback in pret maps ontoresolve_text's ownptr == nil/0 → ctx.data[0]handling; window primitivessrc/ui/game3/window.lua:62 Window.template,:86 stdFrame,:114 Window.print; the existing Help browser is a different API (help_system.lua:133 Help.show,:101 Help.close). - Wiring: one
elseif op == "loadhelp"case:local ir = resolve_text(vm, row[1]), thenHelpWindow.show(TextIR.toPlain(ir, { stringVars = ... })). Do not emulate pret's NULL-cast fallback beyondresolve_text's existing behaviour — it is dead/unsafe in pret (0 users). - Tests: new
tests/engine/game3_help_window_opcode_test.lua(row → window open with the resolved string,resolve_textfallback for ptr 0, overlay id); regressiontests/game3_help_test.lua(L/R Help browser unchanged) andtests/game3_flags_extract_preserve_test.lua. Visual check in-game (headless draws nothing). - Effort: small (new file ~60 lines + one dispatch case). Owner: split —
new
src/ui/game3/help_window.luais A-lane-buildable; theops_a.luacase is handoff (Finisher). Priority low.
5.6 0xc9 unloadhelp — paired with §5.5, handoff
- Decision:
HelpWindow.close(); safe to call when nothing is open (pretDestroyHelpMessageWindow_(2)—src/new_menu_helpers.c:707-710). - Engine anchors:
opcodes.lua:223(size 1 ✅); no dispatch case. - Wiring: one
elseifcalling the §5.5 backend. - Tests: covered by
tests/engine/game3_help_window_opcode_test.lua(close with no window = no-op). - Effort: trivial (subset of §5.5). Owner: handoff (
ops_a.lua).
5.7 Summary for the ticket queue
| Op | Backend decision | Effort | Owner | Priority |
|---|---|---|---|---|
setmysteryeventstatus |
Mystery Event status slot on the gift context | small | handoff (ops_a + mystery_gift) | medium (external payloads can reach it) |
gotonative |
symbol map + documented skip; no ROM-pointer dispatch | small seam / medium extraction | handoff + extractor ticket | low (0 in-ROM users) |
createvobject |
virtual-object registry (new file) + draw hookup | medium | A-lane file, handoff wiring | low (0 in-ROM users) |
turnvobject |
same registry, turn() |
trivial | handoff | low |
loadhelp |
dedicated help message window (new file), resolve_text for the word |
small | A-lane file, handoff wiring | low (Emerald no-ops; 0 users) |
unloadhelp |
same window, close() |
trivial | handoff | low |
Open [pret-unverified] items: (a) the meaning of the Mystery Event status values,
settling file src/mystery_event_script.c; (b) whether any external e-Reader /
Wonder Card payload emits gotonative/loadhelp, settling file: src/mystery_event_script.c
MEventScript_InitContext (:52-62, :70-73) and the payload entry points; (c) Ruby/Sapphire emitting gotonative (its table lists the slot at
pokeruby data/script_cmd_table.inc:39 but event.inc has no macro) — settling file:
a pret/pokeruby clone; (d) all RSE versions_rse.lua offsets — settling file: the
RSE carts.
5.8 0xa8 setobjectsubpriority / 0xa9 resetobjectsubpriority — handoff, medium: object → draw-order seam
ADDENDUM: E10 spec wired-ready corrected 20 → 18; set/resetobjectsubpriority reclassified to seam class per lead after Finisher call-back 5d.
- Decision: freeze the object's draw-order pair on the object record and
stop the per-frame elevation recompute for that object — mirroring pret's
fixedPriorityflag. Store{ fixedPriority = true, subpriority = <byte + 83>, fixedClass = <captured> }on the EventObject; reset drops all three so elevation-driven priority resumes. Do not wireOam.setSubpriority— that module is compositor/UI only (see the anchor list), so the field's ordering seam isfield_view's split + sort, not the OAM pool. - pret: handler
src/scrcmd.c:1122-1130(SetObjectSubpriority(localId, mapNum, mapGroup, priority + 83)— keep the+ 83), reset handlersrc/scrcmd.c:1133-1140; settersSetObjectSubprioritysrc/event_object_movement.c:2089-2101(objectEvent->fixedPriority = TRUE; sprite->subpriority = subpriority;, guarded byTryGetObjectEventIdByLocalIdAndMap, FALSE = found) andResetObjectSubprioritysrc/event_object_movement.c:2104-2116(fixedPriority = FALSE; triggerGroundEffectsOnMove = TRUE— it does not restore an old value, it re-enables the dynamic path); the two freeze points that make it stick:UpdateObjectEventElevationAndPrioritysrc/event_object_movement.c:8379-8387andObjectEventUpdateSubprioritysrc/event_object_movement.c:8424-8429bothreturnwhenfixedPriority; the dynamic writer they gate isSetObjectSubpriorityByElevationsrc/event_object_movement.c:8414-8422. OtherfixedPrioritywriters for context:src/field_player_avatar.c:2050,2101,2133,src/event_object_movement.c:4483,7193,7200. - Usage: 0 scripts in the FRLG ROM emit either op (only the command table,
data/script_cmd_table.inc:172-173) — same low-priority class as §5.1–5.6. - Engine anchors: layout already corrected to pret (Finisher's live bug fix) —
src/core/game3/scripting/opcodes.lua:188op("setobjectsubpriority", 6, {H,B,B,B})and:191op("resetobjectsubpriority", 5, {H,B,B}), both citingscrcmd.c:1122/1133; no dispatch case (src/core/game3/scripting/ops_a.lua:1977-1979skip path). Object store:Objects._byId = {} -- [localId] = EventObjectsrc/core/game3/objects.lua:27, spawn at:120-145. Per-frame recompute:ELEVATION_TO_PRIORITYsrc/core/game3/field_view.lua:389-397,actorPriority():396-417,a.priority = actorPriority(a):554, over/under split:555-562(< 2→ over),sortActors:563-570(sortY, then insertion index), actor drawdrawSingleActor:418-452(sprite renderer /ow_sprites, nooam.lua).Oam.setSubprioritysrc/core/game3/oam.lua:289-294has zero callers;oam.luais required only by compositor/UI paths (intro_movie.lua:4,title_screen.lua:3,display.lua:129/227/256,party_menu.lua:10,evolution_scene.lua:15,new_game_scene.lua:4,egg_hatch.lua:8,src/core/game3/init.lua:28) — the overworld never loads it. - Wiring (three files, all handoff):
src/core/game3/objects.lua(Finisher): addObjects.setFixedPriority(localId, mapId, subpriority)— resolve the object inObjects._byId, ignore +a.logwhenmapIddoes not match the object's map (engine equivalent of pret'sTryGetObjectEventIdByLocalIdAndMapguard), else setobj.fixedPriority, obj.subpriority, obj.fixedClass = true, subpriority, nil; andObjects.resetFixedPriority(localId, mapId)clearing all three.src/core/game3/scripting/ops_a.lua(Finisher): twoelseifcases —setobjectsubpriorityreadsrow[1..4](H localId, B group, B num, B priority), resolves the halfword through the existingvar_get(ops_a.lua:70-76) as pret does withVarGet, builds the engine map id from group/num the wayshowobjectatalready does, and calls the setter withrow[4] + 83;resetobjectsubprioritydoes the same for the reset.src/core/game3/field_view.lua(Refactor): honour the freeze —and in-- :554 local obj = a.obj if obj and obj.fixedPriority then if obj.fixedClass == nil then obj.fixedClass = a.priority or actorPriority(a) end a.priority = obj.fixedClass -- frozen: stop elevation updates a.subpriority = obj.subpriority -- sort key, replaces sortY for this actor else a.priority = actorPriority(a) a.subpriority = nil endsortActors(:563-570) order by(a.subpriority or a.sortY or a.y)so a frozen actor lands where the script put it rather than where its Y says.- Explicit non-wiring:
Oam.setSubprioritystays caller-less; wiring it would reach a module the field never loads (verified above). If an actor path ever moves onto the OAM pool, that is a separate ticket.
- Tests: new
tests/engine/game3_object_subpriority_test.lua— set writespriority + 83, wrong map id is a logged no-op, reset clears all three fields,fixedClasscaptures only on first frame, and a two-actor fixture proves the frozen actor wins the sort while an elevation-driven neighbour still followsELEVATION_TO_PRIORITY. Regression:tests/game3_elevation_oam_priority_test.lua,tests/game3_collision_npc_dir_test.lua,tests/game3_npc_player_collision_test.lua(must stay green — priority changes can reshuffle draw order), plus an in-game layering check (headless draws nothing). - Effort: medium (3 files, one per-frame path). Owner: all three files are handoff (objects/ops_a Finisher, field_view Refactor) — no A-lane file here.
- [pret-unverified]: whether any e-Reader/Mystery Event payload emits these two
ops — settling file
src/mystery_event_script.cpayload handling; and whether the engine's map id is derivable from (group, num) without the ROM's map group table — settling filesrc/import/gba/map_catalog.lua.
5.9 H5 residual — Trainer Tower stash copy after the challenge bracket
- (a) Memory-only or persisted? — PERSISTED (one of the two copies).
Tower.savePlayerPartywrites bothsession.modData.savedPlayerParty(trainer_tower.lua:535, wheresave_block()issession.modData,trainer_tower.lua:519-523) andsession.savedPlayerParty(:536). The schema passesmodDatastraight through to the file:save_schema_firered.lua:245modData = session.modData(restore:311, new game:175), andsavedPlayerPartyis not itself a schema key, so:modData.savedPlayerParty→ in the save file;session.savedPlayerParty→ memory only, dropped on load. The load path prefers the memory copy (trainer_tower.lua:544), so across save+reload only themodDatacopy survives. - (b) Does pret clear it anywhere? — NO clear point exists.
pret's stash target is the save block's own party array:
SavePlayerPartysrc/load_save.c:157-166copies live →gSaveBlock1Ptr->playerParty;LoadPlayerPartysrc/load_save.c:167-176copies back. Nothing ever zeroes that area — it is only overwritten by the nextSavePlayerParty(), whose callers aresrc/load_save.c:198(insideSaveSerializedGame),src/battle_setup.c:417,src/teachy_tv.c:1178,src/union_room.c:1814,1906,1914,1923, and the scriptsdata/maps/SevenIsland_House_Room1/scripts.inc:87,98,data/maps/SevenIsland_House_Room2/scripts.inc,data/scripts/cable_club.inc:288.src/trainer_tower.cnever stashes or clears a party (0 hits forSavePlayerParty/LoadPlayerParty;src/trainer_tower.c:1033-1035reads the livegPlayerParty), confirming the lead's note. The reads are the specials wired insrc/core/game3/scripting/natives_tower.lua:505-515← script specialsdata/specials.inc:50-51, called by the Seven Island reception scripts (data/maps/SevenIsland_House_Room1/scripts.inc:45,95,111,Room2/scripts.inc:32). - (c) Verdict: NO ACTION — document only. Two grounds:
- There is nothing pret-faithful to clear. pret's copy lives in
gSaveBlock1Ptr->playerParty, which is refreshed on every save bySaveSerializedGame(src/load_save.c:196-201) and never freed — so any clear-on-Load (or clear-on-Run-end) the engine invents is engine behaviour with no pret counterpart. The Finisher's revert was correct: keepLoadPlayerPartywithout a consume (tests/game3_tower_party_test:3pins it). - The lingering copy is not read outside the tower path — consumers are only
natives_tower.lua:505-515,Tower.savedPlayerPartytrainer_tower.lua:559-562andTower.copyHeldItems:806-820, all of which the reception script also gates. The stale-restore window is therefore the same script window pret has.
- There is nothing pret-faithful to clear. pret's copy lives in
- Trade-off recorded for the lead (open, low stakes): the engine's
modDatacopy is extra durability — it survives a mid-challenge save+reload where pret's save block would already have been overwritten bySaveSerializedGame. Dropping it would save 6 deep-copied mon records per save but would also let a mid-challenge save strand the player on the reduced party (a state pret itself can reach). Recommendation: keep it, no code change; revisit only if save size becomes a concern, and then by moving the copy under a save-time scrub ofmodDatakeys, not by clearing mid-run. Owner: none (no ticket). Tests that already pin the current behaviour:tests/game3_tower_party_test:3,172,425-426,tests/drivers/game3_tower_run.lua:289.