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

279 lines
11 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local Vm = require("src.core.game3.scripting.vm")
local Flags = require("src.core.game3.scripting.flags")
local Std = require("src.core.game3.scripting.stdscripts")
local Natives = require("src.core.game3.scripting.natives")
local okSeag, Seagallop = pcall(require, "src.core.game3.scripting.natives_seagallop")
if not okSeag then Seagallop = { WARPS = {} } end
local Fade = require("src.ui.game3.fade")
local SEAGALLOP_VERMILION_CITY = 0
local SEAGALLOP_ONE_ISLAND = 1
local SEAGALLOP_TWO_ISLAND = 2
local SEAGALLOP_THREE_ISLAND = 3
local SEAGALLOP_FOUR_ISLAND = 4
local SEAGALLOP_FIVE_ISLAND = 5
local SEAGALLOP_SIX_ISLAND = 6
local SEAGALLOP_SEVEN_ISLAND = 7
local SEAGALLOP_CINNABAR_ISLAND = 8
local SEAGALLOP_NAVEL_ROCK = 9
local SEAGALLOP_BIRTH_ISLAND = 10
local SEAGALLOP_MORE = 254
local SCR_MENU_CANCEL = 127
-- pokefirered/data/scripts/seagallop.inc:102 EventScript_SetSail
local function setSailScript(origin, dest)
return {
{ op = "setvar", var = 0x8004, value = origin },
{ op = "setvar", var = 0x8006, value = dest },
{ op = "specialvar", [1] = 0x800D, [2] = Std.SPECIAL.GetSeagallopNumber },
{ op = "fadescreen", [1] = 1 },
{ op = "special", id = Std.SPECIAL.DoSeagallopFerryScene },
{ op = "waitstate" },
{ op = "copyvar", [1] = 0x4000, [2] = 0x8006 },
{ op = "end" },
}
end
local function newVm(scripts, adapters)
return Vm.new({
store = Flags.newStore(),
scripts = scripts,
adapters = adapters,
})
end
local function logAdapters(extra)
local a = {
log = function(m) print("[log] " .. tostring(m)) end,
fadeScreen = function(mode, speed, done)
Fade.begin(mode, speed, function() if done then done() end end)
Fade.t = (mode == 1 or mode == 3) and 16 or 0
Fade.active = false
local cb = Fade.doneCb
Fade.doneCb = nil
if cb then cb() end
end,
}
for k, v in pairs(extra or {}) do a[k] = v end
return a
end
print("[test] 1. special 0x17B is bound and is not logged as unknown")
Natives.resetLog()
local unknown = {}
do
local warps = {}
local a = logAdapters({
log = function(m)
if tostring(m):match("skip unknown") then unknown[#unknown + 1] = m end
end,
warp = function(g, n, w, x, y, done, kind)
local covered = not Fade.isActive() and (tonumber(Fade.t) or 0) >= 16
warps[#warps + 1] = { g, n, w, x, y, done, kind, covered }
if done then done() end
end,
})
local vm = newVm({ sail = setSailScript(SEAGALLOP_CINNABAR_ISLAND, SEAGALLOP_ONE_ISLAND) }, a)
vm:start("sail")
check(#unknown == 0, "nothing logged as an unknown special (got " .. #unknown .. ")")
check(Flags.getVar(vm.store, vm.ctx, 0x800D) == 1,
"GetSeagallopNumber = 1 for the Cinnabar run (got " ..
tostring(Flags.getVar(vm.store, vm.ctx, 0x800D)) .. ")")
print("[test] 2. waitstate holds the script while the ferry crosses")
check(vm:isRunning(), "the script is still alive after `special` + `waitstate`")
check(#warps == 0, "no warp on the frame the ferry leaves")
local ranFor = 0
for _ = 1, 400 do
if #warps > 0 then break end
ranFor = ranFor + 1
vm:tick()
end
check(#warps == 1, "the ferry warped exactly once (got " .. #warps .. ")")
check(ranFor >= 100, "waitstate suspended the script for the crossing, ticks=" .. ranFor)
local w = warps[1]
check(w and w[1] == 32 and w[2] == 4,
"warped to MAP_ONE_ISLAND_HARBOR group 32 num 4 (got " ..
tostring(w and w[1]) .. "/" .. tostring(w and w[2]) .. ")")
check(w and w[4] == 8 and w[5] == 5,
"landed on sSeag coords (8,5), got (" .. tostring(w and w[4]) .. "," .. tostring(w and w[5]) .. ")")
print("[test] 3. the ferry warps from a covered screen and leaves the fade-in to the warp")
check(w and w[7] == "seagallop", "the ferry warp is a seagallop warp (got " .. tostring(w and w[7]) .. ")")
check(w and w[8] == true, "the screen was covered when the ferry warped")
check(Fade.mode ~= Fade.MODE.FROM_BLACK and Fade.t >= 16,
"the ferry does not start its own FADE_FROM_BLACK (mode=" .. tostring(Fade.mode) .. ")")
print("[test] 4. the script finishes after the warp")
for _ = 1, 10 do vm:tick() end
check(not vm:isRunning(), "the VM reached `end` instead of hanging on waitstate")
end
print("[test] 5. every seagallop id has the pret warp row")
local WANT = {
[SEAGALLOP_VERMILION_CITY] = { 3, 5, 0x17, 0x20 },
[SEAGALLOP_ONE_ISLAND] = { 32, 4, 8, 5 },
[SEAGALLOP_TWO_ISLAND] = { 33, 4, 8, 5 },
[SEAGALLOP_THREE_ISLAND] = { 38, 0, 8, 5 },
[SEAGALLOP_FOUR_ISLAND] = { 35, 5, 8, 5 },
[SEAGALLOP_FIVE_ISLAND] = { 36, 2, 8, 5 },
[SEAGALLOP_SIX_ISLAND] = { 37, 2, 8, 5 },
[SEAGALLOP_SEVEN_ISLAND] = { 31, 6, 8, 5 },
[SEAGALLOP_CINNABAR_ISLAND] = { 3, 8, 0x15, 0x07 },
[SEAGALLOP_NAVEL_ROCK] = { 2, 59, 8, 5 },
[SEAGALLOP_BIRTH_ISLAND] = { 2, 58, 8, 5 },
}
for id, want in pairs(WANT) do
local got = Seagallop.WARPS[id]
check(got and got[1] == want[1] and got[2] == want[2] and got[3] == want[3] and got[4] == want[4],
string.format("seagallop %d -> %d.%d (%d,%d)", id, want[1], want[2], want[3], want[4]))
end
print("[test] 6. an out-of-range destination falls back to Vermilion")
do
local warps = {}
local a = logAdapters({
warp = function(g, n, w, x, y, done)
warps[#warps + 1] = { g, n, w, x, y }
if done then done() end
end,
})
local vm = newVm({ sail = setSailScript(SEAGALLOP_ONE_ISLAND, 40) }, a)
vm:start("sail")
for _ = 1, 400 do
if #warps > 0 then break end
vm:tick()
end
check(#warps == 1 and warps[1][1] == 3 and warps[1][2] == 5,
"dest 40 warped to Vermilion City 3.5")
for _ = 1, 5 do vm:tick() end
check(Flags.getVar(vm.store, vm.ctx, 0x4000) == SEAGALLOP_VERMILION_CITY,
"VAR_0x8006 was clamped to 0 (got " ..
tostring(Flags.getVar(vm.store, vm.ctx, 0x4000)) .. ")")
end
print("[test] 7. GetSeagallopNumber matches the pret cascade")
check(type(Seagallop.seagallopNumber) == "function", "the seagallop module is present")
if type(Seagallop.seagallopNumber) ~= "function" then
print("[test] " .. (failed + 1) .. " failed")
os.exit(1)
end
local NUM = {
{ SEAGALLOP_VERMILION_CITY, SEAGALLOP_ONE_ISLAND, 7 },
{ SEAGALLOP_CINNABAR_ISLAND, SEAGALLOP_ONE_ISLAND, 1 },
{ SEAGALLOP_ONE_ISLAND, SEAGALLOP_CINNABAR_ISLAND, 1 },
{ SEAGALLOP_VERMILION_CITY, SEAGALLOP_NAVEL_ROCK, 7 },
{ SEAGALLOP_ONE_ISLAND, SEAGALLOP_NAVEL_ROCK, 10 },
{ SEAGALLOP_ONE_ISLAND, SEAGALLOP_BIRTH_ISLAND, 12 },
{ SEAGALLOP_ONE_ISLAND, SEAGALLOP_THREE_ISLAND, 2 },
{ SEAGALLOP_FOUR_ISLAND, SEAGALLOP_FIVE_ISLAND, 3 },
{ SEAGALLOP_SIX_ISLAND, SEAGALLOP_SEVEN_ISLAND, 5 },
{ SEAGALLOP_THREE_ISLAND, SEAGALLOP_FOUR_ISLAND, 6 },
}
for _, row in ipairs(NUM) do
local got = Seagallop.seagallopNumber(row[1], row[2])
check(got == row[3], string.format("seagallop number %d -> %d is %d (got %s)",
row[1], row[2], row[3], tostring(got)))
end
print("[test] 8. the destination menu drops the port you are standing in")
if not require("tests.game3_cache").mount() then
print("[skip] sSeagallopDestStrings come from the ROM: " .. tostring(require("tests.game3_cache").reason))
else
local labels = Seagallop.destinationMenu(SEAGALLOP_ONE_ISLAND, 0)
check(#labels == 6, "page 0 has six rows (got " .. #labels .. ")")
check(labels[1] == "VERMILION" and labels[2] == "TWO ISLAND"
and labels[3] == "THREE ISLAND" and labels[4] == "FOUR ISLAND",
"ONE ISLAND is skipped: " .. table.concat(labels, "/"))
check(labels[5] == "OTHER" and labels[6] == "EXIT", "OTHER and EXIT close the page")
local p1 = Seagallop.destinationMenu(SEAGALLOP_ONE_ISLAND, 1)
check(#p1 == 5 and p1[1] == "FIVE ISLAND" and p1[2] == "SIX ISLAND"
and p1[3] == "SEVEN ISLAND", "page 1 from One Island: " .. table.concat(p1, "/"))
local p1b = Seagallop.destinationMenu(SEAGALLOP_SIX_ISLAND, 1)
check(p1b[1] == "FOUR ISLAND" and p1b[2] == "FIVE ISLAND" and p1b[3] == "SEVEN ISLAND",
"page 1 from Six Island: " .. table.concat(p1b, "/"))
end
print("[test] 9. GetSelectedSeagallopDestination maps the row back to a port")
do
local sel = Seagallop.selectedDestination
check(sel(SEAGALLOP_ONE_ISLAND, 0, 0) == SEAGALLOP_VERMILION_CITY, "page 0 row 0 = Vermilion")
check(sel(SEAGALLOP_ONE_ISLAND, 0, 1) == SEAGALLOP_TWO_ISLAND, "page 0 row 1 = Two Island")
check(sel(SEAGALLOP_ONE_ISLAND, 0, 4) == SEAGALLOP_MORE, "page 0 row 4 = OTHER")
check(sel(SEAGALLOP_ONE_ISLAND, 0, 5) == SCR_MENU_CANCEL, "page 0 row 5 = EXIT")
check(sel(SEAGALLOP_ONE_ISLAND, 0, SCR_MENU_CANCEL) == SCR_MENU_CANCEL, "B press cancels")
check(sel(SEAGALLOP_ONE_ISLAND, 1, 0) == SEAGALLOP_FIVE_ISLAND, "page 1 row 0 from One Island")
check(sel(SEAGALLOP_SIX_ISLAND, 1, 0) == SEAGALLOP_FOUR_ISLAND, "page 1 row 0 from Six Island")
check(sel(SEAGALLOP_SIX_ISLAND, 1, 2) == SEAGALLOP_SEVEN_ISLAND, "page 1 row 2 from Six Island")
check(sel(SEAGALLOP_ONE_ISLAND, 1, 3) == SEAGALLOP_MORE, "page 1 row 3 = OTHER")
check(sel(SEAGALLOP_ONE_ISLAND, 1, 4) == SCR_MENU_CANCEL, "page 1 row 4 = EXIT")
end
print("[test] 10. the menu special drives the shared multichoice and sets VAR_0x8006")
if not require("tests.game3_cache").mount() then
print("[skip] the ferry menu labels come from the ROM: " .. tostring(require("tests.game3_cache").reason))
else
local shown
local a = logAdapters({
multichoice = function(row, cb)
local Multi = require("src.core.game3.scripting.multichoice")
local entry = Multi.LISTS[row[3]]
shown = entry and entry.labels
cb(1)
end,
})
local vm = newVm({
page1 = {
{ op = "setvar", var = 0x8004, value = SEAGALLOP_ONE_ISLAND },
{ op = "setvar", var = 0x8005, value = 0 },
{ op = "special", id = Std.SPECIAL.DrawSeagallopDestinationMenu },
{ op = "waitstate" },
{ op = "specialvar", [1] = 0x8006, [2] = Std.SPECIAL.GetSelectedSeagallopDestination },
{ op = "copyvar", [1] = 0x4000, [2] = 0x8006 },
{ op = "end" },
},
}, a)
vm:start("page1")
for _ = 1, 10 do vm:tick() end
check(shown ~= nil and #shown == 6, "the ferry menu reached the multichoice adapter")
check(Flags.getVar(vm.store, vm.ctx, 0x4000) == SEAGALLOP_TWO_ISLAND,
"picking row 1 from One Island sails to Two Island (got " ..
tostring(Flags.getVar(vm.store, vm.ctx, 0x4000)) .. ")")
check(not vm:isRunning(), "the menu script finished")
end
print("[test] 11. waitstate with nothing pending is still a no-op")
do
local a = logAdapters({})
local vm = newVm({
plain = {
{ op = "waitstate" },
{ op = "setflag", [1] = 0x300 },
{ op = "end" },
},
}, a)
vm:start("plain")
check(Flags.getFlag(vm.store, nil, 0x300) == true, "a bare waitstate fell straight through")
check(not vm:isRunning(), "and the script ended in the same frame")
end
if failed == 0 then
print("[test] all passed")
os.exit(0)
end
print("[test] " .. failed .. " failed")
os.exit(1)