Files
gen1recomp/tests/save_compat/_gen3_decode.lua
T
2026-10-01 17:30:52 -04:00

362 lines
15 KiB
Lua

local bit = require("bit")
local D = {}
local U32 = 4294967296
local function b(s, o) return s:byte(o + 1) or 0 end
local function w(s, o) return b(s, o) + b(s, o + 1) * 256 end
local function d(s, o) return w(s, o) + w(s, o + 2) * 65536 end
local function x32(a, k) return bit.bxor(a, k) % U32 end
D.SIZES = {
frlg = { [0] = 0xF24, 0xF80, 0xF80, 0xF80, 0xEE8, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0x7D0 },
emerald = { [0] = 0xF2C, 0xF80, 0xF80, 0xF80, 0xF08, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0xF80, 0x7D0 },
}
D.OFF = {
frlg = { key = 0xF20, code = 0xAC, powder = 0xAF8, partyCount = 0x34, party = 0x38, money = 0x290, coins = 0x294,
pockets = { { 0x310, 42 }, { 0x3B8, 30 }, { 0x430, 13 }, { 0x464, 58 }, { 0x54C, 43 } }, stats = 0x1200 },
emerald = { key = 0xAC, code = 0xAC, powder = 0x1F4, partyCount = 0x234, party = 0x238, money = 0x490, coins = 0x494,
pockets = { { 0x560, 30 }, { 0x5D8, 30 }, { 0x650, 16 }, { 0x690, 64 }, { 0x790, 46 } }, stats = 0x159C },
}
local function sum(s, off, size)
local t = 0
for o = off, off + size - 4, 4 do t = (t + d(s, o)) % U32 end
return (t + math.floor(t / 65536)) % 65536
end
function D.blocks(s, family)
local sizes = D.SIZES[family]
local best
for slot = 0, 1 do
local found, counter, ok = {}, nil, true
for i = 0, 13 do
local base = (slot * 14 + i) * 0x1000
if d(s, base + 0xFF8) == 0x08012025 then
local id = w(s, base + 0xFF4)
if sizes[id] and w(s, base + 0xFF6) == sum(s, base, sizes[id]) then
found[id], counter = base, d(s, base + 0xFFC)
end
end
end
for id = 0, 13 do if not found[id] then ok = false end end
if ok and (not best or counter > best.counter) then best = { found = found, counter = counter, slot = slot } end
end
if not best then return nil end
local function join(a, z)
local parts = {}
for id = a, z do parts[#parts + 1] = s:sub(best.found[id] + 1, best.found[id] + sizes[id]) end
return table.concat(parts)
end
return { sb2 = join(0, 0), sb1 = join(1, 4), storage = join(5, 13), counter = best.counter, slot = best.slot }
end
local ORDER = {
"GAEM", "GAME", "GEAM", "GEMA", "GMAE", "GMEA", "AGEM", "AGME", "AEGM", "AEMG", "AMGE", "AMEG",
"EGAM", "EGMA", "EAGM", "EAMG", "EMGA", "EMAG", "MGAE", "MGEA", "MAGE", "MAEG", "MEGA", "MEAG",
}
function D.mon(raw)
local blank = true
for i = 1, 80 do if raw:byte(i) ~= 0 then blank = false break end end
if blank then return nil end
local pid, otid = d(raw, 0), d(raw, 4)
local key = bit.bxor(pid, otid)
local plain = {}
for i = 0, 11 do
local v = x32(d(raw, 0x20 + i * 4), key)
plain[#plain + 1] = string.char(v % 256, math.floor(v / 256) % 256, math.floor(v / 65536) % 256, math.floor(v / 16777216))
end
plain = table.concat(plain)
local ck = 0
for i = 0, 46, 2 do ck = (ck + w(plain, i)) % 65536 end
local order = ORDER[pid % 24 + 1]
local at = {}
for i = 1, 4 do at[order:sub(i, i)] = (i - 1) * 12 end
local moves = {}
for i = 0, 3 do moves[i + 1] = w(plain, at.A + i * 2) end
local ivw = d(plain, at.M + 4)
return {
pid = pid, otid = otid, nick = raw:sub(9, 18), ot = raw:sub(0x15, 0x1B), flags = b(raw, 0x13),
checksumOk = ck == w(raw, 0x1C), species = w(plain, at.G), item = w(plain, at.G + 2), exp = d(plain, at.G + 4),
moves = moves, ivWord = ivw, isEgg = math.floor(ivw / 2 ^ 30) % 2 == 1, metGame = math.floor(w(plain, at.M + 2) / 128) % 16,
shiny = bit.bxor(bit.bxor(math.floor(otid / 65536), otid % 65536), bit.bxor(math.floor(pid / 65536), pid % 65536)) % 65536 < 8,
}
end
function D.decode(s, family)
local blk = D.blocks(s, family)
if not blk then return nil end
local O = D.OFF[family]
local key = d(blk.sb2, O.key)
local out = { key = key, counter = blk.counter, code = d(blk.sb2, O.code), powderWord = d(blk.sb2, O.powder),
name = blk.sb2:sub(1, 8), tid = w(blk.sb2, 0x0A), sid = w(blk.sb2, 0x0C), gender = b(blk.sb2, 8),
playHours = w(blk.sb2, 0x0E), playMinutes = b(blk.sb2, 0x10),
money = x32(d(blk.sb1, O.money), key), coins = bit.bxor(w(blk.sb1, O.coins), key % 65536) % 65536,
party = {}, boxes = {}, pockets = {}, stats = {}, currentBox = b(blk.storage, 0), wallpapers = {}, boxNames = {} }
out.powder = x32(out.powderWord, key)
for i = 0, math.min(b(blk.sb1, O.partyCount), 6) - 1 do
out.party[i + 1] = D.mon(blk.sb1:sub(O.party + i * 100 + 1, O.party + i * 100 + 80))
out.party[i + 1].level = b(blk.sb1, O.party + i * 100 + 84)
end
for bx = 0, 13 do
out.boxes[bx + 1] = {}
for sl = 0, 29 do
local o = 4 + (bx * 30 + sl) * 80
local m = D.mon(blk.storage:sub(o + 1, o + 80))
if m and math.floor(m.flags / 2) % 2 == 1 then out.boxes[bx + 1][sl + 1] = m end
end
out.wallpapers[bx + 1] = b(blk.storage, 0x83C2 + bx)
out.boxNames[bx + 1] = blk.storage:sub(0x8344 + bx * 9 + 1, 0x8344 + bx * 9 + 9)
end
for p, pk in ipairs(O.pockets) do
out.pockets[p] = {}
for i = 0, pk[2] - 1 do
local id = w(blk.sb1, pk[1] + i * 4)
if id ~= 0 then
out.pockets[p][#out.pockets[p] + 1] = { id, bit.bxor(w(blk.sb1, pk[1] + i * 4 + 2), key % 65536) % 65536 }
end
end
end
for i = 0, 63 do out.stats[i] = x32(d(blk.sb1, O.stats + i * 4), key) end
return out
end
function D.text(s, off, len)
local out = {}
for i = 0, len - 1 do
local c = b(s, off + i)
if c == 0xFF then break end
local ch
if c == 0 then ch = " "
elseif c >= 0xBB and c <= 0xD4 then ch = string.char(65 + c - 0xBB)
elseif c >= 0xD5 and c <= 0xEE then ch = string.char(97 + c - 0xD5)
elseif c >= 0xA1 and c <= 0xAA then ch = string.char(48 + c - 0xA1)
else ch = ("{%02X}"):format(c) end
out[#out + 1] = ch
end
return table.concat(out)
end
D.FULL = {
frlg = { flags = 0xEE0, flagBytes = 288, vars = 0x1000, pcItems = { 0x298, 30 }, profile = 0x2CA0, battle = 0x2CAC,
mail = 0x2CD0, daycare = 0x2F80, roamer = 0x30D0, regItem = 0x296, seen2 = 0x3A18, seen1 = 0x5F8, rival = 0x3A4C,
optionsHi = 0x14, dexOwned = 0x28, dexSeen = 0x5C, dexMagic = 0x1B, dexOrder = 0x18, dexMode = 0x19 },
emerald = { flags = 0x1270, flagBytes = 300, vars = 0x139C, pcItems = { 0x498, 50 }, profile = 0x2BB0, battle = 0x2BBC,
mail = 0x2BE0, daycare = 0x3030, roamer = 0x31DC, regItem = 0x496, seen2 = 0x3B24, seen1 = 0x988, rival = nil,
optionsHi = 0x14, dexOwned = 0x28, dexSeen = 0x5C, dexMagic = 0x1A, dexOrder = 0x18, dexMode = 0x19 },
}
local function s16(s, o) local v = w(s, o); return v >= 32768 and v - 65536 or v end
local function bitList(s, off, nbytes)
local out = {}
for i = 0, nbytes * 8 - 1 do
if math.floor(b(s, off + math.floor(i / 8)) / 2 ^ (i % 8)) % 2 == 1 then out[#out + 1] = i end
end
return out
end
function D.warp(s, o)
local wid = b(s, o + 2)
return { group = b(s, o), num = b(s, o + 1), warpId = wid >= 128 and wid - 256 or wid, x = s16(s, o + 4), y = s16(s, o + 6) }
end
function D.fullMon(raw, party)
local m = D.mon(raw)
if not m then return nil end
local pid, otid = d(raw, 0), d(raw, 4)
local key = bit.bxor(pid, otid)
local plainParts = {}
for i = 0, 11 do
local v = x32(d(raw, 0x20 + i * 4), key)
plainParts[#plainParts + 1] = string.char(v % 256, math.floor(v / 256) % 256, math.floor(v / 65536) % 256, math.floor(v / 16777216))
end
local plain = table.concat(plainParts)
local at = {}
local order = ORDER[pid % 24 + 1]
for i = 1, 4 do at[order:sub(i, i)] = (i - 1) * 12 end
m.nickText, m.otText = D.text(raw, 8, 10), D.text(raw, 0x14, 7)
m.language, m.markings = b(raw, 0x12), b(raw, 0x1B)
m.ppBonuses, m.friendship = b(plain, at.G + 8), b(plain, at.G + 9)
m.pp, m.evs, m.contest = {}, {}, {}
for i = 0, 3 do m.pp[i + 1] = b(plain, at.A + 8 + i) end
for i = 0, 5 do m.evs[i + 1] = b(plain, at.E + i); m.contest[i + 1] = b(plain, at.E + 6 + i) end
m.pokerus, m.metLocation = b(plain, at.M), b(plain, at.M + 1)
local origins = w(plain, at.M + 2)
m.metLevel, m.metGame = origins % 128, math.floor(origins / 128) % 16
m.ball, m.otGender = math.floor(origins / 2048) % 16, math.floor(origins / 32768) % 2
m.ivs = {}
for i = 0, 5 do m.ivs[i + 1] = math.floor(m.ivWord / 2 ^ (5 * i)) % 32 end
m.ability = math.floor(m.ivWord / 2 ^ 31) % 2
m.ribbons = d(plain, at.M + 8)
if party then
m.status, m.level, m.mail = d(raw, 80), b(raw, 84), b(raw, 85)
m.hp, m.maxHp = w(raw, 86), w(raw, 88)
m.stats = {}
for i = 1, 5 do m.stats[i] = w(raw, 88 + i * 2) end
end
return m
end
function D.deep(s, family)
local blk = D.blocks(s, family)
if not blk then return nil end
local O, F = D.OFF[family], D.FULL[family]
local key = d(blk.sb2, O.key)
local out = {
key = key, counter = blk.counter, slot = blk.slot,
name = D.text(blk.sb2, 0, 8), gender = b(blk.sb2, 8), specialSaveWarpFlags = b(blk.sb2, 9),
tid = w(blk.sb2, 0x0A), sid = w(blk.sb2, 0x0C),
playHours = w(blk.sb2, 0x0E), playMinutes = b(blk.sb2, 0x10), playSeconds = b(blk.sb2, 0x11), playVBlanks = b(blk.sb2, 0x12),
buttonMode = b(blk.sb2, 0x13), optionsWord = w(blk.sb2, 0x14),
dexOrder = b(blk.sb2, F.dexOrder), dexMode = b(blk.sb2, F.dexMode), dexMagic = b(blk.sb2, F.dexMagic),
dexOwned = bitList(blk.sb2, F.dexOwned, 52), dexSeen = bitList(blk.sb2, F.dexSeen, 52),
dexSeen1 = bitList(blk.sb1, F.seen1, 52), dexSeen2 = bitList(blk.sb1, F.seen2, 52),
gcnLinkFlags = d(blk.sb2, 0xA8),
posX = s16(blk.sb1, 0), posY = s16(blk.sb1, 2),
location = D.warp(blk.sb1, 4), continueGameWarp = D.warp(blk.sb1, 12), dynamicWarp = D.warp(blk.sb1, 20),
lastHealLocation = D.warp(blk.sb1, 28), escapeWarp = D.warp(blk.sb1, 36),
savedMusic = w(blk.sb1, 44), weather = b(blk.sb1, 46), flashLevel = b(blk.sb1, 48), mapLayoutId = w(blk.sb1, 50),
money = x32(d(blk.sb1, O.money), key), coins = bit.bxor(w(blk.sb1, O.coins), key % 65536) % 65536,
registeredItem = w(blk.sb1, F.regItem),
flagsSet = bitList(blk.sb1, F.flags, F.flagBytes), vars = {}, stats = {},
pcItems = {}, pockets = {}, party = {}, boxes = {}, wallpapers = {}, boxNames = {}, currentBox = b(blk.storage, 0),
profile = {}, mail = {},
}
out.powder = x32(d(blk.sb2, O.powder), key)
for i = 0, 255 do out.vars[0x4000 + i] = w(blk.sb1, F.vars + i * 2) end
for i = 0, 63 do out.stats[i] = x32(d(blk.sb1, O.stats + i * 4), key) end
for i = 0, F.pcItems[2] - 1 do
local id = w(blk.sb1, F.pcItems[1] + i * 4)
if id ~= 0 then out.pcItems[#out.pcItems + 1] = { id, w(blk.sb1, F.pcItems[1] + i * 4 + 2) } end
end
for p, pk in ipairs(O.pockets) do
out.pockets[p] = {}
for i = 0, pk[2] - 1 do
local id = w(blk.sb1, pk[1] + i * 4)
if id ~= 0 then out.pockets[p][#out.pockets[p] + 1] = { id, bit.bxor(w(blk.sb1, pk[1] + i * 4 + 2), key % 65536) % 65536 } end
end
end
for i = 0, 5 do out.profile[i + 1] = w(blk.sb1, F.profile + i * 2) end
for i = 0, math.min(b(blk.sb1, O.partyCount), 6) - 1 do
out.party[i + 1] = D.fullMon(blk.sb1:sub(O.party + i * 100 + 1, O.party + i * 100 + 100), true)
end
for bx = 0, 13 do
out.boxes[bx + 1] = {}
for sl = 0, 29 do
local o = 4 + (bx * 30 + sl) * 80
local m = D.fullMon(blk.storage:sub(o + 1, o + 80), false)
if m and math.floor(m.flags / 2) % 2 == 1 then out.boxes[bx + 1][sl + 1] = m end
end
out.wallpapers[bx + 1] = b(blk.storage, 0x83C2 + bx)
out.boxNames[bx + 1] = D.text(blk.storage, 0x8344 + bx * 9, 9)
end
for i = 0, 15 do
local o = F.mail + i * 36
local words = {}
for k = 0, 8 do words[k + 1] = w(blk.sb1, o + k * 2) end
out.mail[i + 1] = { words = words, name = D.text(blk.sb1, o + 18, 8), trainerId = d(blk.sb1, o + 26),
species = w(blk.sb1, o + 30), itemId = w(blk.sb1, o + 32) }
end
out.roamer = { ivs = d(blk.sb1, F.roamer), pid = d(blk.sb1, F.roamer + 4), species = w(blk.sb1, F.roamer + 8),
hp = w(blk.sb1, F.roamer + 10), level = b(blk.sb1, F.roamer + 12), status = b(blk.sb1, F.roamer + 13) }
return out
end
D.EXTRA = {
frlg = { daycare = 0x2F80, offspringSize = 2, stepCounter = 0x11A, route5 = 0x3C98, roamerSize = 0x14,
rival = 0x3A4C, dexUnown = 0x1C, dexSpinda = 0x20, battleStart = 0x2CAC, registeredTexts = 0x3AD4,
gcn = 0xA8, flash = 0x30, weather = 0x2E, savedMusic = 0x2C },
emerald = { daycare = 0x3030, offspringSize = 4, stepCounter = 0x11C, route5 = nil, roamerSize = 0x14,
rival = nil, dexUnown = 0x1C, dexSpinda = 0x20, battleStart = 0x2BBC, registeredTexts = 0x3C88,
gcn = 0xA8, flash = 0x30, weather = 0x2E, savedMusic = 0x2C },
}
local function rawBytes(s, off, n)
local out = {}
for i = 0, n - 1 do out[i + 1] = b(s, off + i) end
return out
end
D.rawBytes = rawBytes
local function mailAt(s, o)
local words = {}
for k = 0, 8 do words[k + 1] = w(s, o + k * 2) end
return { words = words, name = D.text(s, o + 18, 8), trainerId = d(s, o + 26), species = w(s, o + 30), itemId = w(s, o + 32) }
end
D.mailAt = mailAt
local function daycareMon(sb1, o)
local m = D.fullMon(sb1:sub(o + 1, o + 80), false)
return {
mon = m,
mail = mailAt(sb1, o + 80),
otName = D.text(sb1, o + 80 + 36, 8),
monName = D.text(sb1, o + 80 + 44, 11),
steps = d(sb1, o + 0x88),
}
end
function D.extra(s, family)
local blk = D.blocks(s, family)
if not blk then return nil end
local X = D.EXTRA[family]
local out = {
unownPersonality = d(blk.sb2, X.dexUnown), spindaPersonality = d(blk.sb2, X.dexSpinda),
gcnLinkFlags = d(blk.sb2, X.gcn), flashLevel = b(blk.sb1, X.flash), weather = b(blk.sb1, X.weather),
daycare = {}, roamer = nil, hof = {},
}
if X.rival then out.rivalName = D.text(blk.sb1, X.rival, 8) end
for i = 0, 1 do out.daycare[i + 1] = daycareMon(blk.sb1, X.daycare + i * 0x8C) end
out.offspringPersonality = X.offspringSize == 4 and d(blk.sb1, X.daycare + 0x118) or w(blk.sb1, X.daycare + 0x118)
out.daycareStepCounter = b(blk.sb1, X.daycare + X.stepCounter)
if X.route5 then out.route5 = daycareMon(blk.sb1, X.route5) end
local F = D.FULL[family]
out.roamerRaw = rawBytes(blk.sb1, F.roamer, X.roamerSize)
out.battleWords = {}
for i = 0, 17 do out.battleWords[i + 1] = w(blk.sb1, X.battleStart + i * 2) end
local hofParts = {}
for _, sector in ipairs({ 28, 29 }) do
hofParts[#hofParts + 1] = s:sub(sector * 0x1000 + 1, sector * 0x1000 + 0xF80)
end
local hof = table.concat(hofParts)
out.hofSignatureOk = d(s, 28 * 0x1000 + 0xFF8) == 0x08012025 and d(s, 29 * 0x1000 + 0xFF8) == 0x08012025
out.hofChecksumOk = out.hofSignatureOk and w(s, 28 * 0x1000 + 0xFF4) == sum(s, 28 * 0x1000, 0xF80)
and w(s, 29 * 0x1000 + 0xFF4) == sum(s, 29 * 0x1000, 0xF80)
out.registeredTexts = {}
for i = 0, 9 do out.registeredTexts[i + 1] = D.text(blk.sb1, X.registeredTexts + i * 21, 21) end
for t = 0, out.hofSignatureOk and 49 or -1 do
local team = {}
for i = 0, 5 do
local o = (t * 6 + i) * 20
local sl = w(hof, o + 8)
if sl % 512 == 0 then break end
team[#team + 1] = { tid = d(hof, o), personality = d(hof, o + 4), species = sl % 512,
level = math.floor(sl / 512), nick = D.text(hof, o + 10, 10) }
end
if #team == 0 then break end
out.hof[#out.hof + 1] = team
end
return out
end
function D.diff(a, b2)
local out = {}
local function walk(x, y, path)
if type(x) ~= type(y) then out[#out + 1] = path return end
if type(x) ~= "table" then
if x ~= y then out[#out + 1] = path end
return
end
for k, v in pairs(x) do walk(v, y[k], path .. "." .. tostring(k)) end
for k in pairs(y) do if x[k] == nil then out[#out + 1] = path .. "." .. tostring(k) end end
end
walk(a, b2, "")
return out
end
return D