Files
gen1recomp/tests/save_compat/gen2_object_context_test.lua
T
2026-10-01 17:30:52 -04:00

156 lines
9.2 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
local Objects = require("src.save_convert.Gen2ObjectContext")
local Context = require("src.save_convert.Gen2MapContext")
local Codec = require("src.save_convert.Gen2Save")
local Layout = require("src.save_convert.Gen2Layout")
local function fixture(version)
local objects = {
{ spriteId = 7, x = 4, y = 4, movement = 7, radius = { x = 1, y = 2 }, palette = 4, sight = 3 },
{ spriteId = 8, x = 10, y = 4, movement = 8 },
{ spriteId = 7, x = 4, y = 10, movement = 9 },
}
local blocks = {}; for i = 1, 100 do blocks[i] = 1 end
local data = require("tests.fixtures.save.gen2_sprite_metadata")({ pokemon = {}, moves = {}, items = {},
maps = { TEST = { group = 21, map = 14, width = 10, height = 10, blocks = blocks,
objectEventsAddr = 0x4100, environment = "GATE", objects = objects } } }, version)
for _, id in ipairs({ 7, 8 }) do data.constants.spriteContext.rows[id] = { type = 1, length = 12, palette = 5 } end
return data
end
local function state(x, y)
return { player = { name = "TESTER" }, position = { map = "TEST", mapGroup = 21, mapNumber = 14, x = x, y = y }, events = {} }
end
local function edit(bytes, at, value)
return bytes:sub(1, at) .. string.char(value) .. bytes:sub(at + 2)
end
local function seal(bytes, L)
local sum = 0
for i = L.sGameData + 1, L.sGameDataEnd do sum = (sum + bytes:byte(i)) % 65536 end
bytes = edit(bytes, L.sChecksum, sum % 256)
return edit(bytes, L.sChecksum + 1, math.floor(sum / 256))
end
for _, version in ipairs({ "gold", "silver", "crystal" }) do
local data, save = fixture(version), state(4, 4)
local def, O = data.maps.TEST, Context.offsetsFor(version)
local masks = assert(Context.objectMaskBytes("TEST", def, version, save))
local rows = assert(Objects.rows(data, version, def, save, 4, 4, masks, O.firstObjectSlot))
T.check(rows[1] and rows[2] and not rows[3], version .. " viewport includes right edge and excludes lower edge")
local row = rows[1]
T.eq(row[2], O.firstObjectSlot, version .. " native map-object index")
T.eq(row[3], 12, version .. " NPC sprite follows player tiles")
T.eq(row[4], 7, version .. " movement preserved")
T.eq(row[7], 4, version .. " explicit palette overrides sprite palette")
T.eq(row[9], 4, version .. " facing comes from movement row")
T.eq(row[12], 1, version .. " standing action")
T.eq(row[14], 255, version .. " facing initialized STANDING")
T.eq(row[17], 8, version .. " map X biased")
T.eq(row[21], 8, version .. " initial X biased")
T.eq(row[19], 0, version .. " last X stays clear until reset step")
T.eq(row[23], 0x32, version .. " native radius increments both nibbles")
T.eq(row[24], 64, version .. " native sprite X")
T.eq(row[33], 3, version .. " sight range preserved")
masks[O.firstObjectSlot + 1] = 255
local hidden = assert(Objects.rows(data, version, def, save, 4, 4, masks, O.firstObjectSlot))
T.eq(hidden[1], nil, version .. " masked actor is not instantiated")
local stale = fixture(version); stale.constants.spriteContext = nil
local refused, why = Objects.rows(stale, version, def, save, 4, 4,
assert(Context.objectMaskBytes("TEST", def, version, save)), O.firstObjectSlot)
T.check(refused == nil and why:find("re-import the ROM", 1, true), version .. " visible actors require sprite metadata")
local base = assert(Codec.encode(save, version, nil, data))
local L = version == "crystal" and Layout.crystal or Layout.goldSilver
local at = O.mapObjects + O.firstObjectSlot * 16
base = seal(edit(base, at + 3, 9), L)
local decoded = assert(Codec.decode(base, version, data))
decoded.position.x = 12
local moved = assert(Codec.encode(decoded, version, base, data))
T.eq(moved:byte(at + 4), 9, version .. " same-map move preserves scripted object X")
T.eq(moved:byte(at + 1), 255, version .. " actor outside new viewport loses active struct")
T.eq(moved:byte(at + 17), 1, version .. " entering actor gets first free struct")
T.eq(moved:byte(O.objectStructs + 41), 8, version .. " same-map move instantiates newly visible NPC")
T.eq(moved:byte(O.objectStructs + 40 + 24), 32, version .. " newly visible NPC has new relative sprite X")
decoded.position.x = 7
moved = assert(Codec.encode(decoded, version, base, data))
T.eq(moved:byte(at + 1), 1, version .. " carried scripted NPC remains visible")
T.eq(moved:byte(O.objectStructs + 40 + 17), 9, version .. " carried scripted X initializes actor")
T.eq(moved:byte(O.objectStructs + 40 + 24), 32, version .. " actor sprite X follows moved player")
end
local Json = require("src.link.Json")
local function json(path)
local f = assert(io.open(path, "rb")); local value = assert(Json.decode(f:read("*a"))); f:close(); return value
end
local spriteFixtures = json("tests/fixtures/save/gen2_sprite_context.json")
for index, version in ipairs({ "gold", "silver", "crystal" }) do
local data = fixture(version)
local constants = json("tools/rom_manifest_" .. version .. ".json").constants
constants.spriteContext = spriteFixtures.builds[index].metadata
data.constants, data.sprites = constants, {}
for _, name in ipairs(constants.spriteOrder) do data.sprites[name] = {} end
data.maps = { PLAYERS_HOUSE_2F = { group = 24, map = 7, width = 4, height = 3,
blocks = { 4, 1, 3, 2, 5, 6, 5, 5, 5, 5, 7, 5 }, objectEventsAddr = 0x4100, objects = {} } }
for i, coord in ipairs({ { 4, 2 }, { 4, 4 }, { 5, 4 }, { 0, 1 } }) do
data.maps.PLAYERS_HOUSE_2F.objects[i] = { spriteId = 239 + i, x = coord[1], y = coord[2],
movement = i == 4 and 32 or 1, eventFlag = 1856 + i }
end
local save = { player = { name = "TESTER" }, position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 } }
local O, L = Context.offsetsFor(version), Codec.layoutFor(version)
local image = assert(Codec.encode(save, version, nil, data))
T.eq(save.events, nil, version .. " bedroom export does not mutate the caller's events")
for i = 1, 4 do
T.eq(image:byte(O.objectMasks + O.firstObjectSlot + i), 255, version .. " empty bedroom object " .. i .. " hidden")
T.eq(image:byte(O.mapObjects + (O.firstObjectSlot + i - 1) * 16 + 1), 255,
version .. " empty decoration has no active actor " .. i)
end
T.eq(image:byte(O.objectStructs + 41), 0, version .. " default bedroom has no Chris clones")
local decoded = assert(Codec.decode(image, version, data))
T.eq(assert(Codec.encode(decoded, version, image, data)), image, version .. " default bedroom masks round-trip exactly")
decoded.decorations = { bed = 2, poster = 16, console = 21, leftOrnament = 30, rightOrnament = 51, bigDoll = 26 }
local furnished = assert(Codec.encode(decoded, version, image, data))
for i, raw in ipairs({ 0x5C, 0x8E, 0x5E, 0x33 }) do
T.eq(furnished:byte(L.wVariableSprites + i), raw, version .. " placed decoration variable sprite " .. i)
T.eq(furnished:byte(O.objectMasks + O.firstObjectSlot + i), 0, version .. " placed decoration visible " .. i)
T.eq(furnished:byte(O.mapObjects + (O.firstObjectSlot + i - 1) * 16 + 1), i,
version .. " placed decoration has ordered actor " .. i)
end
decoded = assert(Codec.decode(furnished, version, data))
T.eq(assert(Codec.encode(decoded, version, furnished, data)), furnished, version .. " furnished bedroom round-trip exactly")
decoded.decorations.console = 22
local changed = assert(Codec.encode(decoded, version, furnished, data))
T.eq(changed:byte(L.wVariableSprites + 1), 0x5B, version .. " visible console replacement refreshes sprite")
decoded = assert(Codec.decode(changed, version, data)); decoded.decorations.leftOrnament = 0
local hidden = assert(Codec.encode(decoded, version, changed, data))
T.eq(hidden:byte(O.objectMasks + O.firstObjectSlot + 2), 255, version .. " put-away doll stays hidden on Continue")
T.eq(hidden:byte(O.mapObjects + (O.firstObjectSlot + 1) * 16 + 1), 255, version .. " put-away doll removes actor")
end
local traces = os.getenv("GEN2_OBJECT_TRACE")
if traces then
local Json = require("src.link.Json")
local function read(path)
local f = assert(io.open(path, "rb")); local text = f:read("*a"); f:close(); return text
end
local native = assert(Json.decode(read(traces)))
local fixture = assert(Json.decode(read("tests/fixtures/save/gen2_sprite_context.json")))
for index, group in ipairs(native) do
local version = group.edition
local constants = assert(Json.decode(read("tools/rom_manifest_" .. version .. ".json"))).constants
constants.spriteContext = fixture.builds[index].metadata
local sprites = {}; for _, name in ipairs(constants.spriteOrder) do sprites[name] = {} end
local data = { constants = constants, sprites = sprites }
for _, sample in ipairs(group.constructors) do
local object = { spriteId = 47, x = 2, y = 3, movement = sample.movement,
radius = { x = sample.radius % 16, y = math.floor(sample.radius / 16) }, palette = sample.palette, sight = 5 }
local def = { environment = "INDOOR", objects = { object } }
local masks = {}; for i = 1, 16 do masks[i] = 0 end
local actual = assert(Objects.rows(data, version, def, {}, 4, 5, masks,
Context.offsetsFor(version).firstObjectSlot))[1]
T.same(actual, sample.expected, version .. " exact ROM constructor movement " .. object.movement)
end
end
end
T.finish("gen2 object context")