mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
281 lines
13 KiB
Lua
281 lines
13 KiB
Lua
-- Script verbs that had no handler (E10 significant subset).
|
|
--
|
|
-- Implements the conditional std calls, the door-state verbs, the PC-item
|
|
-- verbs, comparestat, bufferitemnameplural, the four mon verbs and the (same
|
|
-- map) *at verbs. The v* RAM-script family and the compare_* locals family
|
|
-- remain unimplemented and are recorded in the review's remaining-scope list.
|
|
-- luajit tests/engine/game3_script_verbs_subset_test.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
require("tests.game3_cache").mountOrSkip("game3_script_verbs_subset_test")
|
|
|
|
local Vm = require("src.core.game3.scripting.vm")
|
|
local Ops = require("src.core.game3.scripting.ops_a")
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
local Ctx = require("src.core.game3.scripting.ctx")
|
|
local Storage = require("src.core.game3.storage")
|
|
local Bag = require("src.core.game3.bag")
|
|
local Schema = require("src.core.game3.save_schema_firered")
|
|
local Runtime = require("src.core.game3.runtime")
|
|
|
|
local VAR_RESULT = (Ctx and Ctx.VAR_RESULT) or 0x800D
|
|
|
|
local session = Schema.newGame({ name = "RED" })
|
|
session.bag = Bag.new()
|
|
Storage.ensure(session)
|
|
session.storage.items = {}
|
|
Runtime.session = session
|
|
Runtime._game = { data = { maps = {} } }
|
|
|
|
local END = { { op = "end" } }
|
|
local function new_vm()
|
|
local vm = Vm.new({
|
|
store = Flags.newStore(),
|
|
scripts = { t_main = END, ["std:1"] = END, ["std:2"] = END },
|
|
})
|
|
vm.ctx.stack = vm.ctx.stack or {}
|
|
return vm
|
|
end
|
|
|
|
-- 1. callstd_if with a true condition calls the std script.
|
|
local vm = new_vm()
|
|
vm.ctx.comparisonResult = 1 -- EQ
|
|
vm:setPc("t_main", 1)
|
|
Ops.dispatch(vm, { op = "callstd_if", [1] = 1, [2] = 1 })
|
|
eq(#vm.ctx.stack, 1, "callstd_if (condition true) pushes a return address")
|
|
eq(vm.ctx.pc.listKey, "std:1", "...and enters the std script")
|
|
|
|
-- 2. callstd_if with a false condition falls through.
|
|
vm = new_vm()
|
|
vm.ctx.comparisonResult = 0 -- LT
|
|
vm:setPc("t_main", 1)
|
|
Ops.dispatch(vm, { op = "callstd_if", [1] = 1, [2] = 1 })
|
|
eq(#vm.ctx.stack, 0, "callstd_if (condition false) does not push")
|
|
eq(vm.ctx.pc.listKey, "t_main", "...and stays on the current list")
|
|
|
|
-- 3. gotostd_if jumps without a return address, and only when the condition holds.
|
|
vm = new_vm()
|
|
vm.ctx.comparisonResult = 1
|
|
vm:setPc("t_main", 1)
|
|
Ops.dispatch(vm, { op = "gotostd_if", [1] = 1, [2] = 2 })
|
|
eq(vm.ctx.pc.listKey, "std:2", "gotostd_if (condition true) enters the std script")
|
|
eq(#vm.ctx.stack, 0, "...without pushing a return address")
|
|
vm = new_vm()
|
|
vm.ctx.comparisonResult = 0
|
|
vm:setPc("t_main", 1)
|
|
Ops.dispatch(vm, { op = "gotostd_if", [1] = 1, [2] = 2 })
|
|
eq(vm.ctx.pc.listKey, "t_main", "gotostd_if (condition false) falls through")
|
|
|
|
-- 4. The door-state verbs reach the host door seam.
|
|
local seen = {}
|
|
vm = new_vm()
|
|
-- tolerate (op,x,y) and (adapter,op,x,y)
|
|
vm.adapters.doorAnim = function(a1, a2, a3, a4)
|
|
local op, x, y = a1, a2, a3
|
|
if a3 == nil and type(a1) == "table" then op, x, y = a2, a3, a4 end
|
|
seen[#seen + 1] = { op, x, y }
|
|
end
|
|
Ops.dispatch(vm, { op = "setdooropen", [1] = 3, [2] = 7 })
|
|
Ops.dispatch(vm, { op = "setdoorclosed", [1] = 4, [2] = 8 })
|
|
eq(#seen, 2, "both door-state verbs reach the door seam")
|
|
local first, second = seen[1] or {}, seen[2] or {}
|
|
eq(first[1], "opendoor", "setdooropen maps to the open animation")
|
|
eq(first[2], 3, "...carrying its x")
|
|
eq(second[1], "closedoor", "setdoorclosed maps to the close animation")
|
|
|
|
-- 5. checkpcitem reports whether the PC holds enough.
|
|
vm = new_vm()
|
|
session.storage.items = { { id = 13, qty = 5 } }
|
|
Ops.dispatch(vm, { op = "checkpcitem", [1] = 13, [2] = 3 })
|
|
eq(Flags.getVar(vm.store, vm.ctx, VAR_RESULT), 1, "checkpcitem reports enough stored")
|
|
Ops.dispatch(vm, { op = "checkpcitem", [1] = 13, [2] = 9 })
|
|
eq(Flags.getVar(vm.store, vm.ctx, VAR_RESULT), 0, "checkpcitem reports too few stored")
|
|
|
|
-- 6. addpcitem stores into the PC and reports success.
|
|
vm = new_vm()
|
|
session.storage.items = {}
|
|
Ops.dispatch(vm, { op = "addpcitem", [1] = 13, [2] = 4 })
|
|
eq(session.storage.items[1] and session.storage.items[1].qty, 4, "addpcitem stores the quantity")
|
|
eq(Flags.getVar(vm.store, vm.ctx, VAR_RESULT), 1, "addpcitem reports success")
|
|
Ops.dispatch(vm, { op = "addpcitem", [1] = 13, [2] = 2 })
|
|
eq((session.storage.items[1] or {}).qty, 6, "...and stacks onto an existing entry")
|
|
|
|
-- 7. addpcitem respects the PC stack cap instead of destroying overflow.
|
|
vm = new_vm()
|
|
session.storage.items = { { id = 13, qty = Storage.MAX_ITEM_QTY } }
|
|
Ops.dispatch(vm, { op = "addpcitem", [1] = 13, [2] = 5 })
|
|
eq((session.storage.items[1] or {}).qty, Storage.MAX_ITEM_QTY, "a full stack is not overfilled")
|
|
eq(Flags.getVar(vm.store, vm.ctx, VAR_RESULT), 0, "...and the verb reports failure")
|
|
|
|
-- 8. comparestat (src/scrcmd.c:582) reads {statId byte, value word} and sets
|
|
-- ctx.comparisonResult to LT/EQ/GT from the serialized game stat table.
|
|
local Opcodes = require("src.core.game3.scripting.opcodes")
|
|
local cs = Opcodes.get(0xcc)
|
|
eq(cs.size, 7, "comparestat is a 7-byte instruction (0xcc + B + W)")
|
|
eq(cs.args[1].kind, "byte", "...statId is a byte")
|
|
eq(cs.args[2].kind, "word", "...value is a word")
|
|
session.gameStats = { [5] = 10 }
|
|
vm = new_vm()
|
|
Ops.dispatch(vm, { op = "comparestat", [1] = 5, [2] = 11 })
|
|
eq(vm.ctx.comparisonResult, 0, "comparestat reports LT below the stat")
|
|
Ops.dispatch(vm, { op = "comparestat", [1] = 5, [2] = 10 })
|
|
eq(vm.ctx.comparisonResult, 1, "comparestat reports EQ at the stat")
|
|
Ops.dispatch(vm, { op = "comparestat", [1] = 5, [2] = 9 })
|
|
eq(vm.ctx.comparisonResult, 2, "comparestat reports GT above the stat")
|
|
|
|
-- 9. bufferitemnameplural (src/scrcmd.c:1637) pluralises like the ROM: "S" after
|
|
-- a Poké Ball stack, and the final letter replaced by "IES" for a berry
|
|
-- stack. The names themselves come from the item pack, which does not exist
|
|
-- in a ROM-free checkout, so assert the rule against each item's own singular
|
|
-- name rather than a hard-coded one.
|
|
local ItemsData = require("src.core.game3.items_data")
|
|
local function plural(item, qty)
|
|
local vm = new_vm()
|
|
Ops.dispatch(vm, { op = "bufferitemnameplural", [1] = 0, [2] = item, [3] = qty })
|
|
return vm.ctx.stringVars[1]
|
|
end
|
|
local ballName = ItemsData.displayName(4)
|
|
eq(plural(4, 2), ballName .. "S", "a Poké Ball stack pluralises with S")
|
|
eq(plural(4, 1), ballName, "...and stays singular at one")
|
|
local berryName = ItemsData.displayName(133)
|
|
eq(ItemsData.isBerry(133), true, "the berry item is classified as a berry")
|
|
eq(plural(133, 2), berryName:sub(1, -2) .. "IES",
|
|
"a berry stack replaces the final letter with IES")
|
|
eq(plural(133, 1), berryName, "...and stays singular at one")
|
|
|
|
-- 10-12. The party-mon verbs (src/scrcmd.c:1767, :2239, :2248, :2256) use
|
|
-- 0-based party indices and move slots.
|
|
vm = new_vm()
|
|
session.party = { { species = 1, moves = {}, pp = {}, maxPp = {} } }
|
|
Ops.dispatch(vm, { op = "setmonmove", [1] = 0, [2] = 0, [3] = 33 })
|
|
eq(session.party[1].moves[1], 33, "setmonmove writes the 0-based slot")
|
|
eq(type(session.party[1].pp[1]), "number", "...and resets its PP")
|
|
Ops.dispatch(vm, { op = "setmonmetlocation", [1] = 0, [2] = 88 })
|
|
eq(session.party[1].metLocation, 88, "setmonmetlocation writes metLocation")
|
|
Ops.dispatch(vm, { op = "setmonmodernfatefulencounter", [1] = 0 })
|
|
eq(session.party[1].modernFatefulEncounter, true, "setmonmodernfatefulencounter flags the mon")
|
|
Ops.dispatch(vm, { op = "checkmonmodernfatefulencounter", [1] = 0 })
|
|
eq(Flags.getVar(vm.store, vm.ctx, VAR_RESULT), 1, "checkmonmodernfatefulencounter reports it")
|
|
Ops.dispatch(vm, { op = "checkmonmodernfatefulencounter", [1] = 1 })
|
|
eq(Flags.getVar(vm.store, vm.ctx, VAR_RESULT), 0, "...and 0 for a party slot with no mon")
|
|
|
|
-- 13. The door-state verbs read x/y through VarGet (src/scrcmd.c:2156).
|
|
vm = new_vm()
|
|
seen = {}
|
|
vm.adapters.doorAnim = function(a1, a2, a3, a4)
|
|
local op, x, y = a1, a2, a3
|
|
if a3 == nil and type(a1) == "table" then op, x, y = a2, a3, a4 end
|
|
seen[#seen + 1] = { op, x, y }
|
|
end
|
|
local V0x4001 = 0x4001
|
|
Flags.setVar(vm.store, vm.ctx, V0x4001, 6)
|
|
Ops.dispatch(vm, { op = "setdooropen", [1] = V0x4001, [2] = 9 })
|
|
eq((seen[1] or {})[2], 6, "setdooropen resolves variable coordinates")
|
|
eq((seen[1] or {})[3], 9, "...and passes literal ones through")
|
|
|
|
-- 14. The *at verbs (src/scrcmd.c:993-1080) address a specific map. On the
|
|
-- current map they behave as the plain command; elsewhere they skip.
|
|
local Map = require("src.core.game3.map")
|
|
vm = new_vm()
|
|
Map.current = "FR_PALLET_TOWN"
|
|
local added = {}
|
|
vm.adapters.addObject = function(lid) added[#added + 1] = lid end
|
|
Ops.dispatch(vm, { op = "addobjectat", [1] = 2, [2] = 3, [3] = 0 })
|
|
eq(#added, 1, "addobjectat on the current map adds the object")
|
|
eq(added[1], 2, "...with the resolved local id")
|
|
Ops.dispatch(vm, { op = "addobjectat", [1] = 2, [2] = 3, [3] = 1 })
|
|
eq(#added, 1, "addobjectat on another map is skipped")
|
|
Ops.dispatch(vm, { op = "applymovementat", [1] = 2, [2] = { 0xFE }, [3] = 3, [4] = 1 })
|
|
eq(vm.ctx.activeMoves[2], nil, "applymovementat on another map is skipped")
|
|
Ops.dispatch(vm, { op = "applymovementat", [1] = 2, [2] = { 0xFE }, [3] = 3, [4] = 0 })
|
|
eq(vm.ctx.activeMoves[2] ~= nil, true, "applymovementat on the current map starts the movement")
|
|
|
|
-- 15. The pointer family layouts (asm/macros/event.inc) each carry a leading
|
|
-- byte plus a word; the table used to declare them a byte short, which
|
|
-- desynced every following instruction.
|
|
for _, byte in ipairs({ 0x11, 0x12, 0x13 }) do
|
|
local d = Opcodes.get(byte)
|
|
eq(d.size, 6, string.format("0x%02x is a 6-byte instruction", byte))
|
|
eq(d.args[1].kind, "byte", string.format("0x%02x starts with a byte", byte))
|
|
eq(d.args[2].kind, "word", string.format("0x%02x ends with a word", byte))
|
|
end
|
|
|
|
-- 16. Script locals and the synthetic pointer store (src/scrcmd.c:293-375).
|
|
vm = new_vm()
|
|
Ops.dispatch(vm, { op = "loadbyte", 0, 10 })
|
|
Ops.dispatch(vm, { op = "loadbyte", 1, 20 })
|
|
Ops.dispatch(vm, { op = "copylocal", [1] = 0, [2] = 1 })
|
|
eq(vm.ctx.data[0], 20, "copylocal copies a local")
|
|
Ops.dispatch(vm, { op = "loadbyte", 0, 5 })
|
|
Ops.dispatch(vm, { op = "loadbyte", 1, 9 })
|
|
Ops.dispatch(vm, { op = "compare_local_to_local", [1] = 0, [2] = 1 })
|
|
eq(vm.ctx.comparisonResult, 0, "compare_local_to_local reports LT")
|
|
Ops.dispatch(vm, { op = "loadbyte", 0, 7 })
|
|
Ops.dispatch(vm, { op = "compare_local_to_value", [1] = 0, [2] = 7 })
|
|
eq(vm.ctx.comparisonResult, 1, "compare_local_to_value reports EQ")
|
|
Ops.dispatch(vm, { op = "setptr", [1] = 42, [2] = 0x1234 })
|
|
eq(vm.ctx.scriptMem[0x1234], 42, "setptr writes the synthetic byte store")
|
|
Ops.dispatch(vm, { op = "loadbytefromptr", [1] = 1, [2] = 0x1234 })
|
|
eq(vm.ctx.data[1], 42, "loadbytefromptr reads it back into a local")
|
|
Ops.dispatch(vm, { op = "loadbyte", 2, 9 })
|
|
Ops.dispatch(vm, { op = "setptrbyte", [1] = 2, [2] = 0x1235 })
|
|
eq(vm.ctx.scriptMem[0x1235], 9, "setptrbyte stores a local")
|
|
Ops.dispatch(vm, { op = "copybyte", [1] = 0x1236, [2] = 0x1235 })
|
|
eq(vm.ctx.scriptMem[0x1236], 9, "copybyte copies between pointers")
|
|
Ops.dispatch(vm, { op = "compare_local_to_ptr", [1] = 2, [2] = 0x1234 })
|
|
eq(vm.ctx.comparisonResult, 0, "compare_local_to_ptr compares local vs store")
|
|
Ops.dispatch(vm, { op = "compare_ptr_to_ptr", [1] = 0x1234, [2] = 0x1236 })
|
|
eq(vm.ctx.comparisonResult, 2, "compare_ptr_to_ptr compares two stored bytes")
|
|
|
|
Ops.dispatch(vm, { op = "loadword", 0, 0x12AB })
|
|
Ops.dispatch(vm, { op = "copylocal", 3, 0 })
|
|
eq(vm.ctx.data[3], 0x12AB, "copylocal preserves the full loadword register")
|
|
Ops.dispatch(vm, { op = "compare_local_to_value", 3, 0xAB })
|
|
eq(vm.ctx.comparisonResult, 1, "local comparisons read the low byte")
|
|
Ops.dispatch(vm, { op = "setptrbyte", 3, 0x1237 })
|
|
Ops.dispatch(vm, { op = "loadbytefromptr", 0, 0x1237 })
|
|
eq(vm.ctx.data[0], 0xAB, "pointer writes truncate a word register to a byte")
|
|
Ops.dispatch(vm, { op = "loadword", 0, "std:1" })
|
|
Ops.dispatch(vm, { op = "copylocal", 1, 0 })
|
|
eq(vm.ctx.data[1], "std:1", "copylocal preserves the host's symbolic text pointers")
|
|
|
|
-- 17. The RAM-script (v*) control flow (src/scrcmd.c:171-209, :1580).
|
|
vm = new_vm()
|
|
vm:setPc("t_main", 4)
|
|
Ops.dispatch(vm, { op = "setvaddress", [1] = 0x800000 })
|
|
eq(vm.ctx.vaddress, 0x800000, "setvaddress is recorded")
|
|
Ops.dispatch(vm, { op = "vgoto", [1] = "std:1" })
|
|
eq(vm.ctx.pc.listKey, "std:1", "vgoto jumps to the script")
|
|
vm:setPc("t_main", 4)
|
|
Ops.dispatch(vm, { op = "vgoto_if", [1] = 2, [2] = "std:1" })
|
|
eq(vm.ctx.pc.listKey, "t_main", "vgoto_if with a false condition falls through")
|
|
vm.ctx.comparisonResult = 2
|
|
Ops.dispatch(vm, { op = "vgoto_if", [1] = 2, [2] = "std:1" })
|
|
eq(vm.ctx.pc.listKey, "std:1", "...and jumps when it holds")
|
|
vm = new_vm()
|
|
vm:setPc("t_main", 4)
|
|
Ops.dispatch(vm, { op = "vcall", [1] = "std:1" })
|
|
eq(vm.ctx.pc.listKey, "std:1", "vcall enters the script")
|
|
eq(#vm.ctx.stack, 1, "...pushing a return address")
|
|
Ops.dispatch(vm, { op = "returnram" })
|
|
eq(vm.ctx.pc.listKey, "t_main", "returnram resumes the caller")
|
|
eq(vm.ctx.pc.index, 4, "...at the return site")
|
|
vm = new_vm()
|
|
Ops.dispatch(vm, { op = "vmessage", [1] = "std:1" })
|
|
eq(vm.ctx.messageOpen, true, "vmessage opens the message box")
|
|
vm = new_vm()
|
|
Ops.dispatch(vm, { op = "vbuffermessage", [1] = "std:1" })
|
|
eq(type(vm.ctx.stringVars[4]), "string", "vbuffermessage fills the gStringVar4 buffer")
|
|
vm = new_vm()
|
|
Ops.dispatch(vm, { op = "vbufferstring", [1] = 1, [2] = "std:1" })
|
|
eq(type(vm.ctx.stringVars[2]), "string", "vbufferstring fills the requested string var")
|
|
vm = new_vm()
|
|
Ops.dispatch(vm, { op = "endram" })
|
|
eq(vm.ctx.status, "shutdown", "endram stops the script")
|
|
|
|
T.finish("game3_script_verbs_subset_test")
|