mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +02:00
489 lines
21 KiB
Lua
489 lines
21 KiB
Lua
-- Map callbacks: RunMapCallback (home/map.asm) and the four places a map load
|
|
-- runs one. `luajit tests/gen2_map_callbacks_test.lua`; also dofile'd by
|
|
-- tests/run_tests.lua. Fixture-driven, with a final section that runs every
|
|
-- callback in a real Gold cache and SKIPs when there is none.
|
|
--
|
|
-- Three separate things are under test and they fail in different ways:
|
|
--
|
|
-- Vm:runCallback a NESTED script run. RunMapCallback does not check
|
|
-- wScriptRunning, so this must work with a script parked --
|
|
-- which is the normal case, because every warp a script
|
|
-- takes is a map load with that script still on the stack.
|
|
-- the ORDER HandleNewMap, then LoadBlockData, then LoadMapObjects.
|
|
-- A TILES callback that runs after the canvas is baked
|
|
-- paints nothing; an OBJECTS callback that runs after the
|
|
-- people are built shows yesterday's NPC.
|
|
-- the CACHE FLUSH a baked canvas is keyed by map and daytime and knows
|
|
-- nothing about the blocks it was baked from, so a callback
|
|
-- that rewrites blocks on every load needs the stale bakes
|
|
-- dropped or it freezes on its first answer.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 map callbacks")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Vm = require("src.script.gen2.Vm")
|
|
local Events = require("src.world.gen2.Events")
|
|
local World = require("src.world.gen2.World")
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Vm:runCallback -- ExecuteCallbackScript
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local function vmWith(scripts, hooks)
|
|
return Vm.new(scripts, {}, Events.new(), hooks or {})
|
|
end
|
|
|
|
-- The plain case: a body that ends on `endcallback` runs to completion and the
|
|
-- VM is idle again afterwards.
|
|
do
|
|
local flags = {}
|
|
local vm = vmWith({
|
|
cb = { { op = "setevent", event = 7 }, { op = "endcallback" } },
|
|
}, { onFlagsChanged = function() flags[#flags + 1] = true end })
|
|
check(vm:runCallback("cb"), "a callback body runs and reports it ran")
|
|
check(vm.events:get(7), "and its setevent landed")
|
|
check(not vm:running(), "with the VM idle again on the far side")
|
|
check(not vm:runCallback(nil), "no script key is not a run")
|
|
check(not vm:runCallback("nope"), "and neither is a key with no body")
|
|
end
|
|
|
|
-- Script_endcallback is ExitScriptSubroutine then StopScript: it ends the
|
|
-- CALLBACK, not the map load. A body with commands behind it stops there.
|
|
do
|
|
local vm = vmWith({
|
|
cb = {
|
|
{ op = "setevent", event = 1 },
|
|
{ op = "endcallback" },
|
|
{ op = "setevent", event = 2 },
|
|
},
|
|
})
|
|
vm:runCallback("cb")
|
|
check(vm.events:get(1), "commands before endcallback run")
|
|
check(not vm.events:get(2), "and nothing behind it does")
|
|
end
|
|
|
|
-- The case that made this a separate entry point from Vm:start. A script is
|
|
-- parked on a text box; the map load underneath it runs a callback; the parked
|
|
-- script must still be parked, on the SAME request, and must resume where it
|
|
-- was. Vm:start would have refused outright (`if self.busy then return
|
|
-- false`), which is what "nothing runs them yet" looked like from the inside.
|
|
do
|
|
local resumeText
|
|
local vm = vmWith({
|
|
parent = {
|
|
{ op = "writetext", text = "A" },
|
|
{ op = "setevent", event = 20 },
|
|
{ op = "end" },
|
|
},
|
|
cb = { { op = "setevent", event = 21 }, { op = "endcallback" } },
|
|
}, {
|
|
showText = function(_, onDone) resumeText = onDone end,
|
|
})
|
|
vm:start("parent")
|
|
check(vm:running(), "the parent script is up")
|
|
check(vm.pending and vm.pending.kind == "text", "and parked on its text box")
|
|
local parked = vm.pending
|
|
check(vm:runCallback("cb"), "a callback runs with a script already parked")
|
|
check(vm.events:get(21), "the callback's own command landed")
|
|
check(not vm.events:get(20),
|
|
"and the parent did NOT run on past the box it is waiting on")
|
|
check(vm:running(), "the parent is still up")
|
|
eq(vm.pending, parked, "parked on the very same request")
|
|
resumeText()
|
|
check(vm.events:get(20), "and resuming it picks up where it left off")
|
|
check(not vm:running(), "then ends normally")
|
|
end
|
|
|
|
-- A callback cannot block: ScriptEvents runs inside the map load, with no frame
|
|
-- to come back on. Nothing reachable from an extracted callback yields, so
|
|
-- this is a guard rather than a behaviour -- but if one ever does, the parent's
|
|
-- parked frame must survive it untouched.
|
|
do
|
|
local vm = vmWith({
|
|
parent = { { op = "writetext", text = "A" }, { op = "end" } },
|
|
bad = { { op = "writetext", text = "B" }, { op = "endcallback" } },
|
|
}, { showText = function() end })
|
|
vm:start("parent")
|
|
local parked = vm.pending
|
|
check(not vm:runCallback("bad"), "a blocking callback reports failure")
|
|
eq(vm.pending, parked, "and leaves the parent's request alone")
|
|
check(vm:running(), "with the parent still running")
|
|
eq(vm.blockedCallbacks["bad"], "text", "the ledger names what it blocked on")
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- World:mapCallbackScript / World:runMapCallback
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local function callbackWorld(callbacks, scripts)
|
|
local world = World.new({ data = {}, save = { party = {}, inventory = {} } })
|
|
world.maps = {
|
|
TEST_MAP = { id = "TEST_MAP", group = 1, map = 2, width = 2, height = 2,
|
|
blocks = { 1, 2, 3, 4 }, objects = {}, warps = {},
|
|
callbacks = callbacks },
|
|
}
|
|
world.map = { id = "TEST_MAP", def = world.maps.TEST_MAP,
|
|
width = 2, height = 2 }
|
|
world.map.blocks = world.maps.TEST_MAP.blocks
|
|
world.scripts = scripts or {}
|
|
-- Only the hooks these checks reach; World:load builds the real table.
|
|
world.vm = Vm.new(world.scripts, {}, world.events, {
|
|
changeBlock = function(bx, by, block)
|
|
return world:changeBlock(bx, by, block)
|
|
end,
|
|
appear = function(id) world:appearObject(id) end,
|
|
disappear = function(id) world:disappearObject(id) end,
|
|
})
|
|
return world
|
|
end
|
|
|
|
do
|
|
local world = callbackWorld({
|
|
{ callback = "MAPCALLBACK_TILES", scriptKey = "first" },
|
|
{ callback = "MAPCALLBACK_TILES", scriptKey = "second" },
|
|
{ callback = "MAPCALLBACK_NEWMAP", scriptKey = "new" },
|
|
}, {
|
|
first = { { op = "setevent", event = 1 }, { op = "endcallback" } },
|
|
second = { { op = "setevent", event = 2 }, { op = "endcallback" } },
|
|
new = { { op = "setevent", event = 3 }, { op = "endcallback" } },
|
|
})
|
|
eq(world:mapCallbackScript("MAPCALLBACK_TILES"), "first",
|
|
".FindCallback takes the FIRST row of a type")
|
|
eq(world:mapCallbackScript("MAPCALLBACK_NEWMAP"), "new",
|
|
"and matches on the type, not on the order")
|
|
check(world:mapCallbackScript("MAPCALLBACK_SPRITES") == nil,
|
|
"a type this map has no row for is nil, not the next row along")
|
|
check(world:runMapCallback("MAPCALLBACK_TILES"), "and it runs")
|
|
check(world.events:get(1) and not world.events:get(2),
|
|
"only the first row of the type")
|
|
check(not world:runMapCallback("MAPCALLBACK_OBJECTS"),
|
|
"a map with no callback of that type is a no-op, not an error")
|
|
end
|
|
|
|
-- A map with no callback list at all -- 284 of Gold's 368 maps -- must not
|
|
-- cost anything or throw.
|
|
do
|
|
local world = callbackWorld(nil, {})
|
|
check(world:mapCallbackScript("MAPCALLBACK_NEWMAP") == nil,
|
|
"a map with no callbacks answers nil")
|
|
check(not world:runMapCallback("MAPCALLBACK_NEWMAP"), "and runs nothing")
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- The order inside a map load
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- MapSetupScript_Warp reads HandleNewMap, LoadBlockData, LoadMapObjects, in
|
|
-- that order, and each of the three carries one callback. setMap is a single
|
|
-- call here, so the order has to be asserted out loud.
|
|
do
|
|
local world = callbackWorld({
|
|
{ callback = "MAPCALLBACK_OBJECTS", scriptKey = "objects" },
|
|
{ callback = "MAPCALLBACK_TILES", scriptKey = "tiles" },
|
|
{ callback = "MAPCALLBACK_NEWMAP", scriptKey = "newmap" },
|
|
}, {
|
|
newmap = { { op = "setevent", event = 1 }, { op = "endcallback" } },
|
|
tiles = { { op = "changeblock", args = { 0, 0, 9 } },
|
|
{ op = "endcallback" } },
|
|
objects = { { op = "setevent", event = 2 }, { op = "endcallback" } },
|
|
})
|
|
local order = {}
|
|
local newMapCell
|
|
world.tilesets = { TEST = {} }
|
|
world.maps.TEST_MAP.tileset = "TEST"
|
|
-- Stub out everything setMap does with love: the ORDER is the subject, and a
|
|
-- headless suite has no canvas to bake into.
|
|
local Map = require("src.world.gen2.Map")
|
|
world.imageFor = function() order[#order + 1] = "bake" return true end
|
|
world.rebuildNeighbors = function() end
|
|
world.rebuildPeople = function() order[#order + 1] = "people" end
|
|
world.applyPalettes = function() end
|
|
world.noteFlypoint = function() end
|
|
local realRun = World.runMapCallback
|
|
world.runMapCallback = function(self, kind)
|
|
order[#order + 1] = kind
|
|
if kind == "MAPCALLBACK_NEWMAP" then
|
|
newMapCell = self.player and (self.player.cellX .. "," .. self.player.cellY)
|
|
end
|
|
return realRun(self, kind)
|
|
end
|
|
world.map = Map.new(world.maps.TEST_MAP, {})
|
|
check(world:setMap("TEST_MAP", 3, 3, "down"), "the map loads")
|
|
eq(table.concat(order, " "),
|
|
"MAPCALLBACK_NEWMAP MAPCALLBACK_TILES bake MAPCALLBACK_OBJECTS people",
|
|
"HandleNewMap, then LoadBlockData, then LoadMapObjects")
|
|
check(world.events:get(1) and world.events:get(2),
|
|
"and both bodies really ran")
|
|
eq(world.maps.TEST_MAP.blocks[1], 9,
|
|
"the TILES callback's changeblock is in the buffer BEFORE the bake")
|
|
-- data/maps/setup_scripts.asm:99-106: GetWarpDestCoords before HandleNewMap.
|
|
eq(newMapCell, "3,3",
|
|
"the NEWMAP callback reads the DESTINATION coords, not the map it left")
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Route16AlwaysOnBikeCallback
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- maps/Route16.asm:7-17. The callback is two coordinate tests and nothing
|
|
-- else: YCOORD < 5 or XCOORD > 13 takes .CanWalk, everything else `setflag
|
|
-- ENGINE_ALWAYS_ON_BIKE`. It carries no `checkitem` -- the BICYCLE refusal is
|
|
-- the gatehouse's own coord event (maps/Route16Gate.asm:16-29, :69-70) -- so
|
|
-- both arms below run on a save with an empty PACK.
|
|
do
|
|
local Bike = require("src.world.gen2.Bike")
|
|
local FieldMoves = require("src.world.gen2.FieldMoves")
|
|
local Map = require("src.world.gen2.Map")
|
|
|
|
local function route16At(cx, cy)
|
|
local world = callbackWorld({
|
|
{ callback = "MAPCALLBACK_NEWMAP", scriptKey = "bike" },
|
|
}, {
|
|
-- The extracted body, opcode for opcode: VAR_YCOORD $13, VAR_XCOORD $12.
|
|
bike = {
|
|
{ op = "readvar", var = 0x13 },
|
|
{ op = "ifless", value = 5, script = "walk" },
|
|
{ op = "readvar", var = 0x12 },
|
|
{ op = "ifgreater", value = 13, script = "walk" },
|
|
{ op = "setflag", flag = Bike.ENGINE_ALWAYS_ON_BIKE },
|
|
{ op = "endcallback" },
|
|
},
|
|
walk = {
|
|
{ op = "clearflag", flag = Bike.ENGINE_ALWAYS_ON_BIKE },
|
|
{ op = "endcallback" },
|
|
},
|
|
})
|
|
world.maps.TEST_MAP.environment = "ROUTE"
|
|
world.maps.TEST_MAP.width, world.maps.TEST_MAP.height = 10, 10
|
|
world.tilesets = { TEST = {} }
|
|
world.maps.TEST_MAP.tileset = "TEST"
|
|
world.imageFor = function() return true end
|
|
world.rebuildNeighbors = function() end
|
|
world.rebuildPeople = function() end
|
|
world.applyPalettes = function() end
|
|
world.vm = Vm.new(world.scripts, {}, world.events, {
|
|
readVar = function(id) return world:readVar(id) end,
|
|
setEngineFlag = function(flag, value) world:setEngineFlag(flag, value) end,
|
|
})
|
|
world.map = Map.new(world.maps.TEST_MAP, {})
|
|
check(world:setMap("TEST_MAP", cx, cy, "down"), "the map loads")
|
|
return world
|
|
end
|
|
|
|
-- warp_event 9, 6 (maps/Route16.asm:36): the Cycling Road side of the gate.
|
|
local road = route16At(9, 6)
|
|
check(road:alwaysOnBike(),
|
|
"arriving west of the gate sets ENGINE_ALWAYS_ON_BIKE")
|
|
-- .CheckForcedBiking (engine/overworld/map_setup.asm:112-120) reads the flag
|
|
-- and nothing else: no BICYCLE is consulted, on the cart or here.
|
|
eq(road.playerState, FieldMoves.PLAYER_BIKE, "and the load mounts the bike")
|
|
|
|
-- warp_event 14, 6 (maps/Route16.asm:34): the gatehouse doorway, one cell
|
|
-- past the XCOORD test. Read off the map being LEFT this is the road arm,
|
|
-- which is the whole reason the coords are written before HandleNewMap.
|
|
local gate = route16At(14, 6)
|
|
check(not gate:alwaysOnBike(), "arriving at the gate doorway takes .CanWalk")
|
|
eq(gate.playerState, FieldMoves.PLAYER_NORMAL, "and nothing mounts")
|
|
|
|
-- The YCOORD arm, north of the fork: `ifless 5, .CanWalk` on its own.
|
|
local north = route16At(9, 4)
|
|
check(not north:alwaysOnBike(), "and so does anything north of YCOORD 5")
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- The baked canvas has to follow the blocks
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
do
|
|
local world = callbackWorld(nil, {})
|
|
world.mapImages = {
|
|
["TEST_MAP|DAY|gbc|1"] = "a", ["TEST_MAP|NITE|gbc|1"] = "b",
|
|
["OTHER_MAP|DAY|gbc|1"] = "c",
|
|
}
|
|
world:dropMapImages("TEST_MAP")
|
|
check(world.mapImages["TEST_MAP|DAY|gbc|1"] == nil, "both bakes of the map")
|
|
check(world.mapImages["TEST_MAP|NITE|gbc|1"] == nil, "go, daytime and all")
|
|
eq(world.mapImages["OTHER_MAP|DAY|gbc|1"], "c",
|
|
"and the neighbours' keep theirs -- only the edited map's are stale")
|
|
|
|
-- restoreBlocks is LoadMapAttributes' refill. Putting the blocks back
|
|
-- without dropping the bake left a CUT tree cut for the session, and would
|
|
-- have frozen every MAPCALLBACK_TILES map on whichever answer it baked first.
|
|
world.blockEdits = { TEST_MAP = { [1] = 5 } }
|
|
world.maps.TEST_MAP.blocks[1] = 99
|
|
world.mapImages["TEST_MAP|DAY|gbc|1"] = "stale"
|
|
check(world:restoreBlocks(), "restoreBlocks reports it restored something")
|
|
eq(world.maps.TEST_MAP.blocks[1], 5, "the original block is back")
|
|
check(world.mapImages["TEST_MAP|DAY|gbc|1"] == nil,
|
|
"and the bake taken off the edited blocks went with it")
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- readvar VAR_WEEKDAY
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- GetWeekday reads wCurDay, SUNDAY 0 .. SATURDAY 6 -- the same numbering
|
|
-- os.date("%w") uses. 39 of the 40 readvar sites reachable from a callback are
|
|
-- this one, and every one of them was taking the SUNDAY arm.
|
|
do
|
|
local world = callbackWorld(nil, {})
|
|
world.clockDay = 2
|
|
eq(world:weekday(), 2, "clockDay pins the day")
|
|
world.clockDay = 9
|
|
eq(world:weekday(), 2, "and wraps into the week")
|
|
world.clockDay = nil
|
|
local today = tonumber(os.date("%w")) or 0
|
|
eq(world:weekday(), today, "unpinned, it is the host clock's day")
|
|
check(world:weekday() >= 0 and world:weekday() <= 6, "in SUNDAY..SATURDAY")
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Every callback in a real cache
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
if not cache then
|
|
local home = os.getenv("HOME") or ""
|
|
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
|
|
end
|
|
local mapsPath = cache .. "/data/generated/maps.lua"
|
|
local mf = io.open(mapsPath, "r")
|
|
if not mf then
|
|
check(true, "gold cache absent : fixture checks only (SKIP cache facts)")
|
|
S.finish()
|
|
return
|
|
end
|
|
mf:close()
|
|
|
|
local maps = assert(loadfile(mapsPath))()
|
|
local scripts = assert(loadfile(cache .. "/data/generated/scripts.lua"))()
|
|
local constants = assert(loadfile(cache .. "/data/generated/constants.lua"))()
|
|
|
|
local byType, rows = {}, {}
|
|
for id, def in pairs(maps) do
|
|
for _, cb in ipairs(def.callbacks or {}) do
|
|
byType[cb.callback] = (byType[cb.callback] or 0) + 1
|
|
rows[#rows + 1] = { map = id, kind = cb.callback, key = cb.scriptKey }
|
|
end
|
|
end
|
|
check(#rows > 0, "the cache carries map callbacks at all")
|
|
-- The census, so a re-import that loses a whole type fails here rather than by
|
|
-- a door quietly staying open. MAPCALLBACK_SPRITES has no user in Gold.
|
|
eq(byType.MAPCALLBACK_NEWMAP, 39, "39 MAPCALLBACK_NEWMAP")
|
|
eq(byType.MAPCALLBACK_OBJECTS, 24, "24 MAPCALLBACK_OBJECTS")
|
|
eq(byType.MAPCALLBACK_TILES, 19, "19 MAPCALLBACK_TILES")
|
|
eq(byType.MAPCALLBACK_CMDQUEUE, 2, "2 MAPCALLBACK_CMDQUEUE")
|
|
check(byType.MAPCALLBACK_SPRITES == nil,
|
|
"and no MAPCALLBACK_SPRITES, which no Gold map declares")
|
|
|
|
local missing = 0
|
|
for _, row in ipairs(rows) do
|
|
if not (row.key and scripts[row.key]) then missing = missing + 1 end
|
|
end
|
|
eq(missing, 0, "every callback row names a body the cache carries")
|
|
|
|
-- Run all of them. A callback is a script, and the one thing that must hold
|
|
-- for all 84 is that each completes inside the map load: no yield, no unknown
|
|
-- opcode, no bad byte. The hooks are counters rather than a World, so this
|
|
-- checks the BODIES rather than the wiring -- the wiring is the order check
|
|
-- above and tests/drivers/gold_map_callbacks.lua.
|
|
do
|
|
local blocks, appears, disappears = 0, 0, 0
|
|
local vm = Vm.new(scripts, {}, Events.new(), {
|
|
specialOrder = constants.specialOrder,
|
|
specials = {},
|
|
-- VAR_WEEKDAY, so the day-of-week arms are exercised rather than all
|
|
-- taking SUNDAY the way an unanswered readvar makes them.
|
|
readVar = function(id) return id == 0x0b and 2 or 0 end,
|
|
changeBlock = function() blocks = blocks + 1 return true end,
|
|
appear = function() appears = appears + 1 end,
|
|
disappear = function() disappears = disappears + 1 end,
|
|
})
|
|
local ran = 0
|
|
for _, row in ipairs(rows) do
|
|
if vm:runCallback(row.key) then ran = ran + 1 end
|
|
end
|
|
eq(ran, #rows, "all " .. #rows .. " callback bodies run to completion")
|
|
local blocked = {}
|
|
for key in pairs(vm.blockedCallbacks) do blocked[#blocked + 1] = key end
|
|
eq(#blocked, 0,
|
|
"none of them blocks: " .. table.concat(blocked, ", "))
|
|
local unknown = {}
|
|
for op in pairs(vm.unknownOps) do unknown[#unknown + 1] = op end
|
|
table.sort(unknown)
|
|
eq(#unknown, 0,
|
|
"and none reaches an unimplemented opcode: " .. table.concat(unknown, ", "))
|
|
eq(#vm.badBytes, 0, "with no extractor `unknown` row inside one")
|
|
check(blocks > 0, "the run really repainted blocks (" .. blocks .. ")")
|
|
check(appears > 0 and disappears > 0,
|
|
("and moved objects (%d appear, %d disappear)"):format(appears, disappears))
|
|
end
|
|
|
|
-- The named ones, so the census above cannot pass on the wrong rows.
|
|
do
|
|
local function kindOf(mapId, kind)
|
|
for _, cb in ipairs((maps[mapId] or {}).callbacks or {}) do
|
|
if cb.callback == kind then return scripts[cb.scriptKey] end
|
|
end
|
|
return nil
|
|
end
|
|
local function ops(body)
|
|
local out = {}
|
|
for _, c in ipairs(body or {}) do out[#out + 1] = c.op end
|
|
return table.concat(out, ",")
|
|
end
|
|
|
|
-- NewBarkTownFlypointCallback: `setflag ENGINE_FLYPOINT_NEW_BARK` is the
|
|
-- cart's own way of banking a fly point.
|
|
check(ops(kindOf("NEW_BARK_TOWN", "MAPCALLBACK_NEWMAP")):find("setflag"),
|
|
"New Bark's NEWMAP callback is the flypoint flag")
|
|
-- Route31CheckMomCallCallback queues Mom's worried call, which is a whole
|
|
-- phone conversation nothing could reach while the callbacks were dead.
|
|
check(ops(kindOf("ROUTE_31", "MAPCALLBACK_NEWMAP"))
|
|
:find("checkevent") ~= nil, "Route 31's NEWMAP callback checks the egg quest")
|
|
-- BrunosRoomDoorsCallback: the entrance sealing behind you and the exit
|
|
-- opening after the battle. Both `iffalse`s jump FORWARD over their own
|
|
-- changeblock into the next test, so both sit in the body itself rather than
|
|
-- behind a branch -- which is why the whole callback is five commands.
|
|
local bruno = kindOf("BRUNOS_ROOM", "MAPCALLBACK_TILES")
|
|
local brunoBlocks = 0
|
|
for _, c in ipairs(bruno or {}) do
|
|
if c.op == "changeblock" then brunoBlocks = brunoBlocks + 1 end
|
|
end
|
|
eq(brunoBlocks, 2, "Bruno's room walls itself in with two changeblocks")
|
|
check(ops(bruno):find("checkevent"),
|
|
"and it opens on EVENT_BRUNOS_ROOM_ENTRANCE_CLOSED")
|
|
-- GoldenrodUndergroundCheckDayOfWeekCallback: seven arms off VAR_WEEKDAY.
|
|
check(ops(kindOf("GOLDENROD_UNDERGROUND", "MAPCALLBACK_OBJECTS"))
|
|
:find("readvar"), "the Goldenrod underground reads the weekday")
|
|
|
|
-- Route16AlwaysOnBikeCallback (maps/Route16.asm:7-17), read off the cache:
|
|
-- VAR_YCOORD $13 then VAR_XCOORD $12, and no `checkitem` anywhere in it.
|
|
local r16 = kindOf("ROUTE_16", "MAPCALLBACK_NEWMAP")
|
|
eq(ops(r16), "readvar,ifless,readvar,ifgreater,setflag,endcallback",
|
|
"Route 16's NEWMAP callback is the coordinate test")
|
|
local vars = {}
|
|
for _, c in ipairs(r16 or {}) do
|
|
if c.op == "readvar" then vars[#vars + 1] = c.var end
|
|
end
|
|
eq(table.concat(vars, ","), "19,18", "VAR_YCOORD first, then VAR_XCOORD")
|
|
-- Route17AlwaysOnBikeCallback (maps/Route17.asm:13-16): the sibling, with no
|
|
-- coordinate test at all -- ALWAYS_ON_BIKE and DOWNHILL, unconditionally.
|
|
eq(ops(kindOf("ROUTE_17", "MAPCALLBACK_NEWMAP")),
|
|
"setflag,setflag,endcallback", "Route 17 forces the bike and the downhill")
|
|
|
|
-- maps/Route16Gate.asm:69-70: the BICYCLE gate is the gatehouse's two coord
|
|
-- events, which is what keeps a bikeless player off the road the callback
|
|
-- above mounts unconditionally.
|
|
local gateEvents = (maps.ROUTE_16_GATE or {}).coordEvents or {}
|
|
eq(#gateEvents, 2, "the Route 16 gatehouse carries its two coord events")
|
|
local checks = 0
|
|
for _, ev in ipairs(gateEvents) do
|
|
if ops(scripts[ev.scriptKey]):find("checkitem") then checks = checks + 1 end
|
|
end
|
|
eq(checks, 2, "and both are the checkitem BICYCLE refusal")
|
|
end
|
|
|
|
S.finish()
|