Files
gen1recomp/tests/game3_trade_scene_test.lua
2026-09-23 16:34:15 -04:00

316 lines
12 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/trade_scene.c:1337 DoTradeAnim_Cable
package.path = "./?.lua;./?/init.lua;" .. package.path
package.loaded["src.core.game3.rom_text"] = {
plain = function(key) return key end, box = function(key) return key end,
ascii = function(key) return key end, has = function() return true end,
key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
count = function() return 0 end, list = function() return {} end,
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
}
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
package.loaded["src.core.game3.audio"] = setmetatable({}, {
__index = function() return function() end end,
})
local store = { flags = {}, vars = {} }
local session = {
store = store, map = "FR_ROUTE2_HOUSE", party = {},
name = "RED", trainerId = 4242, vars = {}, flags = {},
dex = { seen = {}, owned = {} },
}
package.loaded["src.core.game3.runtime"] = {
getSession = function() return session end,
isActive = function() return true end,
}
local TradeScene = require("src.core.game3.trade_scene")
local function run(offer, received, opts, limit)
local order, counts = {}, {}
local frames = 0
TradeScene.play(offer, received, nil, opts)
local seen = TradeScene.phase()
order[#order + 1] = seen
counts[seen] = 0
while frames < (limit or 20000) do
frames = frames + 1
local over = TradeScene.step()
counts[seen] = (counts[seen] or 0) + 1
if over then break end
local now = TradeScene.phase()
if now ~= seen then
seen = now
order[#order + 1] = seen
counts[seen] = 0
end
end
return order, counts, frames
end
local OFFER = { species = 63, nickname = "ABRA", level = 12 }
local RECEIVED = { species = 122, nickname = "MIMIEN", otName = "REYLEY", otId = 1985, level = 12 }
print("[test] 1. every cable-trade phase runs in pret's order")
-- pokefirered/src/trade_scene.c:1266 the STATE_* enum, cable branch
local EXPECTED = {
{ "start", 1 },
{ "mon_slide_in", 61 },
{ "send_msg", 1 },
{ "bye_bye", 80 },
{ "pokeball_depart", 64 },
{ "pokeball_depart_wait", 87 },
{ "fade_out_to_gba_send", 1 },
{ "wait_fade_out_to_gba_send", 16 },
{ "fade_in_to_gba_send", 1 },
{ "wait_fade_in_to_gba_send", 16 },
{ "gba_zoom_out", 16 },
{ "gba_flash_send", 21 },
{ "gba_stop_flash_send", 126 },
{ "pan_away_gba", 32 },
{ "create_link_mon_leaving", 1 },
{ "link_mon_travel_out", 75 },
{ "link_mon_travel_offscreen", 45 },
{ "fade_out_to_crossing", 1 },
{ "wait_fade_out_to_crossing", 16 },
{ "fade_in_to_crossing", 1 },
{ "wait_fade_in_to_crossing", 16 },
{ "crossing_link_mons_enter", 14 },
{ "crossing_blend_white_1", 1 },
{ "crossing_blend_white_2", 1 },
{ "crossing_blend_white_3", 1 },
{ "crossing_create_mon_pics", 1 },
{ "crossing_mon_pics_move", 75 },
{ "crossing_link_mons_exit", 44 },
{ "create_link_mon_arriving", 16 },
{ "fade_out_to_gba_recv", 1 },
{ "wait_fade_out_to_gba_recv", 16 },
{ "link_mon_travel_in", 28 },
{ "pan_to_gba", 76 },
{ "destroy_link_mon", 1 },
{ "link_mon_arrived_delay", 10 },
{ "move_gba_to_center", 33 },
{ "gba_flash_recv", 1 },
{ "gba_stop_flash_recv", 126 },
{ "gba_zoom_in", 19 },
{ "fade_out_to_new_mon", 1 },
{ "wait_fade_out_to_new_mon", 16 },
{ "fade_in_to_new_mon", 1 },
{ "wait_fade_in_to_new_mon", 16 },
{ "pokeball_arrive", 1 },
{ "fade_pokeball_to_normal", 1 },
{ "pokeball_arrive_wait", 107 },
{ "show_new_mon", 1 },
{ "new_mon_msg", 1 },
{ "delay_for_mon_anim", 61 },
{ "wait_for_mon_cry", 1 },
{ "take_care_of_mon", 250 },
{ "after_new_mon_delay", 60 },
{ "check_ribbons", 1 },
{ "end_link_trade", 1 },
{ "try_evolution", 1 },
{ "wait_evolution", 1 },
{ "fade_out_end", 1 },
{ "wait_fade_out_end", 16 },
}
local order, counts, total = run(OFFER, RECEIVED, { art = { gba = true } })
eq(#order, #EXPECTED, "the cinema ran " .. #EXPECTED .. " phases")
for i, want in ipairs(EXPECTED) do
eq(order[i], want[1], "phase " .. i .. " is " .. want[1])
eq(counts[want[1]], want[2], want[1] .. " lasts " .. want[2] .. " frames")
end
local sum = 0
for _, want in ipairs(EXPECTED) do sum = sum + want[2] end
eq(total, sum, "the whole cinema is " .. sum .. " frames")
check(not TradeScene.isOpen(), "the scene closed itself when the last phase ended")
print("[test] 1b. pokefirered/src/trade_scene.c:1121 BG2 mon shadow under the sent and received mons")
do
local shadow = {}
TradeScene.play(OFFER, RECEIVED, nil, { art = { gba = true } })
for _ = 1, 20000 do
local ph = TradeScene.phase()
local st = TradeScene.state()
if ph and st and shadow[ph] == nil then
shadow[ph] = { on = st.monShadowBg == true, hofs = st.bg2hofs }
end
if TradeScene.phase() == "end_link_trade" then TradeScene.pressA() end
if TradeScene.step() then break end
end
check(shadow.bye_bye and shadow.bye_bye.on, "the shadow platform is up while Bye-bye shows")
eq(shadow.bye_bye and shadow.bye_bye.hofs, 0, "and it has slid in with the mon")
check(shadow.gba_flash_send and not shadow.gba_flash_send.on, "BG2 is the GBA screen during the send")
check(shadow.take_care_of_mon and shadow.take_care_of_mon.on, "the shadow is back under the received mon")
eq(shadow.take_care_of_mon and shadow.take_care_of_mon.hofs, 0, "at hofs 0 (trade_scene.c:1210)")
end
print("[test] 2. a nil art sheet degrades to the timed hold")
local holdOrder, holdCounts, holdTotal = run(OFFER, RECEIVED, nil)
local held = {}
for _, name in ipairs(holdOrder) do held[name] = true end
check(held.hold, "the no-art run holds on black instead of the transfer")
for _, name in ipairs({
"gba_zoom_out", "gba_flash_send", "pan_away_gba", "link_mon_travel_out",
"crossing_mon_pics_move", "gba_zoom_in", "link_mon_travel_in",
}) do
check(not held[name], "the no-art run skips " .. name)
end
check(held.fade_out_to_gba_send, "the no-art run keeps pret's fade to black")
eq(holdCounts.hold, TradeScene.HOLD_FRAMES, "the hold lasts HOLD_FRAMES")
eq(holdCounts.mon_slide_in, 61, "the art-free slide-in keeps pret's 61 frames")
eq(holdCounts.take_care_of_mon, 250, "the art-free reveal keeps pret's 250 frames")
check(holdTotal < total, "the held run is shorter than the full cinema")
print("[test] 3. the ball velocity table is pret's")
-- pokefirered/src/trade_scene.c:538
eq(TradeScene.BALL_VELOCITY[0], 0, "velocity[0]")
eq(TradeScene.BALL_VELOCITY[22], -4, "velocity[22]")
eq(TradeScene.BALL_VELOCITY[43], 0, "velocity[43]")
eq(TradeScene.BALL_VELOCITY[66], -4, "velocity[66]")
eq(TradeScene.BALL_VELOCITY[95], -1, "velocity[95]")
eq(TradeScene.BALL_VELOCITY[107], 3, "velocity[107]")
eq(TradeScene.BALL_VELOCITY[108], nil, "the table stops at 108 entries")
if not require("tests.game3_cache").mount() then
print("[skip] 4-5: the in-game trades are ROM data: " .. tostring(require("tests.game3_cache").reason))
else
print("[test] 4. the received mon lands in the party exactly once")
local Trade = require("src.core.game3.scripting.natives_trade")
local sent = { species = 63, nickname = "ABRA", level = 12, mail = nil }
local offered = {
species = 122, nickname = "MIMIEN", otName = "REYLEY", otId = 1985, level = 12,
}
session.party = { sent, { species = 25, nickname = "PIKA", level = 9 } }
Trade._offered = offered
local swaps = 0
local realTradeMons = Trade.tradeMons
Trade.tradeMons = function(...)
swaps = swaps + 1
return realTradeMons(...)
end
local task = Trade.sceneTask(nil, nil, 0, 0)
local guard = 0
while guard < 20000 do
guard = guard + 1
if task() then break end
end
Trade.tradeMons = realTradeMons
check(guard < 20000, "the scene task finished, frames=" .. guard)
eq(swaps, 1, "TradeMons ran exactly once")
eq(session.party[1], offered, "the received mon took the sent mon's slot")
eq(#session.party, 2, "the party did not grow")
eq(session.party[2].nickname, "PIKA", "the rest of the party is untouched")
eq(session.dex.owned[122], true, "the received species is registered as owned")
print("[test] 5. a second run of the same task does not swap again")
swaps = 0
Trade.tradeMons = function(...)
swaps = swaps + 1
return realTradeMons(...)
end
local done = task()
Trade.tradeMons = realTradeMons
check(done, "the finished task stays finished")
eq(swaps, 0, "no second swap")
eq(session.party[1], offered, "the party still holds the received mon once")
end
print("[test] 6. the map music is cached at the start and put back at the end")
-- pokefirered/src/trade_scene.c:1348
local MAP_SONG = 314
local realAudio = package.loaded["src.core.game3.audio"]
local realLove = _G.love
local songs = {}
package.loaded["src.core.game3.audio"] = {
_mapSong = MAP_SONG,
_currentSong = { id = MAP_SONG },
playSe = function() end,
playFanfare = function() end,
playCry = function() end,
isCryFinished = function() return true end,
playSong = function(id) songs[#songs + 1] = tonumber(id) end,
playMapSong = function(id) songs[#songs + 1] = tonumber(id) end,
}
_G.love = { graphics = {} }
local function runWithMusic(opts)
songs = {}
TradeScene.play(OFFER, RECEIVED, nil, opts)
TradeScene.step()
local cached = TradeScene.state() and TradeScene.state().cachedMapMusic
local guard = 0
while guard < 20000 do
guard = guard + 1
if TradeScene.phase() == "end_link_trade" then TradeScene.pressA() end
if TradeScene.step() then break end
end
return cached
end
local cachedInGame = runWithMusic({})
local inGameSongs = songs
local cachedLink = runWithMusic({ peer = { name = "TRIS", id = 31337 }, uiDriven = true })
local linkSongs = songs
_G.love = realLove
package.loaded["src.core.game3.audio"] = realAudio
eq(cachedInGame, MAP_SONG, "the in-game arm caches the map music at STATE_START")
eq(inGameSongs[1], 264, "STATE_START plays MUS_EVOLUTION")
-- pokefirered/src/trade_scene.c:1790
eq(inGameSongs[#inGameSongs], MAP_SONG, "the in-game arm ends on the cached map music")
eq(cachedLink, MAP_SONG, "the link arm caches the map music too")
-- pokefirered/src/trade_scene.c:2290
eq(linkSongs[#linkSongs], MAP_SONG, "the link arm ends on the cached map music")
check(not TradeScene.isOpen(), "both runs closed the scene")
if not require("tests.game3_cache").mount() then
print("[skip] 7: the in-game trades are ROM data: " .. tostring(require("tests.game3_cache").reason))
else
print("[test] 7. a trade with no mon to swap still fades the field back in")
local Trade = require("src.core.game3.scripting.natives_trade")
local realFade = package.loaded["src.ui.game3.fade"]
local fades = {}
package.loaded["src.ui.game3.fade"] = {
MODE = { TO_BLACK = "to_black", FROM_BLACK = "from_black" },
begin = function(mode) fades[#fades + 1] = mode end,
}
session.party = {}
Trade._offered = nil
local bail = Trade.sceneTask(nil, nil, 0, 3)
local bailFrames = 0
while bailFrames < 20000 do
bailFrames = bailFrames + 1
if bail() then break end
end
package.loaded["src.ui.game3.fade"] = realFade
check(bailFrames < 20000, "the bail-out task finished, frames=" .. bailFrames)
eq(fades[1], "to_black", "it still fades out first")
eq(fades[#fades], "from_black", "and the last fade brings the field back")
end
finish()