Files
gen1recomp/tests/game3_object_interactions_cache_test.lua
T
Shane McGovern e2454bf568 fix(game3): review v3 fixes (battle, field, script, save) + gated T6 tier
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.
2026-09-22 12:22:50 +01:00

104 lines
5.5 KiB
Lua

package.path='./?.lua;./?/init.lua;'..package.path
if not arg[1] then print('SKIP object cache test: supply FireRed cache root');return end
local root=arg[1];local cache={read=function(_,path)
if path=='data/generated/gba/objects/pack.lua' and arg[2] then
local f=assert(io.open(arg[2]));local data=f:read('*a');f:close();return data end
local f=io.open(root..'/'..path);if not f then return end;local s=f:read('*a');f:close();return s end}
local bundle=assert(require('src.import.gba.extract_scripts').loadBundle(cache))
local Field=require('src.core.game3.field');local P=require('src.core.game3.player')
local Space=require('src.core.game3.scripting.space');local C=require('src.core.game3.collision')
local O=require('src.core.game3.objects');O.at=function()return nil end
Space.active=true;Space.bundle=bundle
local tests={{'FR_VIRIDIAN_CITY_SCHOOL',4,4,'up','notebook'},
{'FR_SSANNE_CAPTAINS_OFFICE',2,4,'up','seasickness'},
{'FR_RIVALS_HOUSE',12,1,'up','books'}}
for _,case in ipairs(tests) do
local map,x,y,dir=unpack(case)
local events=assert(bundle.events[map],map..' absent')
local ev
for _,e in ipairs(events.bgEvents or {}) do if e.x==x and e.y==y then ev=e end end
if not ev then print('EVENTS',map);for _,e in ipairs(events.bgEvents or {})do print(e.x,e.y,e.scriptKey)end end
assert(ev,'event missing '..map)
assert(bundle.scripts[ev.scriptKey],'script missing '..ev.scriptKey)
local messages={}
Space.vm=require('src.core.game3.scripting.vm').new({scripts=bundle.scripts,text=bundle.text,movements=bundle.movements,
onMessage=function(t)messages[#messages+1]=t end,askYesNo=function(cb)cb(false)end})
Space.store=Space.vm.store
Field._session={map=map};Field.running=true;Field.locked=false
P.cellX=x;P.cellY=y+1;P.facing=dir;P.moving=false
C._grid=nil;C._mapDef=nil
local game={data={maps={[map]={bgEvents=events.bgEvents}}}}
assert(Field.interact(game),'A-button not handled '..map)
for _=1,300 do Space.vm:tick()end
assert(#messages>0,'no text '..map)
assert(table.concat(messages,' '):lower():find(case[5],1,true),'wrong map-specific text')
assert(not Space.vm:isRunning())
end
print('PASS actual A-button dispatch: school notebook, captain book, rival bookshelf')
-- Audit every imported map and exercise each furniture behavior found there.
local native='data/generated/gba/native/'
local manifest=assert(loadstring(assert(cache:read(native..'manifest.lua'))))()
local I=require('src.core.game3.scripting.interaction_scripts')
-- data/scripts/cable_club.inc:566
local SCREEN_ONLY={CableClub_EventScript_ShowBattleRecords=true}
local covered,maps,cells={},0,0
local order={};for map in pairs(manifest.layouts)do order[#order+1]=map end;table.sort(order)
for _,map in ipairs(order) do
local info=manifest.layouts[map]
local decoded=assert(require('src.import.gba.native_pack').decodeMidLayout(assert(cache:read(native..info.file))))
local layout=require('src.core.game3.layout_native').fromDecoded(decoded,map,info.pair)
local attrs=assert(I.behaviors[info.pair], 'missing pair '..info.pair)
local def={pair=info.pair,midLayout=layout,warps={}}
C.bindMap(nil,map,def)
maps=maps+1
local bg=(bundle.events[map] or {}).bgEvents or {}
for y=0,layout.height-1 do for x=0,layout.width-1 do
local behavior=assert(attrs[layout:midAt(x,y)],'missing metatile behavior')
assert(C.behavior(x,y)==behavior)
local key=I.scriptFor(behavior,'up')
if key and behavior~=0x83 and behavior~=0x85 then
cells=cells+1
-- src/field_control_avatar.c:392-398
local shadowed=false
for _,e in ipairs(bg) do if e.x==x and e.y==y then shadowed=true end end
if not covered[behavior] and y+1<layout.height and not shadowed then
local messages={}
Space.vm=require('src.core.game3.scripting.vm').new({scripts=bundle.scripts,text=bundle.text,
movements=bundle.movements,onMessage=function(t)messages[#messages+1]=t end,
askYesNo=function(cb)cb(false)end})
Space.store=Space.vm.store
Field._session={map=map};Field.running=true;Field.locked=false
P.cellX=x;P.cellY=y+1;P.facing='up';P.moving=false
local game={data={maps={[map]=def}}}
Field.locked=true;assert(not Field.interact(game));Field.locked=false
assert(Field.interact(game),'furniture A-button not handled '..key)
for _=1,100 do Space.vm:tick()end
if SCREEN_ONLY[key] then
assert(#messages==0,'unexpected text from '..key)
-- pokefirered/data/scripts/cable_club.inc:566-575 +
-- pokefirered/src/battle_records.c:83: special ShowBattleRecords + waitstate
-- parks until the player closes the screen, and releaseall runs only after
-- the special returns - so control must NOT release while the screen is up.
assert(Space.vm:isRunning(),'parks on the waitstate with the records screen up')
local Records=require('src.ui.game3.trainer_tower_records')
local Fade=require('src.ui.game3.fade')
assert(Records.isOpen(),'the records screen is up')
Records.close()
for _=1,60 do Fade.tick(1/60)end
for _=1,100 do Space.vm:tick()end
else
assert(#messages==1 and #messages[1]>0,'furniture text missing '..key)
end
assert(not Space.vm:isRunning(),'furniture did not release control '..key)
covered[behavior]=true
end
end
end end
end
local count=0;for _ in pairs(covered)do count=count+1 end
assert(count>=20,'unexpectedly few furniture behaviors exercised')
for _,row in ipairs(I.CODE) do assert(covered[row[1]],'code-root behavior never dispatched '..row[2]) end
print(('PASS %d maps audited, %d furniture cells, %d furniture types dispatched'):format(maps,cells,count))