Files
gen1recomp/tests/game3_stitchscript_seams_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

278 lines
9.6 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").stubSpeciesNames()
local ROM_TEXT = { gText_PkmnsNickname = "'s nickname?" }
local function romTextKey(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end
local function romTextPlain(key, ctx) return ((ROM_TEXT[key] or key):gsub("{PLAYER}", ctx and ctx.playerName or "")) end
package.loaded["src.core.game3.rom_text"] = {
plain = romTextPlain, box = romTextPlain, ascii = romTextPlain, has = function() return true end,
key = romTextKey, at = function(n, i, j, ctx) return romTextPlain(romTextKey(n, i, j), ctx) end,
count = function() return 0 end, list = function() return {} end,
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
}
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local Flags = require("src.core.game3.scripting.flags")
local Vm = require("src.core.game3.scripting.vm")
local Adapters = require("src.core.game3.scripting.adapters")
local Runtime = require("src.core.game3.runtime")
local Storage = require("src.core.game3.storage")
local Natives = require("src.core.game3.scripting.natives")
local FieldView = require("src.core.game3.field_view")
local FieldEffects = require("src.core.game3.field_effects")
local Braille = require("src.ui.game3.braille")
local PartyMenu = require("src.ui.game3.party_menu")
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 VAR_TEMP_1 = 0x4001
local VAR_0x8004 = 0x8004
local VAR_0x8005 = 0x8005
local VAR_0x8006 = 0x8006
local SPECIAL_CHANGE_BOX_POKEMON_NICKNAME = 0x166
local SPECIAL_BRAILLE_CURSOR_TOGGLE = 0x1B2
local SPECIES_MAGIKARP = 129
local SLOT_CANCEL = 7
local prevSession = Runtime.session
local function newSession()
local session = {
party = {},
store = Flags.newStore(),
dex = { seen = {}, owned = {} },
name = "RED",
trainerId = 1,
}
Storage.ensure(session)
Runtime.session = session
return session
end
local function run(rows, adapters)
local store = Flags.newStore()
local vm = Vm.new({
store = store,
scripts = { t = rows },
adapters = adapters or Adapters.host(nil, nil, nil),
})
vm:start("t")
return vm, store
end
print("[test] 1. setflashlevel writes the level pret keeps in SaveBlock1")
FieldView.setFlashLevel(0)
run({
{ op = "setflashlevel", [1] = 3 },
{ op = "end" },
})
eq(FieldView.getFlashLevel(), 3, "a literal argument lands in the flash level")
run({
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 1 },
{ op = "setflashlevel", [1] = VAR_TEMP_1 },
{ op = "end" },
})
eq(FieldView.getFlashLevel(), 1, "the halfword argument goes through VarGet")
run({
{ op = "setflashlevel", [1] = 9 },
{ op = "end" },
})
eq(FieldView.getFlashLevel(), 0, "out of range clamps to 0")
print("[test] 2. animateflash blocks the script until the radius animation ends")
FieldEffects._anims = {}
FieldView.setFlashLevel(4)
local vm2 = run({
{ op = "animateflash", [1] = 0 },
{ op = "setflashlevel", [1] = 0 },
{ op = "end" },
})
local function poll2()
return type(vm2.ctx.nativePoll) == "function" and vm2.ctx.nativePoll() or false
end
check(vm2.ctx.mode == "native" and type(vm2.ctx.nativePoll) == "function",
"the script is parked in a native wait")
eq(poll2(), false, "and does not finish on the first poll")
eq(#FieldEffects._anims, 1, "one flash_level animation is running")
eq(FieldEffects._anims[1] and FieldEffects._anims[1].kind, "flash_level",
"it is the flash_level animation")
check(FieldView.flashRadius() ~= nil, "the radius is being driven while it runs")
local ticks = 0
while ticks < 4000 and #FieldEffects._anims > 0 and not poll2() do
FieldEffects.step()
ticks = ticks + 1
end
check(poll2(), "the native wait ends with the animation, ticks " .. tostring(ticks))
check(ticks > 1, "it really animated instead of finishing instantly")
eq(FieldView.getFlashLevel(), 0, "and the cave is fully lit at the end")
eq(#FieldEffects._anims, 0, "no animation is left over")
FieldEffects._anims = {}
FieldView.setFlashLevel(2)
local vm2b = run({
{ op = "animateflash", [1] = 2 },
{ op = "end" },
})
check(vm2b.ctx.mode ~= "native", "animating to the level already shown does not park the script")
eq(FieldView.getFlashLevel(), 2, "the level is unchanged")
FieldView.setFlashLevel(0)
print("[test] 3. BrailleCursorToggle creates and destroys the blinking cursor")
Braille.clearCursor()
run({
{ op = "setvar", [1] = VAR_0x8006, [2] = 0 },
{ op = "setvar", [1] = VAR_0x8004, [2] = 52 },
{ op = "setvar", [1] = VAR_0x8005, [2] = 130 },
{ op = "special", [1] = SPECIAL_BRAILLE_CURSOR_TOGGLE, id = SPECIAL_BRAILLE_CURSOR_TOGGLE },
{ op = "end" },
})
local cur = Braille.cursor()
check(type(cur) == "table", "VAR_0x8006 = 0 created the cursor")
eq(cur and cur.x, 52 + 27, "x is the braille string width plus 27")
eq(cur and cur.y, 130, "y is VAR_0x8005 straight from the map script")
run({
{ op = "setvar", [1] = VAR_0x8006, [2] = 1 },
{ op = "special", [1] = SPECIAL_BRAILLE_CURSOR_TOGGLE, id = SPECIAL_BRAILLE_CURSOR_TOGGLE },
{ op = "end" },
})
eq(Braille.cursor(), nil, "VAR_0x8006 = 1 destroys it again")
print("[test] 4. ChangeBoxPokemonNickname renames the mon at monBoxId / monBoxPos")
local s4 = newSession()
local giveMagikarp = { { op = "givemon", [1] = SPECIES_MAGIKARP, [2] = 5 }, { op = "end" } }
for _ = 1, 7 do run(giveMagikarp) end
eq(#s4.party, 6, "the party filled up")
local boxed = s4.storage.boxes[1].mons[1]
check(boxed ~= nil, "the seventh gift is in box 1 slot 1")
eq(s4.monBoxId, 0, "session.monBoxId points at box 1")
eq(s4.monBoxPos, 0, "session.monBoxPos points at slot 1")
local opened, titleSeen, beforeVar3 = false, nil, nil
local naming = {
log = function() end,
setStringVar = function(idx, text)
if idx == 3 then beforeVar3 = text end
end,
openNaming = function(opts, done)
opened = true
titleSeen = opts.title
eq(opts.template, "NICKNAME", "the keyboard opens on the NICKNAME template")
eq(opts.maxLen, 10, "with POKEMON_NAME_LENGTH")
eq(opts.species, SPECIES_MAGIKARP, "for the boxed MAGIKARP")
done("SPLASHY")
end,
}
local vm4 = run({
{ op = "special", [1] = SPECIAL_CHANGE_BOX_POKEMON_NICKNAME,
id = SPECIAL_CHANGE_BOX_POKEMON_NICKNAME },
{ op = "waitstate" },
{ op = "end" },
}, naming)
check(opened, "the special reached the naming keyboard")
check(type(titleSeen) == "string" and titleSeen ~= "", "it carried a title")
check(type(beforeVar3) == "string", "STR_VAR_3 got the old name, " .. tostring(beforeVar3))
local renamed = s4.storage.boxes[1].mons[1]
eq(renamed and renamed.nickname, "SPLASHY", "the box mon was renamed")
eq(#s4.party, 6, "no party mon was touched")
check(s4.party[1] and (s4.party[1].nickname == nil or s4.party[1].nickname == ""),
"slot 1 of the party kept its name")
check(not vm4:isRunning(), "the script ran past waitstate once naming was done")
print("[test] 5. it is a no-op when the box slot is empty")
local s5 = newSession()
s5.monBoxId, s5.monBoxPos = 0, 0
local reopened = false
run({
{ op = "special", [1] = SPECIAL_CHANGE_BOX_POKEMON_NICKNAME,
id = SPECIAL_CHANGE_BOX_POKEMON_NICKNAME },
{ op = "end" },
}, { log = function() end, openNaming = function() reopened = true end })
check(not reopened, "no keyboard for an empty slot")
print("[test] 6. the party picker fallback writes VAR_0x8004 exactly once")
local s6 = newSession()
s6.party = {
{ species = 1, level = 5, hp = 10, maxHp = 10 },
{ species = 4, level = 5, hp = 10, maxHp = 10 },
}
local function pickerCtx()
local ctx = { specialVars = {}, writes = {} }
function ctx:setVar(id, value)
if id == VAR_0x8004 then self.writes[#self.writes + 1] = value end
end
return ctx
end
local function pollOf(ctx)
return type(ctx.nativePoll) == "function" and ctx.nativePoll() or false
end
local ctx6 = pickerCtx()
local yielded = Natives.choosePartyMon(ctx6, { log = function() end }, "choose_single")
check(yielded == true, "the script yields while the picker is up")
check(PartyMenu.open == true, "the real party menu opened")
eq(#ctx6.writes, 0, "nothing is written before the player picks")
eq(pollOf(ctx6), false, "the native wait is still pending")
PartyMenu.close()
if PartyMenu._onSelect then PartyMenu._onSelect(2) end
check(pollOf(ctx6), "the wait ends once the menu is gone")
eq(#ctx6.writes, 1, "one write, not a cancel followed by the slot")
eq(Flags.getVar(nil, ctx6, VAR_0x8004), 1, "slot 2 reports as the 0-based 1")
local ctx6b = pickerCtx()
Natives.choosePartyMon(ctx6b, { log = function() end }, "choose_single")
PartyMenu.close()
check(pollOf(ctx6b), "closing with no pick also ends the wait")
eq(#ctx6b.writes, 1, "cancel writes once too")
eq(Flags.getVar(nil, ctx6b, VAR_0x8004), SLOT_CANCEL, "and it is SLOT_CANCEL")
print("[test] 7. waitbuttonpress arms on a later tick, as SetupNativeScript does")
local arms, release = 0, nil
local vm7 = run({
{ op = "waitbuttonpress" },
{ op = "end" },
}, {
log = function() end,
armWaitButton = function(cb)
arms = arms + 1
release = cb
end,
})
eq(arms, 0, "the tick that dispatched the op arms nothing")
check(vm7:isRunning(), "and the script is waiting")
vm7:tick()
eq(arms, 1, "the next tick arms it")
vm7:tick()
eq(arms, 1, "later ticks do not re-arm")
release()
vm7:tick()
check(not vm7:isRunning(), "the press ends the wait")
Runtime.session = prevSession
if failed > 0 then
print(string.format("FAILED %d check(s)", failed))
os.exit(1)
end
print("PASS game3_stitchscript_seams")
os.exit(0)