Files
gen1recomp/tests/game3_ops_vars_test.lua

378 lines
13 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local Flags = require("src.core.game3.scripting.flags")
local Ctx = require("src.core.game3.scripting.ctx")
local Vm = require("src.core.game3.scripting.vm")
local Adapters = require("src.core.game3.scripting.adapters")
local Player = require("src.core.game3.player")
local Runtime = require("src.core.game3.runtime")
local Storage = require("src.core.game3.storage")
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_TEMP_2 = 0x4002
local VAR_RESULT = Ctx.VAR_RESULT
local VAR_0x8004 = 0x8004
local function run(scripts, key, store)
store = store or Flags.newStore()
local vm = Vm.new({ store = store, scripts = scripts, adapters = Adapters.host(nil, nil, nil) })
vm:start(key)
return vm, store
end
print("[test] 1. addvar / subvar arithmetic")
local vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "addvar", [1] = VAR_TEMP_1, [2] = 1 },
{ op = "addvar", [1] = VAR_TEMP_1, [2] = 1 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 2, "addvar twice by 1 reaches 2")
vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 10 },
{ op = "subvar", [1] = VAR_TEMP_1, [2] = 3 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 7, "subvar by a literal")
print("[test] 2. subvar VarGets its second operand, addvar does not")
-- pokefirered/src/scrcmd.c:448
vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_2, [2] = 3 },
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 10 },
{ op = "subvar", [1] = VAR_TEMP_1, [2] = VAR_TEMP_2 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 7, "subvar VAR_TEMP_1, VAR_TEMP_2 subtracts VAR_TEMP_2's value")
print("[test] E9: incrementgamestat / checkpartymove implement pret")
local prevSess = Runtime.session
Runtime.session = { gameStats = {}, party = {} }
-- pokefirered/src/scrcmd.c:576-579, overworld.c:366-375
vm, store = run({
t = {
{ op = "incrementgamestat", [1] = 13 },
{ op = "incrementgamestat", [1] = 99 },
{ op = "end" },
},
}, "t")
eq(Runtime.session.gameStats[13], 1, "incrementgamestat bumps the stat (scrcmd.c:576)")
eq(Runtime.session.gameStats[99], nil, "an out-of-range statId is ignored (overworld.c:369)")
Runtime.session.gameStats[13] = 0xFFFFFF
vm, store = run({
t = {
{ op = "incrementgamestat", [1] = 13 },
{ op = "end" },
},
}, "t")
eq(Runtime.session.gameStats[13], 0xFFFFFF, "the stat saturates at 0xFFFFFF (overworld.c:371-374)")
-- pokefirered/src/scrcmd.c:1777-1795
Runtime.session.party = {
{ species = 1, moves = { 0, 0, 0, 0 } },
{ species = 4, moves = { { id = 15 } }, isEgg = true },
{ species = 7, moves = { { id = 15 } } },
}
vm, store = run({
t = {
{ op = "checkpartymove", [1] = 15 },
{ op = "copyvar", [1] = VAR_TEMP_1, [2] = VAR_RESULT },
{ op = "copyvar", [1] = VAR_TEMP_2, [2] = VAR_0x8004 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 2, "checkpartymove skips the egg, Result is the 0-based slot")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_2), 7, "VAR_0x8004 carries that mon's species")
vm, store = run({
t = {
{ op = "checkpartymove", [1] = 99 },
{ op = "copyvar", [1] = VAR_TEMP_1, [2] = VAR_RESULT },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 6, "no match leaves Result = PARTY_SIZE (scrcmd.c:1781)")
Runtime.session = prevSess
vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_2, [2] = 5 },
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "addvar", [1] = VAR_TEMP_1, [2] = VAR_TEMP_2 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 0x4002, "addvar adds the raw halfword, not VarGet of it")
print("[test] 3. u16 wrap")
vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "subvar", [1] = VAR_TEMP_1, [2] = 1 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 65535, "0 - 1 underflows to 65535")
vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 65535 },
{ op = "addvar", [1] = VAR_TEMP_1, [2] = 2 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 1, "65535 + 2 wraps to 1")
print("[test] 4. Rocket Hideout B4F barrier gate")
-- pokefirered/data/maps/RocketHideout_B4F/scripts.inc:7
local hideout = {
OnLoad = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "call", [1] = "CountGruntDefeated" },
{ op = "call", [1] = "CountGruntDefeated" },
{ op = "compare_var_to_value", [1] = VAR_TEMP_1, [2] = 2 },
{ op = "call_if", [1] = 5, [2] = "SetBarrier" },
{ op = "end" },
},
Defeated = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "call", [1] = "CountGruntDefeated" },
{ op = "call", [1] = "CountGruntDefeated" },
{ op = "compare_var_to_value", [1] = VAR_TEMP_1, [2] = 2 },
{ op = "call_if", [1] = 1, [2] = "RemoveBarrier" },
{ op = "end" },
},
CountGruntDefeated = {
{ op = "addvar", [1] = VAR_TEMP_1, [2] = 1 },
{ op = "return" },
},
SetBarrier = { { op = "setflag", [1] = 0x300 }, { op = "return" } },
RemoveBarrier = { { op = "clearflag", [1] = 0x300 }, { op = "return" } },
}
store = Flags.newStore()
vm = Vm.new({ store = store, scripts = hideout, adapters = Adapters.host(nil, nil, nil) })
vm:start("OnLoad")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 2, "OnLoad counts both grunts")
check(Flags.getFlag(store, vm.ctx, 0x300) == false, "barrier not re-set once both grunts are down")
vm:start("Defeated")
check(Flags.getFlag(store, vm.ctx, 0x300) == false, "RemoveBarrier branch taken at 2")
local single = {}
for k, v in pairs(hideout) do single[k] = v end
single.OnLoad = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "call", [1] = "CountGruntDefeated" },
{ op = "compare_var_to_value", [1] = VAR_TEMP_1, [2] = 2 },
{ op = "call_if", [1] = 5, [2] = "SetBarrier" },
{ op = "end" },
}
store = Flags.newStore()
vm = Vm.new({ store = store, scripts = single, adapters = Adapters.host(nil, nil, nil) })
vm:start("OnLoad")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 1, "one grunt counts 1")
check(Flags.getFlag(store, vm.ctx, 0x300) == true, "barrier stays set with one grunt down")
print("[test] 5. getpartysize counts eggs")
-- pokefirered/src/pokemon.c:3742
local prevSession = Runtime.session
Runtime.session = {
gender = 0,
party = {
{ species = 1, level = 5, hp = 20 },
{ species = 4, level = 5, hp = 0 },
{ species = 172, level = 5, hp = 11, isEgg = true },
},
}
local PARTY_SIZE = {
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 99 },
{ op = "getpartysize" },
{ op = "copyvar", [1] = VAR_TEMP_1, [2] = VAR_RESULT },
{ op = "end" },
},
}
vm, store = run(PARTY_SIZE, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 3, "party of 2 mons + 1 egg reports 3")
Runtime.session.party = {}
vm, store = run(PARTY_SIZE, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 0, "empty party reports 0")
print("[test] 6. gift-mon party slot (pokefirered/data/scripts/pc_transfer.inc:1-5)")
Runtime.session.party = {
{ species = 1, level = 5, hp = 20 },
{ species = 4, level = 5, hp = 20 },
{ species = 7, level = 5, hp = 20 },
}
vm, store = run({
t = {
{ op = "getpartysize" },
{ op = "subvar", [1] = VAR_RESULT, [2] = 1 },
{ op = "copyvar", [1] = VAR_0x8004, [2] = VAR_RESULT },
{ op = "copyvar", [1] = VAR_TEMP_1, [2] = VAR_0x8004 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 2, "last slot of a 3-mon party is index 2")
print("[test] 7. checkplayergender")
-- pokefirered/include/constants/global.h:85
local GENDER = {
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 99 },
{ op = "checkplayergender" },
{ op = "copyvar", [1] = VAR_TEMP_1, [2] = VAR_RESULT },
{ op = "end" },
},
}
Runtime.session.gender = 0
vm, store = run(GENDER, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 0, "male player reports MALE")
Runtime.session.gender = 1
vm, store = run(GENDER, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 1, "female player reports FEMALE")
-- pokefirered/data/maps/Route25_SeaCottage/scripts.inc:28
local bill = {
Bill = {
{ op = "checkplayergender" },
{ op = "compare_var_to_value", [1] = VAR_RESULT, [2] = 0 },
{ op = "goto_if", [1] = 1, [2] = "Male" },
{ op = "compare_var_to_value", [1] = VAR_RESULT, [2] = 1 },
{ op = "goto_if", [1] = 1, [2] = "Female" },
{ op = "end" },
},
Male = { { op = "setvar", [1] = VAR_TEMP_1, [2] = 11 }, { op = "end" } },
Female = { { op = "setvar", [1] = VAR_TEMP_1, [2] = 22 }, { op = "end" } },
}
for gender, want in pairs({ [0] = 11, [1] = 22 }) do
Runtime.session.gender = gender
store = Flags.newStore()
store.vars[VAR_TEMP_1] = 0
vm = Vm.new({ store = store, scripts = bill, adapters = Adapters.host(nil, nil, nil) })
vm.ctx.specialVars[VAR_RESULT] = 4
vm:start("Bill")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), want, "Bill reaches a branch with gender " .. gender)
end
print("[test] 8. getplayerxy reads map-local cell coords")
-- pokefirered/src/scrcmd.c:867
Player.reset(19, 4, "down")
vm, store = run({
t = {
{ op = "getplayerxy", [1] = VAR_0x8004, [2] = 0x8005 },
{ op = "copyvar", [1] = VAR_TEMP_1, [2] = VAR_0x8004 },
{ op = "copyvar", [1] = VAR_TEMP_2, [2] = 0x8005 },
{ op = "end" },
},
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 19, "getplayerxy X into VAR_0x8004")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_2), 4, "getplayerxy Y into VAR_0x8005")
Player.reset(7, 12, "up")
vm, store = run({
t = { { op = "getplayerxy", [1] = VAR_TEMP_1, [2] = VAR_TEMP_2 }, { op = "end" } },
}, "t")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 7, "getplayerxy into a temp var, X")
eq(Flags.getVar(store, vm.ctx, VAR_TEMP_2), 12, "getplayerxy into a temp var, Y")
print("[test] 9. bufferboxname converts the cart's 0-based box id")
-- pokefirered/src/scrcmd.c:1725
Storage.ensure(Runtime.session)
Runtime.session.storage.boxes[3].name = "GHOSTS"
vm, store = run({
t = {
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 0 },
{ op = "bufferboxname", [1] = 0, [2] = VAR_TEMP_1 },
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 2 },
{ op = "bufferboxname", [1] = 1, [2] = VAR_TEMP_1 },
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 13 },
{ op = "bufferboxname", [1] = 2, [2] = VAR_TEMP_1 },
{ op = "end" },
},
}, "t")
eq(vm.ctx.stringVars[1], "BOX 1", "box id 0 buffers BOX 1 into STR_VAR_1")
eq(vm.ctx.stringVars[2], "GHOSTS", "box id 2 buffers the renamed third box into STR_VAR_2")
eq(vm.ctx.stringVars[3], "BOX 14", "box id 13 buffers BOX 14 into STR_VAR_3")
Runtime.session = prevSession
print("[test] 10. warp x/y VarGet returns non-var ids literally")
local function warpArgs(x, y, seed)
local st = Flags.newStore()
for id, v in pairs(seed or {}) do Flags.setVar(st, nil, id, v) end
local got
local adapters = Adapters.host(nil, nil, nil)
adapters.warp = function(g, n, w, wx, wy, cb) got = { g, n, w, wx, wy }; if cb then cb() end end
local v = Vm.new({ store = st, scripts = {
t = { { op = "warp", [1] = 3, [2] = 5, [3] = 1, [4] = x, [5] = y }, { op = "end" } },
}, adapters = adapters })
v:start("t")
return got or {}
end
local w = warpArgs(0xFFFF, 0xFFFF)
eq(w[4], 0xFFFF, "id-only warp keeps x = 0xFFFF")
eq(w[5], 0xFFFF, "id-only warp keeps y = 0xFFFF")
w = warpArgs(7, 9)
eq(w[4], 7, "a literal x below VARS_START passes through")
eq(w[5], 9, "a literal y below VARS_START passes through")
w = warpArgs(VAR_TEMP_1, 0x40FF, { [VAR_TEMP_1] = 12, [0x40FF] = 4 })
eq(w[4], 12, "x in the save var range is read")
eq(w[5], 4, "VARS_END is still a var")
w = warpArgs(0x4100, 0x8015)
eq(w[4], 0x4100, "an id past VARS_END is a literal")
eq(w[5], 0x8015, "an id past SPECIAL_VARS_END is a literal")
print("[test] 11. buffernumberstring VarGets a literal requirement")
-- pokefirered/data/maps/Route10_PokemonCenter_1F/scripts.inc:59
-- pokefirered/src/event_data.c:235-241
store = Flags.newStore()
eq(Flags.getVar(store, nil, 20), 20, "getVar(20) returns the literal 20")
Flags.setVar(store, nil, 0x4050, 3)
eq(Flags.getVar(store, nil, 0x4050), 3, "a var at 0x4050 still reads the store")
vm, store = run({
t = {
{ op = "setvar", [1] = 0x8006, [2] = 7 },
{ op = "setvar", [1] = VAR_TEMP_1, [2] = 42 },
{ op = "buffernumberstring", [1] = 0, [2] = 20 },
{ op = "buffernumberstring", [1] = 1, [2] = VAR_TEMP_1 },
{ op = "buffernumberstring", [1] = 2, [2] = 0x8006 },
{ op = "end" },
},
}, "t")
eq(vm.ctx.stringVars[1], "20", "buffernumberstring STR_VAR_1, 20 buffers \"20\"")
eq(vm.ctx.stringVars[2], "42", "buffernumberstring of a save var buffers its value")
eq(vm.ctx.stringVars[3], "7", "buffernumberstring of VAR_0x8006 buffers the caught count")
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")