mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
200 lines
8.6 KiB
Lua
200 lines
8.6 KiB
Lua
-- Gen1 save codec (src/save_convert/GenSave.lua) for the save.flags names whose
|
|
-- vanilla home is NOT wEventFlags: exporting a slot and importing it back used
|
|
-- to drop them, so the Saffron gate guards were thirsty again (#396). Offsets
|
|
-- are re-derived here byte by byte from ram/wram.asm rather than read out of
|
|
-- GenSave.OFFSETS, and the bit numbers come from constants/ram_constants.asm
|
|
-- (BIT_GOT_OLD_ROD 3, BIT_GAVE_SAFFRON_GUARDS_DRINK 6, BIT_GOT_LAPRAS 0,
|
|
-- BIT_STARTED_ELITE_4 1). The trade bits are wWhichTrade, which
|
|
-- engine/events/in_game_trades.asm uses to index wCompletedInGameTradeFlags,
|
|
-- so they are checked against the 1-based rows the port's scripts pass.
|
|
-- luajit tests/engine/save_convert_extra_flags.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
local bit = require("bit")
|
|
local GenSave = require("src.save_convert.GenSave")
|
|
local SaveData = require("src.core.SaveData")
|
|
|
|
-- the codec crosswalks need the real dataset; CI has no ROM
|
|
local loadPokemon = loadfile("data/generated/pokemon.lua")
|
|
if not loadPokemon then
|
|
print("save_convert_extra_flags skipped (needs data/generated/ for the Gen1 save codec)")
|
|
os.exit(0)
|
|
end
|
|
|
|
GenSave.setCharmap(loadfile("src/save_convert/data/charmap.lua")())
|
|
local events = loadfile("src/save_convert/data/event_flags.lua")()
|
|
local data = {
|
|
pokemon = loadPokemon(),
|
|
moves = loadfile("data/generated/moves.lua")(),
|
|
items = loadfile("data/generated/items.lua")(),
|
|
maps = loadfile("data/generated/maps.lua")(),
|
|
eventFlags = events,
|
|
}
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- offsets, walked forward from wTownVisitedFlag over ram/wram.asm's own
|
|
-- declaration run: 2 wTownVisitedFlag, 2 wSafariSteps, wFossilItem,
|
|
-- wFossilMon, ds 2, wEnemyMonOrTrainerClass, wPlayerJumpingYScreenCoordsIndex,
|
|
-- wRivalStarter, ds 1, wPlayerStarter, wBoulderSpriteIndex, wLastBlackoutMap,
|
|
-- wDestinationMap, wUnusedPlayerDataByte, wTileInFrontOfBoulder...,
|
|
-- wDungeonWarpDestinationMap, wWhichDungeonWarp, wUnusedCardKeyGateID, ds 8
|
|
-- ------------------------------------------------------------------
|
|
|
|
local OFF = GenSave.OFFSETS
|
|
local TOWN_VISITED = OFF.townVisited
|
|
local WRAM = {
|
|
statusFlags1 = TOWN_VISITED + 29,
|
|
statusFlags4 = TOWN_VISITED + 35, -- +30 ds 1, wBeatGymFlags, ds 1, 2/3
|
|
elite4Flags = TOWN_VISITED + 41, -- +36 ds 1, 5, ds 1, 6, 7
|
|
tradeFlags = TOWN_VISITED + 44, -- +42 ds 1, wMovementFlags
|
|
eventFlags = TOWN_VISITED + 60, -- the run's own end, pinned already
|
|
}
|
|
|
|
eq(WRAM.eventFlags, OFF.eventFlags,
|
|
"the wram walk lands on wEventFlags where the codec already pins it")
|
|
eq(OFF.statusFlags1, WRAM.statusFlags1, "wStatusFlags1 is wTownVisitedFlag + 29")
|
|
eq(OFF.statusFlags4, WRAM.statusFlags4, "wStatusFlags4 is wTownVisitedFlag + 35")
|
|
eq(OFF.elite4Flags, WRAM.elite4Flags, "wElite4Flags is wTownVisitedFlag + 41")
|
|
eq(OFF.tradeFlags, WRAM.tradeFlags, "wCompletedInGameTradeFlags is wTownVisitedFlag + 44")
|
|
|
|
-- independent flag_array read (byte = index / 8, bit = index % 8), so nothing
|
|
-- below trusts the writer it is checking
|
|
local function flagGet(bytes, base, index)
|
|
local byte = bytes:byte(base + math.floor(index / 8) + 1)
|
|
return bit.band(bit.rshift(byte, index % 8), 1) == 1
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- the names under test, and the proof they cannot ride wEventFlags
|
|
-- ------------------------------------------------------------------
|
|
|
|
local EXTRA = {
|
|
{ "EVENT_GOT_OLD_ROD", "statusFlags1", 3 },
|
|
{ "EVENT_GOT_GOOD_ROD", "statusFlags1", 4 },
|
|
{ "EVENT_GOT_SUPER_ROD", "statusFlags1", 5 },
|
|
{ "EVENT_GAVE_GUARDS_DRINK", "statusFlags1", 6 },
|
|
{ "EVENT_GOT_LAPRAS", "statusFlags4", 0 },
|
|
{ "EVENT_STARTED_ELITE_4", "elite4Flags", 1 },
|
|
}
|
|
|
|
local named = 0
|
|
for _ in pairs(events.byName) do named = named + 1 end
|
|
eq(named, 507, "event_flags.lua carries every EVENT_* constant and no more")
|
|
for _, row in ipairs(EXTRA) do
|
|
eq(events.byName[row[1]], nil, row[1] .. " has no wEventFlags bit to ride")
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- trade rows as the port's scripts actually pass them: `{ "trade", N, FLAG }`
|
|
-- with N the 1-based data/events/trades.asm row, so the bit is N - 1
|
|
-- ------------------------------------------------------------------
|
|
|
|
local TRADE_ROWS = {}
|
|
local scripts = io.popen("ls data/scripts/*.lua")
|
|
for path in scripts:lines() do
|
|
local f = io.open(path, "r")
|
|
local src = f:read("*a")
|
|
f:close()
|
|
for index, flag in src:gmatch('"trade",%s*(%d+),%s*"(EVENT_[A-Z0-9_]+)"') do
|
|
TRADE_ROWS[#TRADE_ROWS + 1] = { flag = flag, index = tonumber(index) }
|
|
end
|
|
end
|
|
scripts:close()
|
|
check(#TRADE_ROWS == 7, "the nine-row trade table's seven reachable trades are scripted")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- round trips
|
|
-- ------------------------------------------------------------------
|
|
|
|
local function seedSave()
|
|
local save = SaveData.newGame({ playerName = "RED", rivalName = "BLUE" })
|
|
save.party = { {
|
|
species = "SQUIRTLE", level = 6, exp = 200,
|
|
dvs = { hp = 1, attack = 2, defense = 3, speed = 4, special = 5 },
|
|
statExp = { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 },
|
|
stats = { hp = 22, attack = 12, defense = 13, speed = 11, special = 12 },
|
|
hp = 22,
|
|
moves = { { id = "TACKLE", pp = 35, ppUps = 0 } },
|
|
nickname = "SQ", ot = "RED", otId = save.player.id, catchRate = 45,
|
|
} }
|
|
return save
|
|
end
|
|
|
|
local set = seedSave()
|
|
for _, row in ipairs(EXTRA) do set.flags[row[1]] = true end
|
|
for _, row in ipairs(TRADE_ROWS) do set.flags[row.flag] = true end
|
|
set.flags.EVENT_RECEIVED_BIKE_VOUCHER = true
|
|
|
|
local setBytes = GenSave.encode(set, data, nil)
|
|
eq(#setBytes, GenSave.SAVE_SIZE, "the export is a 32768-byte save")
|
|
|
|
for _, row in ipairs(EXTRA) do
|
|
check(flagGet(setBytes, WRAM[row[2]], row[3]),
|
|
row[1] .. " reaches the .sav as its " .. row[2] .. " bit " .. row[3])
|
|
end
|
|
for _, row in ipairs(TRADE_ROWS) do
|
|
check(flagGet(setBytes, WRAM.tradeFlags, row.index - 1),
|
|
row.flag .. " reaches wCompletedInGameTradeFlags bit " .. (row.index - 1))
|
|
end
|
|
-- the port spells bit 337 EVENT_RECEIVED_BIKE_VOUCHER; vanilla calls it
|
|
-- EVENT_GOT_BIKE_VOUCHER (constants/event_constants.asm)
|
|
eq(events.byName.EVENT_GOT_BIKE_VOUCHER, 337, "the bike voucher is event bit 337")
|
|
check(flagGet(setBytes, WRAM.eventFlags, 337),
|
|
"EVENT_RECEIVED_BIKE_VOUCHER reaches the real bike voucher event bit")
|
|
|
|
-- no spill into the neighbouring bits of a shared byte
|
|
eq(bit.band(setBytes:byte(WRAM.statusFlags1 + 1), 0x87), 0,
|
|
"wStatusFlags1 keeps its other bits (0-2, 7) clear")
|
|
eq(bit.band(setBytes:byte(WRAM.tradeFlags + 1), 0x04), 0,
|
|
"the unused CHIKUCHIKU trade bit stays clear")
|
|
|
|
local back = GenSave.decode(setBytes, data)
|
|
eq(#(back.warnings or {}), 0, "the export decodes with no warnings")
|
|
local reflags = back.flags
|
|
for _, row in ipairs(EXTRA) do
|
|
eq(reflags[row[1]], true, row[1] .. " survives export -> import")
|
|
end
|
|
for _, row in ipairs(TRADE_ROWS) do
|
|
eq(reflags[row.flag], true, row.flag .. " survives export -> import")
|
|
end
|
|
eq(reflags.EVENT_RECEIVED_BIKE_VOUCHER, true,
|
|
"EVENT_RECEIVED_BIKE_VOUCHER comes back under the port's own spelling")
|
|
eq(reflags.EVENT_GOT_BIKE_VOUCHER, true, "and under the vanilla spelling too")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- the port's save is the only authority for these bits, so a save that does
|
|
-- NOT hold one must clear it out of the template rather than inherit it
|
|
-- ------------------------------------------------------------------
|
|
|
|
local clear = seedSave()
|
|
local clearBytes = GenSave.encode(clear, data, setBytes)
|
|
for _, row in ipairs(EXTRA) do
|
|
check(not flagGet(clearBytes, WRAM[row[2]], row[3]),
|
|
row[1] .. " is cleared, not inherited from the template")
|
|
end
|
|
for _, row in ipairs(TRADE_ROWS) do
|
|
check(not flagGet(clearBytes, WRAM.tradeFlags, row.index - 1),
|
|
row.flag .. " is cleared, not inherited from the template")
|
|
end
|
|
local reclear = GenSave.decode(clearBytes, data)
|
|
eq(reclear.flags.EVENT_GAVE_GUARDS_DRINK, nil,
|
|
"a save that never watered the guards imports back thirsty")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- cross-file pin: the four Saffron gates read this exact spelling
|
|
-- (data/scripts/story2.lua, pokered scripts/Route5Gate.asm and its twins)
|
|
-- ------------------------------------------------------------------
|
|
|
|
local sf = io.open("data/scripts/story2.lua", "r")
|
|
local story2 = sf:read("*a")
|
|
sf:close()
|
|
check(story2:find("flags.EVENT_GAVE_GUARDS_DRINK", 1, true) ~= nil,
|
|
"the gate scripts still spell the drink flag EVENT_GAVE_GUARDS_DRINK")
|
|
|
|
T.finish("save_convert_extra_flags")
|