mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
3069b2e2a9
* new launcher and save converts and pipeline * fixing bugs
234 lines
9.3 KiB
Lua
234 lines
9.3 KiB
Lua
-- Launcher save Import/Export glue (src/import/SaveFileIO.lua): the end-to-end
|
|
-- importToSlot -> listSlots roundtrip and the exportActiveSlot output-byte
|
|
-- sanity check, driven love-free through the same in-memory filesystem stub
|
|
-- tests/engine/save_slots.lua uses. A synthetic 32KB SRAM image is built via
|
|
-- GenSave.encode (no real save checked in); a fixture-gated case exercises the
|
|
-- real .sav when POKEPORT_SAV_FIXTURE points at one.
|
|
-- luajit tests/engine/save_file_io_tests.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 GenSave = require("src.save_convert.GenSave")
|
|
local SaveConvert = require("src.save_convert.SaveConvert")
|
|
local SaveData = require("src.core.SaveData")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local SaveFileIO = require("src.import.SaveFileIO")
|
|
|
|
local realFS = love.filesystem
|
|
|
|
-- A love.filesystem stub keyed by full path, extended past save_slots' memfs
|
|
-- with the export surface SaveFileIO reaches for (createDirectory /
|
|
-- getSaveDirectory). A directory key is implied by any file under it.
|
|
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
|
|
|
|
-- ---- crosswalk data + synthetic 32KB save (built the way the codec tests do)
|
|
|
|
GenSave.setCharmap(loadfile("src/save_convert/data/charmap.lua")())
|
|
local data = {
|
|
pokemon = loadfile("data/generated/pokemon.lua")(),
|
|
moves = loadfile("data/generated/moves.lua")(),
|
|
items = loadfile("data/generated/items.lua")(),
|
|
maps = loadfile("data/generated/maps.lua")(),
|
|
eventFlags = loadfile("src/save_convert/data/event_flags.lua")(),
|
|
}
|
|
|
|
-- independent checksum re-derivation (complement of the additive byte sum) so
|
|
-- the export sanity check does not trust the encoder that wrote it
|
|
local bit = require("bit")
|
|
local OFF = 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, OFF.checksumStart, OFF.checksumEnd)
|
|
== bytes:byte(OFF.mainChecksum + 1)
|
|
end
|
|
|
|
local function syntheticSave(name)
|
|
local seed = SaveData.newGame({ playerName = name, rivalName = "BLUE" })
|
|
seed.money = 4321
|
|
seed.inventory = { POTION = 2, POKE_BALL = 7, BOULDERBADGE = 1 }
|
|
seed.bagOrder = { "POTION", "POKE_BALL" }
|
|
seed.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, status = nil,
|
|
moves = { { id = "TACKLE", pp = 35, ppUps = 0 } },
|
|
nickname = "SQ", ot = name, otId = seed.player.id, catchRate = 45,
|
|
} }
|
|
return GenSave.encode(seed, data, nil)
|
|
end
|
|
|
|
-- ---------------------------------------------- importToSlot -> listSlots
|
|
|
|
do
|
|
fresh()
|
|
local bytes = syntheticSave("IMP")
|
|
eq(#bytes, GenSave.SAVE_SIZE, "the synthetic save is 32768 bytes")
|
|
|
|
local ok, slotId = SaveFileIO.importToSlot(bytes, "red")
|
|
eq(ok, true, "importToSlot succeeds on a valid 32KB save")
|
|
eq(slotId, "slot1", "the first import registers slot1")
|
|
|
|
local slots = SaveData.listSlots("red")
|
|
eq(#slots, 1, "the imported save shows up as exactly one slot")
|
|
eq(slots[1].id, "slot1", "the listed slot is slot1")
|
|
eq(slots[1].exists, true, "the imported slot reports a save present")
|
|
eq(slots[1].name, "IMP", "the imported slot surfaces the decoded player name")
|
|
eq(SaveData.activeSlot("red"), "slot1", "the imported slot is made active")
|
|
|
|
-- the slot loads cleanly (meta re-stamped from gen1_import to the numeric
|
|
-- format, so runMigrations does not choke)
|
|
local loaded = SaveData.load("red")
|
|
check(loaded ~= nil, "the imported slot loads back")
|
|
eq(loaded and loaded.player.name, "IMP", "loaded save keeps the player name")
|
|
eq(loaded and loaded.money, 4321, "loaded save keeps the money")
|
|
eq(loaded and #loaded.party, 1, "loaded save keeps the party")
|
|
|
|
-- a second import allocates a fresh slot and makes it active
|
|
local ok2, slot2 = SaveFileIO.importToSlot(syntheticSave("TWO"), "red")
|
|
eq(ok2, true, "a second import succeeds")
|
|
eq(slot2, "slot2", "the second import allocates slot2")
|
|
eq(#SaveData.listSlots("red"), 2, "both imported slots are listed")
|
|
eq(SaveData.activeSlot("red"), "slot2", "the newest import becomes active")
|
|
end
|
|
|
|
-- ---------------------------------------------- exportActiveSlot byte sanity
|
|
|
|
do
|
|
local files = fresh()
|
|
SaveFileIO.importToSlot(syntheticSave("EXP"), "red")
|
|
|
|
local ok, path = SaveFileIO.exportActiveSlot("red")
|
|
eq(ok, true, "exportActiveSlot succeeds for an active slot with a save")
|
|
eq(path, "/fake/save/exports/gen1recomp-red-slot1.sav",
|
|
"the export path is absolute and names the version + slot")
|
|
|
|
local outBytes = files["exports/gen1recomp-red-slot1.sav"]
|
|
check(outBytes ~= nil, "the export file lands in the save-dir exports/ folder")
|
|
eq(outBytes and #outBytes, GenSave.SAVE_SIZE, "the export is exactly 32768 bytes")
|
|
check(outBytes and mainChecksumValid(outBytes),
|
|
"the export carries a valid main-data checksum")
|
|
|
|
-- the export re-imports to an equivalent save
|
|
local re = SaveConvert.importSav(outBytes, "red")
|
|
check(re ~= nil, "the export re-imports through SaveConvert")
|
|
eq(re and re.player and re.player.name, "EXP", "the export round-trips the player name")
|
|
eq(re and re.party[1] and re.party[1].species, "SQUIRTLE",
|
|
"the export round-trips the party")
|
|
eq(re and re.inventory and re.inventory.BOULDERBADGE, 1,
|
|
"the export round-trips a badge")
|
|
end
|
|
|
|
-- ---------------------------------------------- failure UX (never raises)
|
|
|
|
do
|
|
fresh()
|
|
-- wrong size via a DroppedFile-shaped source (100 bytes)
|
|
local shortFile = {
|
|
_bytes = string.rep("\0", 100),
|
|
open = function() return true end,
|
|
getSize = function(self) return #self._bytes end,
|
|
read = function(self) return self._bytes end,
|
|
close = function() return true end,
|
|
}
|
|
local ok, err = SaveFileIO.importToSlot(shortFile, "red")
|
|
eq(ok, false, "a wrong-size save is rejected, not imported")
|
|
check(type(err) == "string" and err:find("32", 1, true) ~= nil,
|
|
"the wrong-size error names the required size")
|
|
eq(#SaveData.listSlots("red"), 0, "a rejected import creates no slot")
|
|
|
|
-- bad checksum: flip a modeled byte in an otherwise valid image
|
|
local good = syntheticSave("BAD")
|
|
local corrupt = good:sub(1, OFF.money)
|
|
.. string.char((good:byte(OFF.money + 1) + 1) % 256)
|
|
.. good:sub(OFF.money + 2)
|
|
local okc, errc = SaveFileIO.importToSlot(corrupt, "red")
|
|
eq(okc, false, "a bad-checksum save is rejected")
|
|
check(type(errc) == "string" and errc:find("checksum", 1, true) ~= nil,
|
|
"the bad-checksum error mentions the checksum")
|
|
eq(#SaveData.listSlots("red"), 0, "a rejected checksum creates no slot")
|
|
|
|
-- export with nothing to export
|
|
local oke, erre = SaveFileIO.exportActiveSlot("red")
|
|
eq(oke, false, "exportActiveSlot fails cleanly when there is no save")
|
|
check(type(erre) == "string", "the empty-export failure carries a message")
|
|
end
|
|
|
|
-- ---------------------------------------------- fixture-gated real save
|
|
|
|
do
|
|
local fixturePath = os.getenv("POKEPORT_SAV_FIXTURE")
|
|
local fixtureBytes
|
|
if fixturePath then
|
|
local ff = io.open(fixturePath, "rb")
|
|
if ff then
|
|
fixtureBytes = ff:read("*a")
|
|
ff:close()
|
|
if #fixtureBytes ~= GenSave.SAVE_SIZE then fixtureBytes = nil end
|
|
end
|
|
end
|
|
if not fixtureBytes then
|
|
print("save_file_io fixture case skipped (set POKEPORT_SAV_FIXTURE to a 32KB .sav)")
|
|
else
|
|
local files = fresh()
|
|
local ok, slotId = SaveFileIO.importToSlot(fixtureBytes, "red")
|
|
eq(ok, true, "fixture: a real .sav imports to a slot")
|
|
check(slotId ~= nil, "fixture: the import returns a slot id")
|
|
|
|
local slots = SaveData.listSlots("red")
|
|
eq(#slots, 1, "fixture: the real save shows as one slot")
|
|
check(slots[1].exists and type(slots[1].name) == "string" and #slots[1].name > 0,
|
|
"fixture: the imported slot has a non-empty player name")
|
|
|
|
local loaded = SaveData.load("red")
|
|
check(loaded ~= nil and #loaded.party >= 1 and #loaded.party <= 6,
|
|
"fixture: the imported slot loads with a 1..6 party")
|
|
|
|
local eok, path = SaveFileIO.exportActiveSlot("red")
|
|
eq(eok, true, "fixture: the imported real save exports")
|
|
local rel = path:gsub("^/fake/save/", "")
|
|
local outBytes = files[rel]
|
|
eq(outBytes and #outBytes, GenSave.SAVE_SIZE, "fixture: the export is 32768 bytes")
|
|
check(outBytes and mainChecksumValid(outBytes),
|
|
"fixture: the export has a valid main-data checksum")
|
|
end
|
|
end
|
|
|
|
love.filesystem = realFS
|
|
|
|
T.finish("save_file_io")
|