-- home/map.asm ObjectEvent, the generic "Object event." line an object with no -- script of its own says. It is a ROM0 body, so every map names it with the -- same sub-$4000 `dw` and the extractor's map walk (which only ever queues -- banked pointers) used to drop all 44 of them on the floor: the objects kept -- a scriptKey in their own map's script bank, where nothing was disassembled, -- and talking to one did nothing at all. -- -- ROM-free: `luajit tests/gen2_object_event_test.lua`. The cache section at -- the bottom SKIPs when no Gold cache is present, and again when the cache -- predates the extractor change (a re-import is what fills it in). package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 object event") local check, eq = S.check, S.eq love = require("tests.love_stub") local Vm = require("src.script.gen2.Vm") local Events = require("src.world.gen2.Events") local Opcodes = require("src.script.gen2.Opcodes") -- ---- the key a ROM0 body gets --------------------------------------------- -- pokegold.sym: 00:2812 ObjectEvent, 00:2815 ObjectEventText. Bank 0 is not a -- banked script bank, it is the home bank every other bank can see, so the one -- key is shared by every map rather than repeated per map bank. local OBJECT_EVENT = Opcodes.key(0, 0x2812) local OBJECT_EVENT_TEXT = Opcodes.key(0, 0x2815) eq(OBJECT_EVENT, "00:2812", "ObjectEvent keys at bank 0") eq(OBJECT_EVENT_TEXT, "00:2815", "and so does the text behind it") -- ---- the VM runs a bank 0 key like any other ------------------------------ -- home/map.asm: `ObjectEvent: jumptextfaceplayer ObjectEventText`, so the body -- is one command, it turns the object toward the player, and it ends there. do local shown, faced = {}, {} local scripts = { generation = 2, [OBJECT_EVENT] = { { op = "jumptextfaceplayer", text = OBJECT_EVENT_TEXT }, }, } local texts = { [OBJECT_EVENT_TEXT] = "Object event." } local vm = Vm.new(scripts, texts, Events.new(), { showText = function(body, onDone) shown[#shown + 1] = body onDone() end, facePlayer = function() faced[#faced + 1] = true end, }) check(vm:start(OBJECT_EVENT), "a bank 0 script key starts") for _ = 1, 4 do vm:update() end check(not vm:running(), "jumptextfaceplayer is a terminator") eq(shown[1], "Object event.", "the shared line prints") check(#faced >= 1, "and the object faces the player first") -- The regression itself: the same pointer keyed at the MAP's bank is not in -- the table, and Vm:start answers false rather than saying anything. check(not vm:start(Opcodes.key(0x42, 0x2812)), "the same address keyed at a map bank resolves to nothing") end -- ---- the extractor half ---------------------------------------------------- -- Read as source: the walk itself needs the ROM, so what is checked here is -- that the ROM0 arm is still wired, both as a seed from the symbol and as the -- normalisation an object_event pointer goes through. do local f = assert(io.open("src/import/RomExtractorGen2.lua", "r")) local src = f:read("*a") f:close() check(src:find("local function enqueueHome(address)", 1, true) ~= nil, "the extractor has a ROM0 queue arm") check(src:find("self.symbols.ObjectEvent", 1, true) ~= nil, "and seeds ObjectEvent from the symbol table") check(src:find("obj.scriptKey = enqueueHome(obj.script)", 1, true) ~= nil, "an object_event pointer goes through it before its map bank") -- enqueue proper must keep refusing bank 0: a farscall or a map pointer that -- decodes to ROM0 is noise, and only the two call sites above know better. check(src:find("-- Scripts live in banked ROM, not ROM0.", 1, true) ~= nil, "the banked queue still refuses ROM0 pointers") end -- ---- the 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 mapsFile = io.open(cache .. "/data/generated/maps.lua", "r") local scriptsFile = io.open(cache .. "/data/generated/scripts.lua", "r") if not mapsFile or not scriptsFile then if mapsFile then mapsFile:close() end if scriptsFile then scriptsFile:close() end check(true, "gold cache absent : unit checks only (SKIP cache facts)") S.finish() return end mapsFile:close(); scriptsFile:close() local maps = assert(loadfile(cache .. "/data/generated/maps.lua"))() local scripts = assert(loadfile(cache .. "/data/generated/scripts.lua"))() local texts = assert(loadfile(cache .. "/data/generated/text.lua"))() -- Every object that names the shared line, and every one that names it the old -- way (its own map's bank plus the ROM0 address). local shared, stale = 0, 0 for _, def in pairs(maps) do if type(def) == "table" then for _, obj in ipairs(def.objects or {}) do if obj.scriptKey == OBJECT_EVENT then shared = shared + 1 elseif type(obj.scriptKey) == "string" and obj.scriptKey:find(":2812", 1, true) then stale = stale + 1 end end end end if shared == 0 and stale > 0 then check(true, ("cache predates the ROM0 seed (%d stale rows) : re-import " .. "for the shared line (SKIP)"):format(stale)) S.finish() return end -- 44 object_events across pokegold/maps/ point at ObjectEvent; the one that is -- OBJECTTYPE_TRAINER (BurnedTower1F's rival) reads the pointer as a `trainer` -- struct instead, so the count of SCRIPT rows is one short of that. check(shared >= 40, ("%d objects share the ObjectEvent line"):format(shared)) eq(stale, 0, "no object is left pointing into its own bank at $2812") local body = scripts[OBJECT_EVENT] check(type(body) == "table" and #body >= 1, "the shared body was disassembled") if type(body) == "table" and body[1] then eq(body[1].op, "jumptextfaceplayer", "and it is the one jumptextfaceplayer") local line = body[1].text and texts[body[1].text] check(type(line) == "string" and line:find("Object event", 1, true) ~= nil, "whose text decoded through TX_FAR into _ObjectEventText") end S.finish()