Files
gen1recomp/tests/emerald_secret_base_test.lua
T
bryanthaboi 690f0cd9a1 TOUCHDOWN
2026-09-27 16:07:31 -04:00

213 lines
10 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local ROM_PATH = os.getenv("POKEPORT_EMERALD_ROM") or "../pokeemerald/pokeemerald.gba"
local f = io.open(ROM_PATH, "rb")
if not f then
print("emerald_secret_base_test: skipped (no ROM at " .. ROM_PATH .. ")")
os.exit(0)
end
local data = f:read("*a")
f:close()
local GameVersion = require("src.core.GameVersion")
GameVersion.set("emerald")
local imports = {
info = function(_, id) return { id = id, size = #data, md5 = "emerald", file = "memory" } end,
read = function(_, _, off, len) return data:sub(off + 1, off + len) end,
}
local rom = assert(require("src.import.gba.rom").open(imports, "emerald"))
local store = {}
local cache = {
write = function(_, rel, bytes) store[rel] = bytes; return true end,
read = function(_, rel) return store[rel] end,
exists = function(_, rel) return store[rel] ~= nil end,
}
local ROOT = "data/generated/gba"
local Plans = require("src.import.gba.plans.registry")
local plan = require("src.import.gba.plans.rse.sb")
for _, step in ipairs(plan.tasks[1].steps) do
local mod = require(Plans.moduleFor(step.name))
local ok, err = pcall(mod.run, rom, cache, { cacheRoot = ROOT })
check(ok, step.name .. " runs: " .. tostring(err))
for _, rel in ipairs(mod.REQUIRED or {}) do
check(store[ROOT .. "/" .. rel] ~= nil, step.name .. " wrote " .. rel)
end
end
check(pcall(require("src.import.gba.rse.decorations_extract").run, rom, cache, { cacheRoot = ROOT }), "decorations table")
local function load(rel)
return assert(loadstring(assert(store[ROOT .. "/" .. rel], rel), "@" .. rel))()
end
local man = load("secret_base/manifest.lua")
local Decor = require("src.core.game3.rse.decoration")
Decor.install(man, load("decorations/decorations.lua"))
-- pokeemerald/src/secret_base.c:111
eq(#man.entrances + 1, 24, "24 secret base layouts")
eq(man.entrances[13].x, 8, "YELLOW_CAVE2 computer x")
eq(man.entrances[13].y, 7, "YELLOW_CAVE2 computer y")
eq(man.entrances[16].warpId, 0, "entrance warp 0")
eq(#man.entranceMetatiles, 7, "7 entrance metatile pairs")
eq(#man.ownerGfx, 10, "10 owner gfx ids")
eq(man.decorCount, 121, "121 decoration rows")
local C = require("src.core.game3.constants").of("emerald")
local function decor(name) return C:require("decorations", name) end
local function mt(name) return C:require("metatile_labels", name) end
-- pokeemerald/src/data/decoration/tiles.h:3
eq(man.decorTiles[decor("DECOR_SMALL_DESK")][1] + Decor.PRIMARY, mt("METATILE_SecretBase_SmallDesk"), "SMALL DESK metatile")
eq(#man.decorTiles[decor("DECOR_HEAVY_DESK")], 6, "HEAVY DESK is 3x2")
eq(#man.decorTiles[decor("DECOR_RED_TENT")], 9, "RED TENT is 3x3")
eq(man.decorTiles[decor("DECOR_PIKA_POSTER")][1] + Decor.PRIMARY, mt("METATILE_SecretBase_PikaPoster_Left"),
"PIKA POSTER left metatile")
eq(man.decorTiles[decor("DECOR_PICHU_DOLL")][1], C:require("event_objects", "OBJ_EVENT_GFX_PICHU_DOLL"),
"PICHU DOLL is an object gfx")
check(Decor.isSprite(decor("DECOR_PICHU_DOLL")), "dolls are sprites")
eq(Decor.info(decor("DECOR_PIKA_POSTER")).permission, Decor.PERM.NA_WALL, "posters hang on the wall")
-- pokeemerald/src/secret_base.c:98
local function pairOf(closed, open)
for _, e in ipairs(man.entranceMetatiles) do
if e.closed == mt(closed) then return e.open == mt(open) end
end
return false
end
check(pairOf("METATILE_Fortree_SecretBase_Shrub", "METATILE_Fortree_SecretBase_ShrubOpen"), "shrub entrance pair")
check(pairOf("METATILE_General_YellowCaveIndent", "METATILE_General_YellowCaveOpen"), "yellow cave entrance pair")
local MB = require("src.core.game3.mb")
local function grid(cells, w, h)
local g = { set = {}, objects = {} }
function g.behavior(x, y)
local c = cells[y * w + x + 1]
return c and MB.id(c) or nil
end
function g.metatile(x, y) local s = g.set[y * 64 + x] return s and s.mid or 0 end
function g.metatileId(name) return mt(name) end
function g.setMetatile(x, y, mid, imp, elev) g.set[y * 64 + x] = { mid = mid, imp = imp, elev = elev } end
function g.objectAt(x, y) return g.objects[y * 64 + x] end
function g.size() return w, h end
function g.original() return 0 end
function g.restoreMetatile(x, y, elev) g.set[y * 64 + x] = { mid = 0, restored = true, elev = elev } end
return g
end
local W, H = 6, 5
local cells = {}
for y = 0, H - 1 do
for x = 0, W - 1 do
cells[y * W + x + 1] = y == 0 and "SECRET_BASE_NORTH_WALL" or "NORMAL"
end
end
cells[3 * W + 2 + 1] = "SECRET_BASE_TRAINER_SPOT"
cells[3 * W + 4 + 1] = "HOLDS_SMALL_DECORATION"
cells[3 * W + 5 + 1] = "SECRET_BASE_HOLE"
local g = grid(cells, W, H)
local place = { x = 1, y = 2, initialX = 0, initialY = 4 }
-- pokeemerald/src/decoration.c:1528
check(Decor.canPlace(g, place, decor("DECOR_SMALL_DESK")), "desk on floor")
check(not Decor.canPlace(g, { x = 2, y = 3, initialX = 0, initialY = 4 }, decor("DECOR_SMALL_DESK")),
"desk not on a trainer spot")
check(not Decor.canPlace(g, { x = 1, y = 0, initialX = 0, initialY = 4 }, decor("DECOR_SMALL_DESK")), "desk not on wall")
check(Decor.canPlace(g, { x = 1, y = 0, initialX = 0, initialY = 4 }, decor("DECOR_PIKA_POSTER")), "poster on wall")
check(not Decor.canPlace(g, place, decor("DECOR_PIKA_POSTER")), "poster not on floor")
check(Decor.canPlace(g, { x = 4, y = 3, initialX = 0, initialY = 4 }, decor("DECOR_PICHU_DOLL")), "doll on a holder")
check(not Decor.canPlace(g, place, decor("DECOR_PICHU_DOLL")), "doll not on bare floor")
check(Decor.canPlace(g, { x = 5, y = 3, initialX = 0, initialY = 4 }, decor("DECOR_SOLID_BOARD")), "board over hole")
check(not Decor.canPlace(g, { x = 5, y = 3, initialX = 0, initialY = 4 }, decor("DECOR_SMALL_DESK")), "desk not in hole")
g.objects[2 * 64 + 1] = 3
check(not Decor.canPlace(g, place, decor("DECOR_SMALL_DESK")), "an object blocks floor placement")
g.objects[2 * 64 + 1] = 0
check(Decor.canPlace(g, place, decor("DECOR_SMALL_DESK")), "the player's own cell does not block a floor decoration")
g.objects[2 * 64 + 1] = nil
local heavy = decor("DECOR_HEAVY_DESK")
check(Decor.canPlace(g, { x = 1, y = 2, initialX = 5, initialY = 4 }, heavy), "heavy desk fits on open floor")
check(Decor.layerType(man.decorTiles[heavy][2]) ~= 0, "heavy desk top row is a covered layer")
check(not Decor.canPlace(g, { x = 1, y = 2, initialX = 2, initialY = 1 }, heavy),
"a covered tile cannot sit where the player stood at the PC")
-- pokeemerald/src/decoration.c:1208
Decor.showOnMap(g, 1, 2, decor("DECOR_SMALL_DESK"))
eq(g.set[2 * 64 + 1].mid, mt("METATILE_SecretBase_SmallDesk"), "desk metatile written")
Decor.showOnMap(g, 1, 0, decor("DECOR_LONG_POSTER"))
eq(g.set[0 * 64 + 1].mid, man.decorTiles[decor("DECOR_LONG_POSTER")][1] + Decor.PRIMARY, "wall poster keeps base metatile")
local stand = decor("DECOR_STAND")
Decor.showOnMap(g, 1, 3, stand)
local topRow = g.set[2 * 64 + 1]
check(topRow and topRow.elev == man.standElevations[1], "stand writes its elevation table")
-- pokeemerald/src/decoration.c:1685
local sess = { version = "emerald", decorationInventory = nil }
local SB = require("src.core.game3.rse.secret_base")
local bases = SB.bases(sess)
eq(#bases, 20, "20 secret base records")
local DecorInv = require("src.core.game3.rse.decoration_inventory")
DecorInv.add(decor("DECOR_SMALL_DESK"), sess)
DecorInv.add(decor("DECOR_SMALL_DESK"), sess)
DecorInv.add(decor("DECOR_PICHU_DOLL"), sess)
local ctx = Decor.context(sess, false)
eq(Decor.record(ctx, decor("DECOR_SMALL_DESK"), 3, 4), 1, "first decoration slot")
eq(bases[1].decorationPositions[1], 0x34, "position packed x<<4|y")
local inBase = Decor.inUse(sess, Decor.CAT.DESK)
check(inBase[1] and not inBase[2], "one of two desks in use")
check(not Decor.isInPc(sess, Decor.CAT.DESK, 1) and Decor.isInPc(sess, Decor.CAT.DESK, 2), "second desk still in the PC")
local room = Decor.context(sess, true)
Decor.record(room, decor("DECOR_PICHU_DOLL"), 5, 2)
local _, inRoom = Decor.inUse(sess, Decor.CAT.DOLL)
check(inRoom[1], "bedroom doll marked in use")
Decor.toss(sess, Decor.CAT.DESK, 2)
eq(DecorInv.countInCategory(Decor.CAT.DESK, sess), 1, "toss removes the stored desk")
-- pokeemerald/src/decoration.c:2570
local g2 = grid(cells, W, H)
local marked = Decor.markForRemoval(g2, ctx, 3, 4)
eq(#marked, 1, "cursor on the desk marks it")
check(#Decor.markForRemoval(g2, ctx, 2, 4) == 0, "cursor next to it marks nothing")
Decor.clearNonSprites(g2, ctx, marked)
check(g2.set[4 * 64 + 3] and g2.set[4 * 64 + 3].restored and g2.set[4 * 64 + 3].elev == 3, "put away restores the map")
eq(ctx.items[1], 0, "put away clears the slot")
-- pokeemerald/src/secret_base.c:1132
bases[2].trainerId = { 7, 0, 0, 0 }
bases[2].gender = 1
eq(SB.ownerType(1, sess), 7, "owner type = id % 5 + gender * 5")
eq(SB.typeAt(MB.id("SECRET_BASE_SPOT_SHRUB")), SB.TYPE.SHRUB, "shrub spot")
eq(SB.typeAt(MB.id("SECRET_BASE_SPOT_TREE_RIGHT_OPEN")), SB.TYPE.TREE, "open tree spot")
check(SB.isOpenDoor(MB.id("SECRET_BASE_SPOT_YELLOW_CAVE_OPEN")), "open yellow cave is a door")
check(not SB.isOpenDoor(MB.id("SECRET_BASE_SPOT_YELLOW_CAVE")), "closed indent is not a door")
check(SB.isCave(MB.id("SECRET_BASE_SPOT_RED_CAVE")) and not SB.isCave(MB.id("SECRET_BASE_SPOT_RED_CAVE_OPEN")),
"Secret Power only on closed caves")
-- pokeemerald/src/secret_base.c:381
local events = { { type = "secret_base", secretBaseId = 131, x = 2, y = 1 }, { type = "secret_base", secretBaseId = 141, x = 4, y = 1 } }
local g3 = grid(cells, W, H)
g3.set[1 * 64 + 2] = { mid = mt("METATILE_General_YellowCaveIndent") }
g3.set[1 * 64 + 4] = { mid = mt("METATILE_General_YellowCaveIndent") }
bases[1].secretBaseId = 131
SB.setOccupiedEntrances(events, g3, sess)
eq(g3.set[1 * 64 + 2].mid, mt("METATILE_General_YellowCaveOpen"), "the player's entrance opens on load")
check(g3.set[1 * 64 + 2].imp == true, "open entrance stays impassable")
eq(g3.set[1 * 64 + 4].mid, mt("METATILE_General_YellowCaveIndent"), "an empty spot stays closed")
check(SB.toggleEntrance(4, 1, g3) and g3.set[1 * 64 + 4].mid == mt("METATILE_General_YellowCaveOpen"), "toggle opens")
check(SB.toggleEntrance(4, 1, g3) and g3.set[1 * 64 + 4].mid == mt("METATILE_General_YellowCaveIndent"), "toggle closes")
-- pokeemerald/src/secret_base.c:1804
local SaveSections = require("src.core.game3.save_sections")
local out = {}
local def = SaveSections.register("secretBases", SaveSections.fields({ "secretBases", "playerRoomDecorationPositions" }))
def.export(sess, out)
local back = {}
def.restore(out, back)
eq(back.secretBases[1].secretBaseId, 131, "base id survives a save round trip")
eq(back.playerRoomDecorationPositions[1], 0x52, "bedroom doll position survives")
T.finish()