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