mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
278 lines
9.6 KiB
Lua
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)
|