mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
421 lines
19 KiB
Lua
421 lines
19 KiB
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 Gen3Save = require("src.save_convert.Gen3Save")
|
|
local Layout = require("src.save_convert.Gen3Layout")
|
|
local Rse = require("src.save_convert.gen3_port.rse")
|
|
local F = require("tests.fixture_data.gen3_saves_emerald")
|
|
local FR = require("tests.fixture_data.gen3_saves")
|
|
|
|
local E = Gen3Save.forVersion("emerald")
|
|
local L = E.L
|
|
|
|
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 IMAGES, FR_IMAGES = {}, {}
|
|
for name, r in pairs(F.images) do IMAGES[name] = unrle(r) end
|
|
for name, r in pairs(FR.images) do FR_IMAGES[name] = unrle(r) end
|
|
|
|
local function show(v)
|
|
if type(v) ~= "table" then return tostring(v) end
|
|
local parts = {}
|
|
for k, x in pairs(v) do parts[#parts + 1] = tostring(k) .. "=" .. show(x) end
|
|
table.sort(parts)
|
|
return "{" .. table.concat(parts, ",") .. "}"
|
|
end
|
|
|
|
local function same_eq(a, b, label)
|
|
check(Rse.same(a, b), label .. " (" .. show(a) .. " vs " .. show(b) .. ")")
|
|
end
|
|
|
|
local function hex(s)
|
|
return (s:gsub(".", function(c) return string.format("%02x", c:byte()) end))
|
|
end
|
|
|
|
local function unhex(h)
|
|
return (h:gsub("%x%x", function(x) return string.char(tonumber(x, 16)) end))
|
|
end
|
|
|
|
local function pairsList(list)
|
|
local out = {}
|
|
for _, it in ipairs(list) do out[#out + 1] = { it.id, it.qty } end
|
|
return out
|
|
end
|
|
|
|
local function plain(t)
|
|
local out = {}
|
|
for k, v in pairs(t or {}) do out[tonumber(k) or k] = v end
|
|
return out
|
|
end
|
|
|
|
-- pokeemerald/src/save.c:57
|
|
do
|
|
local sizes = {}
|
|
for id = 0, 13 do sizes[id + 1] = L.CHUNK_SIZES[id] end
|
|
same_eq(sizes, { 0xF2C, 0xF80, 0xF80, 0xF80, 0xF08, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0x7D0 },
|
|
"emerald chunk sizes")
|
|
eq(L.BLOCKS[1].size, 0xF2C, "SaveBlock2 size")
|
|
eq(L.BLOCKS[2].size, 0x3D88, "SaveBlock1 size")
|
|
eq(L.KEY_OFF, 0xAC, "emerald key lives at SB2 0xAC")
|
|
eq(L.PC_ITEMS.count, 50, "PC holds 50 items")
|
|
eq(L.DAYCARE.offspringKind, "u32", "daycare offspring personality is u32")
|
|
eq(L.DAYCARE.stepCounter, 0x11C, "daycare step counter after the u32")
|
|
eq(L.LINK_BATTLE_RECORDS.block, "sb1", "link battle records moved to SB1")
|
|
eq(L.FAME_CHECKER, nil, "no fame checker on emerald")
|
|
eq(L.TRAINER_TOWER, nil, "no trainer tower on emerald")
|
|
eq(L.ROUTE5_DAYCARE, nil, "no route 5 daycare on emerald")
|
|
eq(L.RSE.sb2.battlePoints, 0xEB8, "battle points at SB2 0xEB8")
|
|
local fr = Layout.forVersion("firered")
|
|
eq(fr, Layout, "firered keeps the top-level layout table")
|
|
eq(Layout.forVersion("leafgreen"), Layout, "leafgreen shares it")
|
|
eq(Layout.forVersion("emerald"), L, "emerald resolves its own family")
|
|
eq(fr.CHUNK_SIZES[0], 0xF24, "firered SB2 chunk unchanged")
|
|
eq(fr.CHUNK_SIZES[4], 0xEE8, "firered SB1 tail chunk unchanged")
|
|
eq(fr.KEY_OFF, 0xF20, "firered key stays at 0xF20")
|
|
eq(Gen3Save.forVersion("firered"), Gen3Save, "firered codec is the module")
|
|
eq(Gen3Save.forVersion("emerald"), E, "emerald codec is cached")
|
|
end
|
|
|
|
for name, img in pairs(IMAGES) do eq(Gen3Save.sniff(img), "emerald", name .. " sniffs as emerald") end
|
|
for name, img in pairs(FR_IMAGES) do eq(Gen3Save.sniff(img), "frlg", name .. " sniffs as frlg") end
|
|
|
|
local function checkMon(label, m, o)
|
|
eq(m.personality, o.pid, label .. " personality")
|
|
eq(m.otIdRaw, o.otid, label .. " OT id")
|
|
eq(m.species, o.species, label .. " species")
|
|
eq(m.heldItem, o.heldItem, label .. " held item")
|
|
eq(m.exp, o.exp, label .. " exp")
|
|
same_eq(m.moves, o.moves, label .. " moves")
|
|
eq(m.ribbons, o.ribbons, label .. " ribbons")
|
|
eq(hex(m.nicknameRaw), o.nickRaw, label .. " nickname bytes")
|
|
eq(hex(m.otNameRaw), o.otRaw, label .. " OT name bytes")
|
|
check(m.checksumOk, label .. " checksum")
|
|
end
|
|
|
|
local function checkOracle(name, c, blocks, o, loose)
|
|
eq(E.decodeString(unhex(o.nameRaw), 0, 8), c.name, name .. " player name")
|
|
eq(c.gender, o.gender, name .. " gender")
|
|
eq(c.trainerId, o.tid, name .. " TID")
|
|
eq(c.secretId, o.sid, name .. " SID")
|
|
if not loose then
|
|
same_eq({ c.playHours, c.playMinutes, c.playSeconds }, o.playTime, name .. " play time")
|
|
eq(c.encryptionKey, o.key, name .. " encryption key")
|
|
eq(c.specialSaveWarpFlags, o.warpFlags, name .. " save warp flags")
|
|
end
|
|
eq(c.money, o.money, name .. " money")
|
|
eq(c.coins, o.coins, name .. " coins")
|
|
eq(c.berryPowder, o.berryPowder, name .. " berry powder")
|
|
eq(c.registeredItem, o.registeredItem, name .. " registered item")
|
|
eq(c.optionsWord, o.options, name .. " options word")
|
|
eq(c.dexNationalMagic, o.nationalMagic, name .. " national magic at SB2 0x1A")
|
|
same_eq({ c.posX, c.posY }, o.pos, name .. " position")
|
|
local w = c.location
|
|
same_eq({ w.group, w.num, w.warpId, w.x, w.y }, o.location, name .. " location")
|
|
w = c.lastHealLocation
|
|
same_eq({ w.group, w.num, w.warpId, w.x, w.y }, o.heal, name .. " last heal location")
|
|
eq(c.weather, o.weather, name .. " saved weather")
|
|
if not loose then eq(c.weatherCycleStage, o.weatherCycleStage, name .. " weather cycle stage") end
|
|
eq(#c.party, #o.party, name .. " party count")
|
|
for i, pm in ipairs(o.party) do
|
|
local m = c.party[i]
|
|
checkMon(name .. " party " .. i, m, pm)
|
|
eq(m.level, pm.level, name .. " party " .. i .. " level")
|
|
eq(m.hp, pm.hp, name .. " party " .. i .. " HP")
|
|
eq(m.maxHp, pm.maxHp, name .. " party " .. i .. " max HP")
|
|
eq(m.mail, pm.mail, name .. " party " .. i .. " mail slot")
|
|
end
|
|
local count, first = 0, nil
|
|
for b = 1, 14 do
|
|
for sl = 1, 30 do
|
|
local m = c.storage.boxes[b].mons[sl]
|
|
if m then
|
|
count = count + 1
|
|
first = first or { m = m, slot = (b - 1) * 30 + sl - 1 }
|
|
end
|
|
end
|
|
end
|
|
eq(count, o.boxCount, name .. " boxed mons")
|
|
eq(c.storage.currentBox, o.currentBox, name .. " current box")
|
|
if o.firstBoxMon then
|
|
eq(first and first.slot, o.firstBoxMon.slot, name .. " first boxed slot")
|
|
if first then checkMon(name .. " first boxed mon", first.m, o.firstBoxMon) end
|
|
end
|
|
for key, list in pairs(o.pockets) do same_eq(pairsList(c.pockets[key]), list, name .. " pocket " .. key) end
|
|
same_eq(pairsList(c.pcItems), o.pcItems, name .. " PC items")
|
|
same_eq(c.flags, o.flags, name .. " flags (300 bytes)")
|
|
if not loose then same_eq(c.vars, plain(o.vars), name .. " vars") end
|
|
same_eq(c.gameStats, plain(o.gameStats), name .. " game stats (xor key)")
|
|
same_eq(c.dexOwned, o.dexOwned, name .. " dex owned")
|
|
same_eq(c.dexSeen, o.dexSeen, name .. " dex seen")
|
|
eq(c.daycare.offspringPersonality, o.daycare.offspringPersonality, name .. " daycare offspring personality (u32)")
|
|
eq(c.daycare.stepCounter, o.daycare.stepCounter, name .. " daycare step counter")
|
|
for i = 1, 2 do
|
|
eq(c.daycare.mons[i] and c.daycare.mons[i].species or 0, o.daycare.species[i], name .. " daycare mon " .. i)
|
|
eq(c.daycare.steps[i], o.daycare.steps[i], name .. " daycare steps " .. i)
|
|
end
|
|
local mailItems = {}
|
|
for i, m in ipairs(c.mail) do mailItems[i] = m.itemId end
|
|
same_eq(mailItems, o.mailItems, name .. " mail items")
|
|
eq(c.roamer.species, o.roamer.species, name .. " roamer species")
|
|
eq(c.roamer.level, o.roamer.level, name .. " roamer level")
|
|
eq(c.roamer.active, o.roamer.active, name .. " roamer active")
|
|
eq(c.roamer.personality, o.roamer.personality, name .. " roamer personality")
|
|
eq(c.roamer.ivs, o.roamer.ivs, name .. " roamer IVs")
|
|
|
|
local s = Rse.readSections(E, blocks, {})
|
|
local trees = {}
|
|
for id, t in pairs(s.berryTrees) do trees[id] = { t.berry, t.stage, t.minutesUntilNextStage, t.berryYield } end
|
|
same_eq(trees, plain(o.berryTrees), name .. " berry trees")
|
|
local blocksList = {}
|
|
for _, b in ipairs(s.pokeblocks) do
|
|
if b.color + b.spicy + b.dry + b.sweet + b.bitter + b.sour + b.feel > 0 then
|
|
blocksList[#blocksList + 1] = { b.color, b.spicy, b.dry, b.sweet, b.bitter, b.sour, b.feel }
|
|
end
|
|
end
|
|
same_eq(blocksList, o.pokeblocks, name .. " pokeblocks")
|
|
if not loose then
|
|
local t = s.localTimeOffset
|
|
same_eq({ t.days, t.hours, t.minutes, t.seconds }, o.localTimeOffset, name .. " local time offset")
|
|
t = s.lastBerryTreeUpdate
|
|
same_eq({ t.days, t.hours, t.minutes, t.seconds }, o.lastBerryTreeUpdate, name .. " last berry tree update")
|
|
end
|
|
eq(s.trainerRematchStepCounter, o.trainerRematchStepCounter, name .. " rematch step counter")
|
|
same_eq(s.trainerRematches, plain(o.rematches), name .. " rematch table")
|
|
same_eq(s.giftRibbons, o.giftRibbons, name .. " gift ribbons")
|
|
local inv = {}
|
|
for cat = 0, 7 do for _, d in ipairs(s.decorationInventory[cat]) do inv[#inv + 1] = d end end
|
|
same_eq(inv, o.decorInventory, name .. " decoration inventory")
|
|
same_eq(s.playerRoomDecorations, o.playerRoomDecorations, name .. " player room decorations")
|
|
eq(s.outbreakPokemonSpecies, o.outbreakSpecies, name .. " outbreak species")
|
|
local words = {}
|
|
for i, t in ipairs(s.dewfordTrends) do words[i] = t.words[1] end
|
|
if not loose then same_eq(words, o.dewfordWords, name .. " dewford trend words") end
|
|
local species = {}
|
|
for i, w in ipairs(s.contestWinners) do species[i] = w.species end
|
|
same_eq(species, o.contestWinnerSpecies, name .. " contest winner species")
|
|
eq(s.secretBases[1].secretBaseId, o.secretBaseId, name .. " own secret base id")
|
|
local lr = s.linkBattleRecords[1]
|
|
local raw = unhex(o.linkRecord0)
|
|
if raw:byte(1) ~= 0xFF and raw:byte(1) ~= 0 then
|
|
eq(lr and lr.name, E.decodeString(raw, 0, 8), name .. " link record name")
|
|
eq(lr and lr.wins, raw:byte(11) + raw:byte(12) * 256, name .. " link record wins")
|
|
else
|
|
eq(lr, nil, name .. " no link record")
|
|
end
|
|
eq(E.u16(blocks.sb2, L.RSE.sb2.battlePoints), o.battlePoints, name .. " battle points")
|
|
end
|
|
|
|
local decoded = {}
|
|
for _, name in ipairs({ "em_battle", "em_fresh", "em_doctored" }) do
|
|
local c, blocks = E.decode(IMAGES[name])
|
|
check(c ~= nil, name .. " decodes (" .. tostring(blocks) .. ")")
|
|
if c then
|
|
decoded[name] = { c = c, blocks = blocks }
|
|
eq(c.olderSlot, false, name .. " newest slot valid")
|
|
checkOracle(name, c, blocks, F.oracle[name])
|
|
end
|
|
end
|
|
|
|
do
|
|
local c = decoded.em_doctored.c
|
|
eq(c.encryptionKey, 0, "doctored save carries key 0")
|
|
eq(c.daycare.offspringPersonality, 0xDEADBEEF, "doctored daycare egg personality keeps the high half")
|
|
eq(#c.pcItems, 50, "doctored PC holds 50 items")
|
|
eq(c.pcItems[50].qty, 50, "PC slot 50 quantity is not xor'd")
|
|
eq(c.mail[7].itemId, 121, "PC mailbox mail decodes")
|
|
eq(c.mail[7].playerName, "RIVAL", "mail author name")
|
|
eq(c.mail[7].trainerIdRaw, 0x12345678, "mail trainer id")
|
|
eq(c.roamer.cool, 1, "roamer contest stats decode")
|
|
check(decoded.em_fresh.c.encryptionKey ~= 0, "a fresh save is re-keyed by the truck map load")
|
|
end
|
|
|
|
do
|
|
local img = IMAGES.em_battle
|
|
local newest = decoded.em_battle.blocks.slot
|
|
local base = (newest * 14 + 3) * 0x1000 + 100
|
|
local broken = img:sub(1, base) .. string.char((img:byte(base + 1) + 1) % 256) .. img:sub(base + 2)
|
|
local c, blocks = E.decode(broken)
|
|
check(c ~= nil, "bundled slot decodes once the newest slot is damaged")
|
|
if c then
|
|
eq(c.olderSlot, true, "the older slot is the bundled all-shiny save")
|
|
checkOracle("em_shiny", c, blocks, F.oracle.em_shiny, true)
|
|
eq(F.meta.em_shiny.ramAfterContinue, true, "the shiny oracle came from RAM after CONTINUE")
|
|
end
|
|
end
|
|
|
|
local function rebuild(bytes, counter)
|
|
local c, blocks = E.decode(bytes)
|
|
return E.buildFlash(E.encodeBlocks(c, blocks), { counter = counter or (blocks.counter + 1), image = blocks.image }), c
|
|
end
|
|
|
|
for name, d in pairs(decoded) do
|
|
local img, before = rebuild(IMAGES[name])
|
|
local c, blocks = E.decode(img)
|
|
check(c ~= nil, name .. " re-encoded flash decodes")
|
|
if c then
|
|
eq(blocks.counter, d.blocks.counter + 1, name .. " counter advanced")
|
|
eq(blocks.slot, (d.blocks.slot + 1) % 2, name .. " save went to the other slot")
|
|
for _, blk in ipairs(L.BLOCKS) do
|
|
if name ~= "em_battle" then
|
|
check(blocks[blk.key] == d.blocks[blk.key], name .. " " .. blk.key .. " bytes survive re-encode")
|
|
end
|
|
end
|
|
checkOracle(name .. " re-encoded", c, blocks, F.oracle[name])
|
|
for sec = 0, 13 do
|
|
local phys = blocks.slot * 14 + (sec + blocks.counter) % 14
|
|
local o = phys * 0x1000
|
|
local id = E.u16(img, o + 0xFF4)
|
|
eq(E.u16(img, o + 0xFF6), E.checksum(img, o, L.CHUNK_SIZES[id]), name .. " sector " .. phys .. " checksum")
|
|
end
|
|
for sec = 28, 31 do
|
|
local o = sec * 0x1000
|
|
check(img:sub(o + 1, o + 0x1000) == IMAGES[name]:sub(o + 1, o + 0x1000), name .. " sector " .. sec .. " kept")
|
|
end
|
|
end
|
|
local wrapped = rebuild(IMAGES[name], 0xFFFFFFFF)
|
|
local w = E.decode(wrapped)
|
|
eq(w and w.counter, 0xFFFFFFFF, name .. " counter at the wrap edge")
|
|
local after = rebuild(wrapped, 0)
|
|
local z = E.decode(after)
|
|
eq(z and z.counter, 0, name .. " counter wraps to 0 and still wins")
|
|
eq(z and z.money, before.money, name .. " wrapped save keeps money")
|
|
end
|
|
|
|
local function opts(template)
|
|
return {
|
|
template = template, version = "emerald", metGame = 3,
|
|
toNational = function() return nil end,
|
|
itemId = function(id) return tonumber(id) end,
|
|
}
|
|
end
|
|
|
|
for _, name in ipairs({ "em_battle", "em_fresh", "em_doctored" }) do
|
|
local bytes = IMAGES[name]
|
|
local save, err = E.importPort(bytes, "emerald")
|
|
check(save ~= nil, name .. " imports (" .. tostring(err) .. ")")
|
|
if save then
|
|
eq(save.version, "emerald", name .. " tagged emerald")
|
|
eq(save.rivalName, nil, name .. " has no stored rival name")
|
|
eq(save.encryptionKey, F.oracle[name].key, name .. " native encryption key")
|
|
check(type(save.modData.emerald_daycare) == "table", name .. " daycare under modData.emerald_daycare")
|
|
check(type(save.modData.cartImport.recordMixTvBytes256) == "table"
|
|
and #save.modData.cartImport.recordMixTvBytes256 == 256, name .. " preserves first 256 raw TV bytes for record mixing")
|
|
local decoded = E.decode(bytes)
|
|
for i = 1, 256 do
|
|
eq(save.modData.cartImport.recordMixTvBytes256[i], decoded.tvShowsRawPrefix:byte(i),
|
|
name .. " raw TV byte " .. i .. " retained")
|
|
end
|
|
eq(save.modData.firered_daycare, nil, name .. " no FireRed daycare")
|
|
eq(save.modData.fameChecker, nil, name .. " no fame checker")
|
|
check(type(save.map) == "string" and save.map:sub(1, 3) == "EM_", name .. " map " .. tostring(save.map))
|
|
local out, xerr = E.exportPort(save, opts(bytes))
|
|
check(out ~= nil, name .. " exports (" .. tostring(xerr) .. ")")
|
|
if out then
|
|
local a, b = E.readBlocks(bytes), E.readBlocks(out)
|
|
for _, blk in ipairs(L.BLOCKS) do
|
|
check(a[blk.key] == b[blk.key], name .. " " .. blk.key .. " byte-identical after import/export")
|
|
end
|
|
for sec = 28, 31 do
|
|
local o = sec * 0x1000
|
|
check(out:sub(o + 1, o + 0x1000) == bytes:sub(o + 1, o + 0x1000), name .. " sector " .. sec .. " byte-identical")
|
|
end
|
|
eq(b.counter, a.counter + 1, name .. " export bumps the save counter")
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
local bytes = IMAGES.em_doctored
|
|
local save = assert(E.importPort(bytes, "emerald"))
|
|
save.money = 424242
|
|
save.coins = 77
|
|
table.insert(save.bag.pockets.ITEMS, { id = 13, qty = 9 })
|
|
save.flags[0x867] = true
|
|
save.vars[0x4046] = nil
|
|
save.berryTrees[20] = { berry = 3, stage = 2, stopGrowth = true, minutesUntilNextStage = 60, berryYield = 0,
|
|
regrowthCount = 1, watered1 = true, watered2 = false, watered3 = true, watered4 = false }
|
|
save.pokeblocks[2] = { color = 3, spicy = 0, dry = 40, sweet = 0, bitter = 0, sour = 0, feel = 25 }
|
|
save.trainerRematches[10] = 3
|
|
save.linkBattleRecords = { { name = "MAY", trainerId = 7, wins = 9, losses = 2, draws = 1 } }
|
|
save.modData.emerald_daycare.daycare.offspringPersonality = 0xFEDCBA98
|
|
save.localTimeOffset = { days = 3, hours = -1, minutes = 2, seconds = 0 }
|
|
local out = assert(E.exportPort(save, opts(bytes)))
|
|
local c, blocks = E.decode(out)
|
|
eq(c.money, 424242, "edited money exports under the save's key")
|
|
eq(c.coins, 77, "edited coins export")
|
|
local last = c.pockets.ITEMS[#c.pockets.ITEMS]
|
|
same_eq({ last.id, last.qty }, { 13, 9 }, "added bag item exports")
|
|
local has = false
|
|
for _, id in ipairs(c.flags) do if id == 0x867 then has = true end end
|
|
check(has, "set flag exports into the 300-byte flag array")
|
|
eq(c.daycare.offspringPersonality, 0xFEDCBA98, "u32 egg personality exports")
|
|
local s = Rse.readSections(E, blocks, {})
|
|
eq(s.berryTrees[20] and s.berryTrees[20].stopGrowth, true, "berry tree stopGrowth bit")
|
|
eq(s.berryTrees[20] and s.berryTrees[20].watered3, true, "berry tree watered bits")
|
|
eq(s.berryTrees[20] and s.berryTrees[20].regrowthCount, 1, "berry tree regrowth")
|
|
eq(s.pokeblocks[2].dry, 40, "pokeblock exports")
|
|
eq(s.trainerRematches[10], 3, "rematch table exports")
|
|
eq(s.linkBattleRecords[1].name, "MAY", "link record exports to SB1")
|
|
eq(s.linkBattleRecords[1].wins, 9, "link record wins")
|
|
eq(s.localTimeOffset.hours, -1, "local time offset keeps its sign")
|
|
eq(s.localTimeOffset.days, 3, "local time offset days")
|
|
local again = assert(E.importPort(out, "emerald"))
|
|
eq(again.money, 424242, "re-import sees the edit")
|
|
end
|
|
|
|
do
|
|
local fr = FR_IMAGES.fr_rich_game
|
|
local _, why = E.decode(fr)
|
|
eq(why, "frlg", "a FireRed save is named for the emerald slot")
|
|
local _, msg = E.importPort(fr, "emerald")
|
|
eq(msg, E.MSG.frlg, "FireRed-into-Emerald message")
|
|
local _, frWhy = Gen3Save.decode(IMAGES.em_battle)
|
|
eq(frWhy, "notFrlg", "an Emerald save is refused by the FireRed codec")
|
|
local _, small = E.importPort(string.rep("\0", 32768), "emerald")
|
|
eq(small, E.MSG.size:format(32768), "size sentence names Emerald")
|
|
local _, empty = E.importPort(string.rep("\255", 0x20000), "emerald")
|
|
eq(empty, E.MSG.empty, "an erased flash is empty")
|
|
local blocks = assert(E.readBlocks(IMAGES.em_fresh))
|
|
local sb2 = blocks.sb2:sub(1, 0x890) .. string.rep("\0", #blocks.sb2 - 0x890)
|
|
local sb1 = blocks.sb1:sub(1, 0x3AC0) .. string.rep("\0", #blocks.sb1 - 0x3AC0)
|
|
local rs = E.buildFlash({ sb2 = sb2, sb1 = sb1, storage = blocks.storage }, { counter = 3 })
|
|
eq(Gen3Save.sniff(rs), "rs", "zeroed Emerald tails sniff as Ruby/Sapphire")
|
|
local _, rsWhy = E.decode(rs)
|
|
eq(rsWhy, "rs", "Ruby/Sapphire save named for the emerald slot")
|
|
local mgba = IMAGES.em_fresh
|
|
eq(#mgba, 0x20010, "fixture keeps the emulator RTC footer")
|
|
check(E.decode(mgba) ~= nil, "an mGBA RTC footer is dropped by normalizeSize")
|
|
end
|
|
|
|
do
|
|
local fr = FR_IMAGES.fr_rich_game
|
|
local a = Gen3Save.decode(fr)
|
|
local b = Gen3Save.forVersion("leafgreen").decode(fr)
|
|
eq(a.money, b.money, "leafgreen uses the FireRed codec")
|
|
eq(a.nicknameRaw, nil, "FireRed decode carries no raw name fields")
|
|
check(a.party[1] and a.party[1].nicknameRaw == nil, "FireRed mons carry no raw name bytes")
|
|
end
|
|
|
|
do
|
|
local names = { "rtc", "encryptionKey", "berryTrees", "tv", "decorations", "weather", "matchCall", "giftRibbons",
|
|
"dewfordTrends", "contests", "lilycoveLady", "lottery", "pokeblocks", "secretBases", "oldMan" }
|
|
for _, n in ipairs(names) do check(Rse.SECTIONS[n] ~= nil, "port mapper covers save section " .. n) end
|
|
end
|
|
|
|
T.finish("gen3_save_emerald_codec")
|