mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
500 lines
21 KiB
Lua
500 lines
21 KiB
Lua
-- Hidden items: engine/events/checkforhiddenitems.asm, the BGEVENT_ITEM arm of
|
|
-- the bg event dispatch (engine/overworld/events.asm `.itemifset`) and
|
|
-- HiddenItemScript (engine/events/hidden_item.asm), plus the extractor half
|
|
-- that stopped walking a `hiddenitem` struct as bytecode.
|
|
--
|
|
-- ROM-free: `luajit tests/gen2_hidden_items_test.lua`. The cache section at
|
|
-- the bottom SKIPs when no Gold cache is present.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 hidden items")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local HiddenItems = require("src.world.gen2.HiddenItems")
|
|
local Events = require("src.world.gen2.Events")
|
|
local Vm = require("src.script.gen2.Vm")
|
|
|
|
-- maps/Route42.asm:
|
|
-- bg_event 16, 11, BGEVENT_ITEM, Route42HiddenMaxPotion
|
|
-- Route42HiddenMaxPotion: hiddenitem MAX_POTION, EVENT_ROUTE_42_HIDDEN_MAX_POTION
|
|
-- MAX_POTION is item 15 (constants/item_constants.asm) and the flag is 173
|
|
-- (constants/event_flags.asm).
|
|
local MAX_POTION, ROUTE_42_FLAG = 15, 173
|
|
|
|
local function itemRow(x, y, item, event)
|
|
return { x = x, y = y, kind = HiddenItems.BGEVENT_ITEM,
|
|
hiddenItem = { item = item, event = event } }
|
|
end
|
|
|
|
-- ---- the bg event row -----------------------------------------------------
|
|
do
|
|
local row = itemRow(16, 11, MAX_POTION, ROUTE_42_FLAG)
|
|
eq(HiddenItems.dataOf(row).item, MAX_POTION, "kind 7 carries hiddenitem data")
|
|
eq(HiddenItems.dataOf(row).event, ROUTE_42_FLAG, "and its event flag")
|
|
check(HiddenItems.dataOf({ x = 1, y = 1, kind = 0, scriptKey = "1:4000" }) == nil,
|
|
"BGEVENT_READ is a sign, not an item")
|
|
check(HiddenItems.dataOf({ x = 1, y = 1, kind = 7 }) == nil,
|
|
"a kind 7 row from a cache that predates the decode answers nil")
|
|
check(HiddenItems.dataOf(nil) == nil, "and nil is not a row")
|
|
end
|
|
|
|
-- ---- CheckForHiddenItems' box ---------------------------------------------
|
|
-- The corner is player + SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4 = +5, +4, and the
|
|
-- differences that survive are 0..9 on x and 0..8 on y. So the box is ten
|
|
-- cells wide with the player LEFT of centre and nine tall with the player
|
|
-- centred, and it is not a radius.
|
|
do
|
|
local px, py = 20, 20
|
|
check(HiddenItems.onScreen(px, py, px, py), "the player's own cell is on screen")
|
|
check(HiddenItems.onScreen(px, py, px + 5, py), "five cells right is the last x")
|
|
check(not HiddenItems.onScreen(px, py, px + 6, py), "six is past the corner")
|
|
check(HiddenItems.onScreen(px, py, px - 4, py), "four cells left is the first x")
|
|
check(not HiddenItems.onScreen(px, py, px - 5, py), "five left is a screen away")
|
|
check(HiddenItems.onScreen(px, py, px, py + 4), "four cells down is the last y")
|
|
check(not HiddenItems.onScreen(px, py, px, py + 5), "five down is past the corner")
|
|
check(HiddenItems.onScreen(px, py, px, py - 4), "four cells up is the first y")
|
|
check(not HiddenItems.onScreen(px, py, px, py - 5), "five up is a screen away")
|
|
check(not HiddenItems.onScreen(px, py, px + 6, py + 4),
|
|
"either axis out of range is out")
|
|
end
|
|
|
|
-- ---- the sweep ------------------------------------------------------------
|
|
do
|
|
local def = { bgEvents = {
|
|
{ x = 3, y = 3, kind = 0, scriptKey = "1:4000" },
|
|
itemRow(16, 11, MAX_POTION, ROUTE_42_FLAG),
|
|
itemRow(40, 40, 32, 182),
|
|
} }
|
|
local events = Events.new()
|
|
eq(#HiddenItems.unfound(def, events), 2, "two hidden items, one sign")
|
|
local found = HiddenItems.nearby(def, 14, 10, events)
|
|
check(found ~= nil, "the ITEMFINDER answers for the one in range")
|
|
eq(found.item, MAX_POTION, "and hands back the item")
|
|
eq(found.event, ROUTE_42_FLAG, "and its flag")
|
|
check(HiddenItems.nearby(def, 30, 30, events) == nil,
|
|
"and nothing when both are off screen")
|
|
|
|
events:set(ROUTE_42_FLAG, true)
|
|
check(HiddenItems.nearby(def, 14, 10, events) == nil,
|
|
"an item already found never sets the ITEMFINDER off again")
|
|
eq(#HiddenItems.unfound(def, events), 1, "and drops out of the unfound list")
|
|
check(HiddenItems.nearby(def, 14, 10, nil) ~= nil,
|
|
"with no flag store at all nothing has been found yet")
|
|
end
|
|
|
|
-- ---- the facing cell ------------------------------------------------------
|
|
do
|
|
local def = { bgEvents = {
|
|
{ x = 3, y = 3, kind = 0, scriptKey = "1:4000" },
|
|
itemRow(16, 11, MAX_POTION, ROUTE_42_FLAG),
|
|
} }
|
|
local events = Events.new()
|
|
eq(HiddenItems.at(def, 16, 11, events).item, MAX_POTION,
|
|
"the facing cell finds the hidden item")
|
|
check(HiddenItems.at(def, 3, 3, events) == nil, "a sign is not one")
|
|
check(HiddenItems.at(def, 9, 9, events) == nil, "and an empty cell is not one")
|
|
events:set(ROUTE_42_FLAG, true)
|
|
check(HiddenItems.at(def, 16, 11, events) == nil,
|
|
"`.itemifset` jumps to .dontread once the flag is set")
|
|
end
|
|
|
|
-- ---- HiddenItemScript -----------------------------------------------------
|
|
do
|
|
local script = HiddenItems.pickupScript(MAX_POTION, ROUTE_42_FLAG)
|
|
local ops = {}
|
|
for _, cmd in ipairs(script) do ops[#ops + 1] = cmd.op end
|
|
eq(table.concat(ops, ","),
|
|
"opentext,getitemname,rawtext,giveitem,iffalse,setevent,specialsound," ..
|
|
"itemnotify,closetext,end",
|
|
"the order is HiddenItemScript's, with readmem folded into the item")
|
|
eq(script[4].item, MAX_POTION, "giveitem ITEM_FROM_MEM gives the found item")
|
|
eq(script[4].quantity, 1, "one of it")
|
|
eq(script[6].event, ROUTE_42_FLAG,
|
|
"callasm SetMemEvent sets the flag by NUMBER")
|
|
local fullOps = {}
|
|
for _, cmd in ipairs(script[5].script) do fullOps[#fullOps + 1] = cmd.op end
|
|
eq(table.concat(fullOps, ","),
|
|
"promptbutton,rawtext,waitbutton,closetext,end",
|
|
".bag_full prints and stops; it sets no flag")
|
|
end
|
|
|
|
-- The same list through the real VM, which is where the ordering matters: the
|
|
-- flag must be set only on the arm that really took the item.
|
|
local function runPickup(bagFull)
|
|
local events = Events.new()
|
|
local log, given = {}, nil
|
|
local vm = Vm.new({}, {}, events, {
|
|
showText = function(body, onDone)
|
|
log[#log + 1] = body
|
|
if onDone then onDone() end
|
|
end,
|
|
getItemName = function(index)
|
|
eq(index, MAX_POTION, "getitemname is asked for the found item")
|
|
return "MAX POTION"
|
|
end,
|
|
giveItem = function(index, qty)
|
|
if bagFull then return false end
|
|
given = { index = index, qty = qty }
|
|
return true
|
|
end,
|
|
specialSound = function() end,
|
|
waitSfx = function() return true end,
|
|
})
|
|
vm:start(HiddenItems.pickupScript(MAX_POTION, ROUTE_42_FLAG))
|
|
for _ = 1, 8 do vm:update() end
|
|
return events, log, given
|
|
end
|
|
|
|
do
|
|
local events, log, given = runPickup(false)
|
|
eq(log[1], "{PLAYER} found\nMAX POTION.",
|
|
"_PlayerFoundItemText names the item; {PLAYER} is TextBox's to fill")
|
|
eq(given.index, MAX_POTION, "and the item lands in the pack")
|
|
eq(given.qty, 1, "one of it")
|
|
check(events:get(ROUTE_42_FLAG), "SetMemEvent ticks the flag off")
|
|
eq(log[2], "{PLAYER} put the\nMAX POTION in\nthe ITEM POCKET.",
|
|
"itemnotify is the second line")
|
|
|
|
local fullEvents, fullLog = runPickup(true)
|
|
check(not fullEvents:get(ROUTE_42_FLAG),
|
|
"a full pack leaves the flag CLEAR, so the item is still there")
|
|
eq(fullLog[2], "But {PLAYER} has\nno space left…", "_ButNoSpaceText instead")
|
|
end
|
|
|
|
-- ---- FindItemInBallScript, the OBJECTTYPE_ITEMBALL press -------------------
|
|
-- engine/events/misc_scripts.asm:9. The object's pointer is two raw bytes, so
|
|
-- nothing exists for the VM to start: World:interact feeds it this built list.
|
|
-- Two load-bearing orderings. `callasm .TryReceiveItem` names the item AND
|
|
-- takes it before any box is drawn, so the full-pocket branch is chosen with
|
|
-- nothing on screen and its arm prints two boxes of its own; and `disappear`
|
|
-- (which is what sets the ball's own event flag through World:disappearObject)
|
|
-- sits on the success side of the iffalse only, or a full pack would still eat
|
|
-- the ball. This is NOT the hidden item's script with a disappear swapped in,
|
|
-- which is what it used to assert.
|
|
local function runBallPickup(bagFull)
|
|
local events = Events.new()
|
|
local log, given, gone, sfx = {}, nil, nil, nil
|
|
local vm = Vm.new({}, {}, events, {
|
|
showText = function(body, onDone)
|
|
log[#log + 1] = body
|
|
if onDone then onDone() end
|
|
end,
|
|
getItemName = function() return "HM07" end,
|
|
giveItem = function(index, qty)
|
|
if bagFull then return false end
|
|
given = { index = index, qty = qty }
|
|
return true
|
|
end,
|
|
disappear = function(objectId) gone = objectId end,
|
|
playSound = function(id) sfx = id end,
|
|
specialSound = function() end,
|
|
waitSfx = function() return true end,
|
|
})
|
|
vm:start(HiddenItems.ballPickupScript(MAX_POTION, 1, 2))
|
|
-- The cart's `pause 60` now rides the found line's own row as `hold` (see
|
|
-- the order assertion below), and a `hold` is counted by the TEXT BOX, not
|
|
-- by the VM -- this stub showText answers straight away, so nothing parks
|
|
-- here. The loop is left long so a future park would still be outlasted
|
|
-- rather than read as a hang.
|
|
for _ = 1, 160 do vm:update() end
|
|
return log, given, gone, sfx, vm
|
|
end
|
|
|
|
do
|
|
local script = HiddenItems.ballPickupScript(MAX_POTION, 3, 5)
|
|
local ops = {}
|
|
for _, cmd in ipairs(script) do ops[#ops + 1] = cmd.op end
|
|
-- FindItemInBallScript is `writetext .FoundItemText / playsound SFX_ITEM /
|
|
-- pause 60 / itemnotify` (engine/events/misc_scripts.asm:13-17), all of it
|
|
-- inside ONE MapTextbox: nothing between the found line and the itemnotify
|
|
-- line takes a box down. The port's box does take itself down -- it waits
|
|
-- for its own button and pops on it -- so written in that order the pause
|
|
-- ran with an EMPTY state stack: 120 frames of bare overworld inside one
|
|
-- cart textbox, with Game2's play clock counting for all of them
|
|
-- (tests/drivers/gold_item_pickup_box.lua measures exactly that).
|
|
--
|
|
-- So the pause rides the found line's own row as `hold`, which World:showText
|
|
-- counts on the standing box, and the sound leads the text instead of
|
|
-- trailing it -- it has to be rung before the row that does not come back
|
|
-- until the hold has drained.
|
|
eq(table.concat(ops, ","),
|
|
"getitemname,giveitem,iffalse,disappear,opentext,playsound,rawtext," ..
|
|
"itemnotify,closetext,end",
|
|
"FindItemInBallScript's order: the callasm pair first, then the freeze")
|
|
eq(script[2].quantity, 3, "the itemball row's own quantity is given")
|
|
eq(script[4].object, 5, "disappear names the object const the caller built")
|
|
eq(script[6].id, 1, "playsound SFX_ITEM, not specialsound's TM-aware pick")
|
|
eq(script[7].stay, true, "the found line holds the ONE MapTextbox open")
|
|
eq(script[7].hold, 60, "and `pause 60` holds the world under that box")
|
|
|
|
local log, given, gone, sfx, vm = runBallPickup(false)
|
|
eq(log[1], "{PLAYER} found\nHM07!",
|
|
"_FoundItemText ends on '!' -- the '.' one is the hidden item's")
|
|
eq(given.index, MAX_POTION, "and the item lands in the pack")
|
|
eq(gone, 2, "the ball object disappears -- its event flag rides on that")
|
|
eq(sfx, 1, "SFX_ITEM rang")
|
|
check(not vm:running(), "and the script ran off the end of the pause")
|
|
|
|
local fullLog, fullGiven, fullGone = runBallPickup(true)
|
|
check(fullGiven == nil, "a full pack takes nothing")
|
|
check(fullGone == nil, "and the ball stays where it is")
|
|
eq(fullLog[1], "{PLAYER} found\nHM07!", "the .no_room arm shows the find too")
|
|
eq(fullLog[2], "But {PLAYER} can't\ncarry any more\vitems!",
|
|
"_CantCarryItemText is the second box, not _ButNoSpaceText")
|
|
end
|
|
|
|
-- ---- ItemFinder's two queued scripts --------------------------------------
|
|
do
|
|
local asked = {}
|
|
local function sfxId(want, fallback)
|
|
asked[#asked + 1] = want
|
|
return fallback + 100
|
|
end
|
|
local found = HiddenItems.itemfinderScript(
|
|
{ x = 1, y = 1, item = MAX_POTION, event = ROUTE_42_FLAG }, sfxId)
|
|
local ops = {}
|
|
for _, cmd in ipairs(found) do ops[#ops + 1] = cmd.op end
|
|
eq(#asked, 8, ".ItemfinderSound is four rounds of two sounds")
|
|
eq(asked[1], "Sfx_SecondPartOfItemfinder", "the beep comes first")
|
|
eq(asked[2], "Sfx_Transaction", "then the answering blip")
|
|
eq(ops[1], "waitsfx", "WaitPlaySFX waits BEFORE it plays")
|
|
eq(ops[2], "playsound", "and the play follows the wait")
|
|
eq(found[2].id, 118, "the id is resolved through this cache's sfx table")
|
|
eq(#found, 21, "sixteen sound rows and the five the text box needs")
|
|
eq(table.concat({ ops[16], ops[17], ops[18], ops[19], ops[20], ops[21] }, ","),
|
|
"playsound,opentext,rawtext,waitbutton,closetext,end",
|
|
"and the text lands after the last sound, unwaited")
|
|
|
|
local nothing = HiddenItems.itemfinderScript(nil, sfxId)
|
|
local nothingOps = {}
|
|
for _, cmd in ipairs(nothing) do nothingOps[#nothingOps + 1] = cmd.op end
|
|
eq(table.concat(nothingOps, ","),
|
|
"opentext,rawtext,waitbutton,closetext,end",
|
|
".Script_FoundNothing makes no sound at all")
|
|
eq(#asked, 8, "and asks the sfx table for nothing")
|
|
end
|
|
|
|
-- ---- the world wiring -----------------------------------------------------
|
|
local World = require("src.world.gen2.World")
|
|
|
|
local COLL_FLOOR = 0x00
|
|
|
|
local function fakeWorld(bgEvents, px, py, facing)
|
|
local game = {
|
|
data = { items = {
|
|
MAX_POTION = { id = "MAX_POTION", name = "MAX POTION",
|
|
pocket = "ITEM", index = MAX_POTION },
|
|
ITEMFINDER = { id = "ITEMFINDER", name = "ITEMFINDER",
|
|
pocket = "KEY_ITEM", index = 0x3c },
|
|
} },
|
|
save = { player = { name = "GOLD" }, party = {},
|
|
inventory = { ITEMFINDER = 1 } },
|
|
}
|
|
local world = World.new(game)
|
|
game.world = world
|
|
world.map = {
|
|
id = "TEST_MAP",
|
|
def = { bgEvents = bgEvents, objects = {}, width = 10, height = 10 },
|
|
cellCollision = function() return COLL_FLOOR end,
|
|
warpAt = function() return nil end,
|
|
}
|
|
world.maps = { TEST_MAP = world.map.def }
|
|
world.player = {
|
|
cellX = px, cellY = py, px = px * 16, py = py * 16,
|
|
facing = facing or "down", moving = false,
|
|
update = function() return false end,
|
|
}
|
|
world.pollTimeOfDay = function() end
|
|
world.playSfx = function(self, id) self.sfxLog = self.sfxLog or {}
|
|
self.sfxLog[#self.sfxLog + 1] = id end
|
|
local started = {}
|
|
world.startedScripts = started
|
|
world.vm = {
|
|
running = function() return false end,
|
|
update = function() end,
|
|
start = function(_, script) started[#started + 1] = script return true end,
|
|
}
|
|
return world, game
|
|
end
|
|
|
|
do
|
|
-- Facing the hidden item's cell: the A press runs HiddenItemScript.
|
|
local world = fakeWorld({ itemRow(5, 6, MAX_POTION, ROUTE_42_FLAG) },
|
|
5, 5, "down")
|
|
check(world:interact(), "an A press into a hidden item is taken")
|
|
eq(#world.startedScripts, 1, "and starts one script")
|
|
eq(world.startedScripts[1][4].item, MAX_POTION,
|
|
"the script the world built is the pickup for that item")
|
|
eq(world.sfxLog[1], 8, "PlayTalkObject's SFX_READ_TEXT_2 opens it")
|
|
|
|
-- Already found: `.dontread`, so the press is NOT taken and falls through to
|
|
-- the tile events (none here, so interact answers false).
|
|
world.events:set(ROUTE_42_FLAG, true)
|
|
check(not world:interact(), "a hidden item already taken does not eat the press")
|
|
eq(#world.startedScripts, 1, "and starts nothing")
|
|
end
|
|
|
|
do
|
|
-- OBJECTTYPE_ITEMBALL: World:interact has to route the A press to the built
|
|
-- ball pickup, because the object's pointer is raw (item, quantity) bytes
|
|
-- and there is no scriptKey. Without this arm no floor item in the game
|
|
-- could be taken -- HM07 WATERFALL among them.
|
|
local world = fakeWorld({}, 5, 5, "up")
|
|
world.map.def.objects = {
|
|
{ index = 0, x = 5, y = 4, type = 1, -- OBJECTTYPE_ITEMBALL, faced cell
|
|
itemball = { item = MAX_POTION, quantity = 1 } },
|
|
}
|
|
world.npcs = {
|
|
{ cellX = 5, cellY = 4, def = world.map.def.objects[1] },
|
|
}
|
|
check(world:interact(), "an A press into an item ball is taken")
|
|
eq(#world.startedScripts, 1, "and starts the pickup script")
|
|
eq(world.startedScripts[1][2].item, MAX_POTION, "for the ball's item")
|
|
-- And hLastTalked is set to the object, so the script's disappear names the
|
|
-- ball and not whatever an earlier conversation left behind.
|
|
eq(world.vm.lastTalked, 1, "hLastTalked points at the ball object (index+1)")
|
|
end
|
|
|
|
do
|
|
-- ItemFinder from the PACK: QueueScript, so nothing runs until the menus are
|
|
-- gone and the world owns the frame again.
|
|
local world = fakeWorld({ itemRow(5, 6, MAX_POTION, ROUTE_42_FLAG) },
|
|
5, 5, "down")
|
|
eq(world:useFieldItem("ITEMFINDER"), "itemfinder",
|
|
"the ITEMFINDER always quits the PACK; there is no refusal arm")
|
|
check(world.queuedScript ~= nil, "the script is QUEUED, not run")
|
|
eq(#world.startedScripts, 0, "so nothing has started yet")
|
|
eq(world.queuedScript[1].op, "waitsfx", "and it is the found-something one")
|
|
world:step()
|
|
eq(#world.startedScripts, 1, "the drain starts it the next frame")
|
|
check(world.queuedScript == nil, "and the queue is empty again")
|
|
|
|
local empty = fakeWorld({}, 5, 5, "down")
|
|
eq(empty:useFieldItem("ITEMFINDER"), "itemfinder", "a bare map still quits")
|
|
eq(empty.queuedScript[1].op, "opentext",
|
|
"and queues .Script_FoundNothing, which is silent")
|
|
check(empty:useFieldItem("MAX_POTION") == nil,
|
|
"any other item still falls through to the PACK's own onChoose")
|
|
end
|
|
|
|
-- ---- against the cache ----------------------------------------------------
|
|
-- Every row here is transcribed from pokegold: `bg_event x, y, BGEVENT_ITEM`
|
|
-- out of maps/<Map>.asm and the `hiddenitem item, flag` it points at, with the
|
|
-- item id from constants/item_constants.asm and the flag from
|
|
-- constants/event_flags.asm.
|
|
local PINNED = {
|
|
{ "AZALEA_TOWN", 31, 6, 38, 177 }, -- FULL_HEAL
|
|
{ "ROUTE_42", 16, 11, 15, 173 }, -- MAX_POTION
|
|
{ "TIN_TOWER_4F", 11, 6, 15, 125 }, -- MAX_POTION, the lowest flag
|
|
{ "DRAGONS_DEN_B1F", 31, 15, 21, 162 }, -- MAX_ELIXER
|
|
{ "CERULEAN_CITY", 2, 12, 152, 250 }, -- BERSERK_GENE
|
|
{ "MOUNT_MOON_SQUARE", 7, 7, 8, 236 }, -- MOON_STONE
|
|
{ "ROUTE_17", 8, 77, 21, 247 }, -- MAX_ELIXER, the highest flag
|
|
{ "GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES", 1, 8, 39, 143 }, -- REVIVE
|
|
}
|
|
|
|
-- NATIONAL_PARK and NATIONAL_PARK_BUG_CONTEST are the same map twice and share
|
|
-- one flag: finding the FULL_HEAL during the contest finds it outside too.
|
|
local SHARED_FLAG = { "NATIONAL_PARK", "NATIONAL_PARK_BUG_CONTEST", 6, 47, 38, 132 }
|
|
|
|
local HIDDEN_ITEM_COUNT = 87
|
|
|
|
local cacheDir = os.getenv("GOLD_CACHE")
|
|
if not cacheDir then
|
|
cacheDir = (os.getenv("HOME") or "") ..
|
|
"/Library/Application Support/LOVE/gold-dev/gold"
|
|
end
|
|
local mapsFile = loadfile(cacheDir .. "/data/generated/maps.lua")
|
|
if not mapsFile then
|
|
check(true, "no Gold cache (SKIP)")
|
|
S.finish()
|
|
return
|
|
end
|
|
local maps = mapsFile()
|
|
|
|
local rows, decoded, withKey = {}, 0, 0
|
|
for mapId, def in pairs(maps) do
|
|
if type(def) == "table" then
|
|
for _, ev in ipairs(def.bgEvents or {}) do
|
|
if ev.kind == HiddenItems.BGEVENT_ITEM then
|
|
rows[#rows + 1] = true
|
|
if ev.hiddenItem then decoded = decoded + 1 end
|
|
if ev.scriptKey then withKey = withKey + 1 end
|
|
local key = ("%s:%d:%d"):format(mapId, ev.x, ev.y)
|
|
rows[key] = ev
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if decoded == 0 then
|
|
eq(#rows, HIDDEN_ITEM_COUNT, "the cache has every BGEVENT_ITEM row")
|
|
check(true, "cache predates the hiddenitem decode; re-import (SKIP)")
|
|
S.finish()
|
|
return
|
|
end
|
|
|
|
eq(#rows, HIDDEN_ITEM_COUNT, "eighty-seven hidden items in Gold")
|
|
eq(decoded, HIDDEN_ITEM_COUNT, "every one of them decoded its `hiddenitem`")
|
|
eq(withKey, 0,
|
|
"and none of them is queued as a script: a scriptKey here is the pointer " ..
|
|
"walk disassembling the flag word as opcodes")
|
|
|
|
for _, want in ipairs(PINNED) do
|
|
local mapId, x, y, item, event = want[1], want[2], want[3], want[4], want[5]
|
|
local ev = rows[("%s:%d:%d"):format(mapId, x, y)]
|
|
check(ev ~= nil, mapId .. " has a hidden item at " .. x .. "," .. y)
|
|
if ev then
|
|
eq(ev.hiddenItem.item, item, mapId .. " hidden item id")
|
|
eq(ev.hiddenItem.event, event, mapId .. " hidden item flag")
|
|
end
|
|
end
|
|
|
|
do
|
|
local a = rows[("%s:%d:%d"):format(SHARED_FLAG[1], SHARED_FLAG[3], SHARED_FLAG[4])]
|
|
local b = rows[("%s:%d:%d"):format(SHARED_FLAG[2], SHARED_FLAG[3], SHARED_FLAG[4])]
|
|
check(a ~= nil and b ~= nil, "NATIONAL PARK carries its FULL_HEAL on both maps")
|
|
if a and b then
|
|
eq(a.hiddenItem.event, SHARED_FLAG[6], "at the same flag")
|
|
eq(b.hiddenItem.event, SHARED_FLAG[6], "on the contest copy too")
|
|
eq(a.hiddenItem.item, SHARED_FLAG[5], "and the same item")
|
|
end
|
|
end
|
|
|
|
-- Every flag has to land inside wEventFlags and every item inside ItemNames:
|
|
-- a mis-read `dwb` would put the item byte in the low half of the flag word.
|
|
do
|
|
local items = assert(loadfile(cacheDir .. "/data/generated/items.lua"))()
|
|
local byIndex = {}
|
|
for id, def in pairs(items) do
|
|
if type(def) == "table" and def.index then byIndex[def.index] = id end
|
|
end
|
|
local bad = 0
|
|
for key, ev in pairs(rows) do
|
|
if type(key) == "string" then
|
|
if not byIndex[ev.hiddenItem.item] then bad = bad + 1 end
|
|
if ev.hiddenItem.event >= 2048 then bad = bad + 1 end
|
|
end
|
|
end
|
|
eq(bad, 0, "every decoded item is a real item and every flag is in range")
|
|
end
|
|
|
|
-- The point of the extractor half: those 87 pointers were being disassembled,
|
|
-- and the noise is where the port's unknown-opcode rows came from.
|
|
do
|
|
local scripts = assert(loadfile(cacheDir .. "/data/generated/scripts.lua"))()
|
|
local unknown = 0
|
|
for key, list in pairs(scripts) do
|
|
if key ~= "movements" and type(list) == "table" then
|
|
for _, cmd in ipairs(list) do
|
|
if cmd.op == "unknown" then unknown = unknown + 1 end
|
|
end
|
|
end
|
|
end
|
|
check(unknown <= 4,
|
|
("unknown opcode rows are down to %d (64 of the old 68 were hidden items)")
|
|
:format(unknown))
|
|
end
|
|
|
|
S.finish()
|