Files
gen1recomp/tests/engine/gen1_save_identity_roundtrip.lua

172 lines
7.0 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = love or require("tests.love_stub")
local GenSave = require("src.save_convert.GenSave")
local SaveConvert = require("src.save_convert.SaveConvert")
local SaveData = require("src.core.SaveData")
local SaveSerializer = require("src.core.SaveSerializer")
local GameVersion = require("src.core.GameVersion")
local SaveFileIO = require("src.import.SaveFileIO")
local SyncEngine = require("src.sync.SyncEngine")
if not loadfile("data/generated/pokemon.lua") then
print("gen1_save_identity_roundtrip skipped (needs data/generated/ for the Gen1 save codec)")
os.exit(0)
end
local data = assert(SaveConvert.loadData("red"))
local stampMapWindow = loadfile("tests/fixture_data/map_window.lua")()
for mapId in pairs(data.maps) do stampMapWindow(data, mapId) end
local realFS = love.filesystem
local function memfs(files)
return {
files = files,
write = function(path, content) files[path] = content return true end,
read = function(path) return files[path] end,
remove = function(path) files[path] = nil return true end,
getInfo = function(path)
if files[path] then return { type = "file" } end
local prefix = path .. "/"
for key in pairs(files) do
if key:sub(1, #prefix) == prefix then return { type = "directory" } end
end
return nil
end,
createDirectory = function() return true end,
getSaveDirectory = function() return "/fake/save" end,
}
end
local function fresh()
local files = {}
love.filesystem = memfs(files)
SaveData.resetSlotState()
GameVersion.set("red")
return files
end
local bit = require("bit")
local O = GenSave.OFFSETS
local function rawChecksum(bytes, from, to)
local sum = 0
for i = from, to - 1 do sum = bit.band(sum + bytes:byte(i + 1), 0xFF) end
return bit.band(bit.bnot(sum), 0xFF)
end
local function mainChecksumValid(bytes)
return rawChecksum(bytes, O.checksumStart, O.checksumEnd)
== bytes:byte(O.mainChecksum + 1)
end
local TAG = O.identityTag
local TAG_LEN = #GenSave.IDENTITY_MAGIC + GenSave.IDENTITY_ID_LENGTH
local ID = "0123456789abcdef0123456789abcdef"
do
T.eq(TAG, 0x2000, "the tag sits at the start of SRAM bank 1")
T.check(TAG + TAG_LEN <= O.checksumStart,
"and ends before the checksummed sGameData window opens")
local save = SaveData.newGame({ playerName = "ASH", rivalName = "GARY" })
save.money = 4321
save.meta = SaveData.buildMeta(nil, { mods = {} })
local plain = GenSave.encode(save, data, nil)
save.meta.playthroughId = ID
local tagged = GenSave.encode(save, data, nil)
T.eq(#tagged, GenSave.SAVE_SIZE, "a tagged export is still 32768 bytes")
T.check(tagged:sub(1, TAG) == plain:sub(1, TAG),
"bank 0 and the bytes before the tag match a tagless export")
T.check(tagged:sub(TAG + TAG_LEN + 1) == plain:sub(TAG + TAG_LEN + 1),
"and so does every byte after it")
T.eq(tagged:sub(TAG + 1, TAG + TAG_LEN), GenSave.IDENTITY_MAGIC .. ID,
"the tag is the magic followed by the 32 hex id")
T.eq(plain:sub(TAG + 1, TAG + TAG_LEN), string.rep("\0", TAG_LEN),
"a tagless export leaves the bank 1 padding zero")
T.check(mainChecksumValid(tagged), "the main-data checksum still verifies")
T.eq(tagged:byte(O.mainChecksum + 1), plain:byte(O.mainChecksum + 1),
"and is the same byte as the tagless export's")
T.check(GenSave.mainChecksumValid(tagged), "the codec's own check agrees")
T.eq(GenSave.readIdentity(tagged), ID, "the tag reads back")
T.eq(GenSave.readIdentity(plain), nil, "a tagless image has no identity")
local decoded = GenSave.decode(tagged, data)
T.eq(decoded.meta.playthroughId, ID, "decode carries the id into the meta")
T.eq(decoded.player.name, "ASH", "beside the progress it always read")
T.eq(GenSave.decode(plain, data).meta.playthroughId, nil,
"and a tagless image decodes with none")
T.check(GenSave.encode(decoded, data, nil) == tagged,
"a template round trip is byte-identical, tag included")
decoded.meta.playthroughId = nil
T.check(GenSave.encode(decoded, data, nil) == tagged,
"a save that lost its id leaves the template's bytes verbatim")
save.meta.playthroughId = "abc"
T.check(GenSave.encode(save, data, nil) == plain,
"an id that is not 32 hex characters is never written")
local junk = tagged:sub(1, TAG) .. GenSave.IDENTITY_MAGIC .. ("zz"):rep(16)
.. tagged:sub(TAG + TAG_LEN + 1)
T.eq(GenSave.readIdentity(junk), nil, "a non-hex id after the magic is ignored")
local wrongMagic = tagged:sub(1, TAG) .. "G1RX" .. tagged:sub(TAG + 5)
T.eq(GenSave.readIdentity(wrongMagic), nil, "and so is the wrong magic")
T.eq(GenSave.decode(wrongMagic, data).meta.playthroughId, nil,
"leaving the decoded meta identity-free")
end
do
local files = fresh()
local slot = SaveData.createSlot("red")
SaveData.setActiveSlot("red", slot)
local save = SaveData.newGame({ playerName = "ASH", rivalName = "GARY" })
save.money = 4321
save.meta = SaveData.buildMeta({}, save.meta, os.time() - 60)
T.check(SaveData.save(save), "device one plays and saves")
T.eq(SaveData.load("red").meta.playthroughId, nil,
"with no identity minted yet")
local ok, path = SaveFileIO.exportActiveSlot("red")
T.eq(ok, true, "device one exports: " .. tostring(path))
local out = files["exports/red/gen1recomp-red-" .. slot .. ".sav"]
T.check(type(out) == "string" and #out == GenSave.SAVE_SIZE,
"the export is a 32 KB image")
local id = GenSave.readIdentity(out)
T.check(type(id) == "string" and #id == 32 and id:match("^%x+$") ~= nil,
"an export mints and tags the slot's identity")
T.eq(SaveData.loadOptions().playthroughIds.red[slot], id,
"the same identity device one syncs under")
local mine = SyncEngine.defaultSaves().list()
T.eq(mine[1] and mine[1].playthroughId, id, "as its sync listing confirms")
T.check(mainChecksumValid(out), "and the image still verifies")
local onDisk = SaveSerializer.decode(files["saves/red/" .. slot .. ".lua"])
T.eq(onDisk.meta.playthroughId, nil, "exporting rewrote no progress bytes")
local files2 = fresh()
local imported, slot2 = SaveFileIO.importToSlot(out, "red")
T.eq(imported, true, "device two imports the export: " .. tostring(slot2))
local loaded = SaveData.load("red")
T.eq(loaded and loaded.meta.playthroughId, id,
"and the imported slot carries the same identity")
T.eq(loaded and loaded.player.name, "ASH", "with the progress")
local theirs = SyncEngine.defaultSaves().list()
T.eq(#theirs, 1, "device two lists the import for sync")
T.eq(theirs[1] and theirs[1].playthroughId, id,
"under the same key, so both devices meet on the server")
T.eq(SaveData.loadOptions().playthroughIds.red[slot2], id,
"and the slot is mapped so a download replaces it in place")
T.eq(SaveFileIO.exportActiveSlot("red"), true, "device two exports again")
T.eq(GenSave.readIdentity(files2["exports/red/gen1recomp-red-" .. slot2 .. ".sav"]),
id, "keeping the identity through the second hop")
end
love.filesystem = realFS
SaveData.resetSlotState()
GameVersion.set("red")
T.finish("gen1_save_identity_roundtrip")