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 GameVersion = require("src.core.GameVersion") GameVersion.set("emerald") require("src.import.gba.versions").select("emerald") local Dataset = require("src.core.game3.dataset") local cache = Dataset.cache() if not cache:read("data/generated/gba/native/manifest.lua") or not cache:read("data/generated/gba/pokemon/national.lua") then print("emerald_sav_roundtrip_test: skipped (no Emerald cache for identity " .. tostring(os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d") .. ")") os.exit(0) end Dataset.mountExtractRoots() Dataset.hydrate({ data = {} }) local SaveSections = require("src.core.game3.save_sections") local registered = {} local realRegister = SaveSections.register SaveSections.register = function(name, def) registered[name] = true return realRegister(name, def) end for _, mod in ipairs({ "src.core.game3.rse.ribbons", "src.core.game3.rse.dewford_trend", "src.core.game3.weather", "src.core.game3.rse.contest_util", "src.core.game3.rse.lilycove_lady", "src.core.game3.rse.lottery", "src.core.game3.rse.tv", "src.core.game3.rse.pokeblock", "src.core.game3.rse.secret_base", "src.core.game3.rse.rematch", "src.core.game3.rse.old_man", "src.core.game3.rse.berry_trees", "src.core.game3.rse.decoration_inventory" }) do package.loaded[mod] = nil local ok, err = pcall(require, mod) check(ok, "loads " .. mod .. " (" .. tostring(err) .. ")") end SaveSections.register = realRegister for _, name in ipairs({ "rtc", "encryptionKey", "berryTrees", "tv", "decorations" }) do registered[name] = true end local Rse = require("src.save_convert.gen3_port.rse") for name in pairs(registered) do check(Rse.SECTIONS[name] ~= nil, "the cart mapper covers registered save section " .. name) end local Profile = require("src.core.game3.profile") local listed = {} for _, entry in ipairs(Profile.of("emerald").save.sections or {}) do local name = type(entry) == "table" and entry.name or entry listed[name] = true check(Rse.SECTIONS[name] ~= nil, "the cart mapper covers profile save section " .. tostring(name)) end for name in pairs(registered) do if not listed[name] then print("emerald_sav_roundtrip_test: note: section " .. name .. " is registered but not in profiles/emerald/save.lua sections") end end local SaveConvert = require("src.save_convert.SaveConvert") local Schema = require("src.core.game3.save_schema_firered") local Gen3Save = require("src.save_convert.Gen3Save") local E = Gen3Save.forVersion("emerald") local F = require("tests.fixture_data.gen3_saves_emerald") local FR = require("tests.fixture_data.gen3_saves") local function unrle(s) local out = {} for tok in s:gmatch("%S+") do local k, n = tok:match("^([ZF])(%x+)$") if k then out[#out + 1] = string.rep(k == "Z" and "\0" or "\255", tonumber(n, 16)) else out[#out + 1] = (tok:gsub("%x%x", function(h) return string.char(tonumber(h, 16)) end)) end end return table.concat(out) end local function mapExists(mapId) for g = 0, 40 do for n = 0, 120 do if E.mapFor(g, n) == mapId then return cache:read(("data/generated/gba/map_tree/maps/%d_%d/header.json"):format(g, n)) ~= nil end end end return false end for _, name in ipairs({ "em_battle", "em_fresh", "em_doctored" }) do local bytes = unrle(F.images[name]) local o = F.oracle[name] local save, err = SaveConvert.importSav(bytes, "emerald", "emerald") check(save ~= nil, name .. " imports through SaveConvert (" .. tostring(err) .. ")") if save then check(mapExists(save.map), name .. " lands on a cached map " .. tostring(save.map)) eq(save.x, o.pos[1], name .. " x") eq(save.y, o.pos[2], name .. " y") eq(#save.party, #o.party, name .. " party size") check(type(save.rtcSkew) == "number", name .. " RTC anchored to the save's last berry update") local ok, sess = pcall(Schema.fromSaveTable, save) check(ok, name .. " loads into a session (" .. tostring(not ok and sess or "") .. ")") if ok then eq(sess.version, "emerald", name .. " session is emerald") eq(sess.money, o.money, name .. " session money") eq(sess.encryptionKey, o.key, name .. " session keeps the cart key") check(type(sess.berryTrees) == "table", name .. " berry trees restored") if listed.pokeblocks then check(type(sess.pokeblocks) == "table", name .. " pokeblocks restored") end check(type(sess.localTimeOffset) == "table", name .. " local time offset restored") local back = Schema.toSaveTable(sess) local out, xerr = SaveConvert.exportSav(back, "emerald", bytes) check(out ~= nil, name .. " exports through SaveConvert (" .. tostring(xerr) .. ")") if out then local a, b = E.readBlocks(bytes), E.readBlocks(out) for _, blk in ipairs(E.L.BLOCKS) do check(a[blk.key] == b[blk.key], name .. " " .. blk.key .. " byte-identical through the native schema") end for sec = 28, 31 do local off = sec * 0x1000 check(out:sub(off + 1, off + 0x1000) == bytes:sub(off + 1, off + 0x1000), name .. " sector " .. sec .. " kept") end end sess.money = 12345 sess.x = sess.x + 1 local moved = assert(SaveConvert.exportSav(Schema.toSaveTable(sess), "emerald", bytes)) local c = assert(E.decode(moved)) eq(c.money, 12345, name .. " edited money reaches the cart") eq(c.posX, o.pos[1] + 1, name .. " moved position reaches the cart") eq(c.continueGameWarp.x, o.pos[1] + 1, name .. " continue warp follows the player") end end end do local sess = Schema.newGame({ version = "emerald", name = "BRENDAN", gender = 0, trainerIdLower = 4321 }) local save = Schema.toSaveTable(sess) local out, err = SaveConvert.exportSav(save, "emerald", nil) check(out ~= nil, "a native new game exports with no cart template (" .. tostring(err) .. ")") if out then local c = assert(E.decode(out)) eq(c.name, "BRENDAN", "fresh export player name") eq(c.trainerId, 4321, "fresh export trainer id") eq(c.money, 3000, "fresh export money") eq(E.mapFor(c.location.group, c.location.num), "EM_INSIDE_OF_TRUCK", "fresh export starts in the truck") eq(#c.pcItems, 1, "fresh export PC potion") eq(c.encryptionKey, 0, "fresh export key 0 like NewGameInitData") eq(c.easyChatBattle.start[1], 8 * 512 + 15, "fresh export battle-start words (EC_WORD_ARE)") eq(c.easyChatBattle.lost[5], 3 * 512 + 48, "fresh export battle-lost words (EC_WORD_LOST)") local again = assert(SaveConvert.importSav(out, "emerald", "emerald")) eq(again.name, "BRENDAN", "fresh export re-imports") end end do local _, msg = SaveConvert.importSav(unrle(FR.images.fr_rich_game), "emerald", "emerald") eq(msg, E.MSG.frlg, "a FireRed cart dropped on Emerald says so") local _, frMsg = SaveConvert.importSav(unrle(F.images.em_fresh), "firered", "firered") eq(frMsg, Gen3Save.MSG.notFrlg, "an Emerald cart dropped on FireRed says so") local _, gen1 = SaveConvert.mainChecksumValid(unrle(F.images.em_fresh), "red") eq(gen1, "That is an Emerald (Game Boy Advance) save, not a save for this game.", "an Emerald cart dropped on Red") end do local SaveData = require("src.core.SaveData") local save = assert(SaveConvert.importSav(unrle(F.images.em_battle), "emerald", "emerald")) GameVersion.set("firered") local _, meta = SaveData.slotSummary(save) GameVersion.set("emerald") eq(meta and meta.badges, 8, "launcher counts Emerald badges (0x867-0x86E) while FireRed is active") eq(meta and meta.timeText, "135:33", "launcher play time from the cart") end T.finish("emerald_sav_roundtrip")