mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 15:50:39 -04:00
279 lines
15 KiB
Lua
279 lines
15 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 Compat = require("src.save_convert.Compat")
|
|
local B = require("tests.fixtures.save.bytes")
|
|
local G1 = require("tests.fixtures.save.gen1_build")
|
|
local G2 = require("tests.fixtures.save.gen2_build")
|
|
local G3 = require("tests.fixtures.save.gen3_build")
|
|
|
|
local fired = {}
|
|
|
|
local function rules(report, list)
|
|
local out = {}
|
|
for _, e in ipairs(report[list]) do
|
|
out[e.rule] = (out[e.rule] or 0) + 1
|
|
fired[e.rule] = true
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function expectError(bytes, version, rule, label)
|
|
local r = Compat.check(bytes, version)
|
|
check(rules(r, "errors")[rule] ~= nil, ("%s: flags %s -- got: %s"):format(label, rule, Compat.describe(r)))
|
|
eq(r.ok, false, label .. ": the report is not ok")
|
|
return r
|
|
end
|
|
|
|
local function expectWarn(bytes, version, rule, label)
|
|
local r = Compat.check(bytes, version)
|
|
check(rules(r, "warnings")[rule] ~= nil, ("%s: warns %s -- got: %s"):format(label, rule, Compat.describe(r)))
|
|
eq(r.ok, true, label .. ": a warning alone keeps the report ok -- " .. Compat.describe(r))
|
|
return r
|
|
end
|
|
|
|
local function expectClean(bytes, version, label)
|
|
local r = Compat.check(bytes, version)
|
|
eq(#r.errors, 0, label .. ": no errors -- " .. Compat.describe(r))
|
|
return r
|
|
end
|
|
|
|
local function edit(bytes, fn)
|
|
local b = B.fromString(bytes)
|
|
fn(b)
|
|
return B.pack(b)
|
|
end
|
|
|
|
for rule, sev in pairs(Compat.RULES) do
|
|
check(sev == "error" or sev == "warn", "rule " .. rule .. " has a known severity")
|
|
end
|
|
|
|
local function g1(spec)
|
|
spec = spec or {}
|
|
spec.version = spec.version or "red"
|
|
return G1.build(spec)
|
|
end
|
|
|
|
local function g1Reseal(b)
|
|
b[0x3523] = B.complement8(b, 0x2598, 0x3523)
|
|
end
|
|
|
|
expectClean(g1(), "red", "gen1 basic")
|
|
expectClean(g1({ version = "yellow" }), "yellow", "gen1 yellow")
|
|
expectError(string.rep("\0", 100), "red", "size", "gen1 short file")
|
|
expectError(edit(g1(), function(b) b[0x3523] = (b[0x3523] + 1) % 256 end), "red", "gen1.mainChecksum",
|
|
"gen1 bad main checksum")
|
|
expectError(edit(g1(), function(b)
|
|
b[0x2D0D] = B.sum8(b, 0x2009, 0x2B83)
|
|
b[0x1F0D] = B.sum8(b, 0x1209, 0x1D83)
|
|
g1Reseal(b)
|
|
end), "red", "gen1.openhomeMisdetect", "gen1 image whose Crystal 8-bit sums validate")
|
|
expectError(edit(g1(), function(b)
|
|
B.le(b, 0x2D0D, B.sum16(b, 0x2009, 0x2C8C), 2)
|
|
B.le(b, 0x7F0D, B.sum16(b, 0x2009, 0x2C8C), 2)
|
|
g1Reseal(b)
|
|
end), "red", "gen1.openhomeMisdetect", "gen1 image whose Japanese Gen 2 sums validate")
|
|
expectWarn(g1({ patch = function(b) b[0x271C] = 9 end }), "red", "gen1.versionMarker",
|
|
"gen1 Red save whose byte 0x271C OpenHome reads as Pikachu friendship")
|
|
expectError(edit(g1(), function(b) b[0x2F2C + 2] = 0x99; g1Reseal(b) end), "red", "gen1.partyList",
|
|
"gen1 party list without terminator")
|
|
expectError(edit(g1(), function(b) b[0x30C0] = 21; g1Reseal(b) end), "red", "gen1.curBoxList",
|
|
"gen1 current box count past 20")
|
|
expectError(g1({ party = { G1.mon(), G1.mon() }, patch = function(b) b[0x2F2C + 2] = 0 end }), "red",
|
|
"gen1.listSpecies0", "gen1 species 0 inside the party list")
|
|
expectError(g1({ currentBox = 12, genuineCurrentSlot = false }), "red", "gen1.currentBoxIndex", "gen1 box index 12")
|
|
expectError(g1({ patch = function(b) B.put(b, 0x2ED5, 0, 0xFF); B.put(b, 0x302D, 0, 0xFF) end }), "red",
|
|
"gen1.japaneseCollision", "gen1 lists that also pass PKHeX's Japanese test")
|
|
expectError(edit(g1(), function(b)
|
|
b[0x2D69] = B.sum8(b, 0x2009, 0x2D69)
|
|
b[0x7E6D] = (B.sum8(b, 0x15C7, 0x17ED) + B.sum8(b, 0x3D96, 0x3F40) + B.sum8(b, 0x0C6B, 0x10E8)
|
|
+ B.sum8(b, 0x7E39, 0x7E6D) + B.sum8(b, 0x10E8, 0x15C7)) % 256
|
|
g1Reseal(b)
|
|
end), "red", "gen1.openhomeMisdetect", "gen1 image whose Gold/Silver 8-bit sums validate (G1-25)")
|
|
expectError(edit(g1({ boxes = { G1.boxOf(1, 3) }, currentBox = 1 }), function(b)
|
|
b[0x4000] = 25
|
|
end), "red", "gen1.bankList", "gen1 stored box count past 20")
|
|
expectWarn(g1({ party = { G1.mon({ species = 0x1F }) } }), "red", "gen1.unknownSpecies", "gen1 MissingNo index")
|
|
expectWarn(g1({ staleBankChecksums = true }), "red", "gen1.bankChecksum", "gen1 stale bank checksums")
|
|
expectWarn(g1({ starter = 0x54 }), "red", "gen1.versionMarker", "gen1 Red save with Pikachu as starter")
|
|
expectWarn(g1({ version = "yellow", patch = function(b) b[0x29C3] = 0xB0 end }), "yellow", "gen1.versionMarker",
|
|
"gen1 Yellow save without the Pikachu starter")
|
|
expectWarn(g1({ boxes = { G1.boxOf(1, 3), G1.boxOf(1, 4) }, currentBox = 0,
|
|
patch = function(b) b[0x4462 + 22 + 2] = 0xFF end }), "red", "gen1.openhomeHp255", "gen1 boxed mon with HP 255")
|
|
expectWarn(g1({ patch = function(b) b[0x29C5] = 0x30 end }), "red", "gen1.blackoutMap", "gen1 blackout map outside the fly table")
|
|
expectWarn(g1({ boxesInitialized = false, bankFill = 0,
|
|
after = function(b) B.put(b, 0x4462, 1, 0x99, 0xFF) end }), "red", "gen1.boxesUninitialized",
|
|
"gen1 bank list with bit 7 clear")
|
|
expectClean(g1({ boxesInitialized = false }), "red", "gen1 never changed boxes, banks still 0xFF")
|
|
|
|
local function g2(version, spec)
|
|
spec = spec or {}
|
|
spec.version = version
|
|
return G2.build(spec)
|
|
end
|
|
|
|
for _, v in ipairs({ "gold", "silver", "crystal" }) do
|
|
local L = G2.layout(v)
|
|
expectClean(g2(v), v, v .. " basic")
|
|
expectClean(g2(v, { footer = string.rep("\0", 48) }), v, v .. " with a 48-byte RTC footer")
|
|
expectError(g2(v, { footer = string.rep("\0", 13) }), v, "size", v .. " with an odd 13-byte tail")
|
|
expectError(edit(g2(v), function(b) b[L.sCheckValue1] = 0 end), v, "gen2.checkValues", v .. " check value 1")
|
|
expectError(edit(g2(v), function(b) b[L.sChecksum] = (b[L.sChecksum] + 1) % 256 end), v, "gen2.checksum",
|
|
v .. " bad primary sum")
|
|
expectError(g2(v, { party = { G2.mon(), G2.mon() }, patch = function(b) b[L.wPartySpecies + 1] = 0 end }), v,
|
|
"gen2.listSpecies0", v .. " species 0 inside the party list")
|
|
expectError(g2(v, { patch = function(b) b[L.boxes[4]] = 30 end }), v, "gen2.boxList", v .. " box count past 20")
|
|
expectError(g2(v, { patch = function(b) b[L.wPartyCount] = 30 end }), v, "gen2.partyList", v .. " party count past 6")
|
|
expectError(edit(g2(v), function(b) b[L.sChecksum] = (b[L.sChecksum] + 1) % 256 end), v, "gen2.openhomeChecksum",
|
|
v .. " whose 8-bit sums do not validate for OpenHome")
|
|
local egg = expectClean(g2(v, { party = { G2.mon(), G2.mon({ listed = G2.EGG, nick = "EGG" }) } }), v,
|
|
v .. " party egg is legal")
|
|
eq(rules(egg, "warnings")["gen2.listStructMismatch"], nil, v .. ": an 0xFD egg marker is not a list mismatch")
|
|
expectWarn(g2(v, { party = { G2.mon({ species = 252 }) } }), v, "gen2.unknownSpecies", v .. " species past Celebi")
|
|
expectWarn(g2(v, { party = { G2.mon({ listed = 7 }) } }), v, "gen2.listStructMismatch", v .. " list/struct disagree")
|
|
expectWarn(g2(v, { party = { G2.mon({ nick = "" }) } }), v, "gen2.blankNickname", v .. " blank nickname")
|
|
expectWarn(g2(v, { patch = function(b)
|
|
local caught = v == "crystal" and 0x2A27 or 0x2A4C
|
|
b[caught + 25] = 1
|
|
end }), v, "gen2.unownDex", v .. " Unown caught with an empty Unown dex")
|
|
end
|
|
|
|
for _, v in ipairs({ "gold", "silver" }) do
|
|
expectWarn(g2(v, { after = function(b) B.fill(b, 0x3D69, 0x2D, 0x5A) end }),
|
|
v, "gen2.pkhexHallOfFame", v .. " Hall of Fame tail overwritten by PKHeX")
|
|
local clean = Compat.check(g2(v), v)
|
|
eq(rules(clean, "warnings")["gen2.pkhexHallOfFame"], nil, v .. " empty Hall of Fame tail has no loss warning")
|
|
end
|
|
local crystalHall = Compat.check(g2("crystal", { after = function(b) B.fill(b, 0x3D69, 0x2D, 0x5A) end }), "crystal")
|
|
eq(rules(crystalHall, "warnings")["gen2.pkhexHallOfFame"], nil, "Crystal writer does not overlap Hall of Fame")
|
|
|
|
expectError(edit(g2("gold"), function(b) b[0x7E6D] = 0; b[0x7E6E] = 0 end), "gold", "gen2.backupChecksum",
|
|
"gold whose PKHeX checksum 2 was never written (G2-08)")
|
|
expectError(edit(g2("crystal"), function(b) b[0x1F0D] = (b[0x1F0D] + 1) % 256 end), "crystal", "gen2.backupChecksum",
|
|
"crystal backup sum wrong")
|
|
local gsBothZero = edit(g2("gold"), function(b)
|
|
for _, seg in ipairs(G2.GS_BACKUP) do B.fill(b, seg.to, seg.size, 0) end
|
|
B.fill(b, 0x7E30, 0x40, 0)
|
|
local pad = 0x2D68
|
|
b[pad] = (b[pad] - B.sum8(b, 0x2009, 0x2D69)) % 256
|
|
B.le(b, 0x2D69, B.sum16(b, 0x2009, 0x2D69), 2)
|
|
end)
|
|
eq(gsBothZero:byte(0x2D69 + 1), 0, "the crafted Gold image sums to a 0 low byte")
|
|
expectError(gsBothZero, "gold", "gen2.openhomeChecksum", "gold with low byte 0 and an empty backup (G2-22)")
|
|
expectError(g2("gold", { after = function(b)
|
|
B.le(b, 0x2D0D, B.sum16(b, 0x2009, 0x2C8C), 2)
|
|
B.le(b, 0x7F0D, B.sum16(b, 0x2009, 0x2C8C), 2)
|
|
end }), "gold", "gen2.openhomeJapanCollision", "gold whose bytes also pass OpenHome's Japanese Gen 2 checksums")
|
|
expectError(g2("crystal", { patch = function(b) B.put(b, 0x288A, 0, 0xFF); B.put(b, 0x2D6C, 0, 0xFF) end }),
|
|
"crystal", "gen2.gsCollision", "crystal whose bytes also pass PKHeX's Gold/Silver list test")
|
|
expectError(g2("gold", { patch = function(b) B.put(b, 0x2F2C, 0, 0xFF); B.put(b, 0x30C0, 0, 0xFF) end }),
|
|
"gold", "gen2.gen1Collision", "gold whose bytes also pass PKHeX's Gen 1 list test")
|
|
|
|
local function g3(version, fn)
|
|
local w = G3.base(version)
|
|
if fn then fn(w) end
|
|
return G3.emit(w), w
|
|
end
|
|
|
|
local function sectorOf(w, id) return w.at[id] end
|
|
|
|
local function reseal(b, off, size)
|
|
local s = {}
|
|
for k = 0, 0xF7F do s[k + 1] = string.char(b[off + k]) end
|
|
B.le(b, off + 0xFF6, Compat.gen3SectorChecksum(table.concat(s), 0, size), 2)
|
|
end
|
|
|
|
for _, v in ipairs({ "firered", "leafgreen", "emerald" }) do
|
|
local fam = v == "emerald" and "emerald" or "frlg"
|
|
local chunks = G3.CHUNKS[fam]
|
|
local good, w = g3(v)
|
|
expectClean(good, v, v .. " basic")
|
|
expectError(good:sub(1, 0x10000), v, "size", v .. " 64K file")
|
|
expectWarn(good .. string.rep("\0", 16), v, "gen3.trailer", v .. " with a 16-byte trailer")
|
|
expectWarn(good .. string.rep("\255", 7), v, "gen3.trailer", v .. " with a 7-byte footer")
|
|
expectWarn(good .. string.rep("\255", 0x80), v, "gen3.trailer", v .. " with a uniform 0x80 pad that only PKForge trims")
|
|
expectError(good .. string.rep("\1", 13), v, "gen3.trailerUnreadable", v .. " with a 13-byte non-uniform tail")
|
|
expectError(good .. string.rep("\1", 0x80), v, "gen3.trailerUnreadable", v .. " with a 0x80-byte non-uniform tail")
|
|
expectError(good .. string.rep("\0", 0x101), v, "size", v .. " past OpenHome's 0x20100 window")
|
|
expectWarn(edit(good, function(b) B.le(b, sectorOf(w, 3) + 0xFFC, 0x01020304, 4) end), v, "gen3.slotCounter",
|
|
v .. " section whose counter differs from section 0")
|
|
expectWarn(edit(good, function(b)
|
|
local other = w.at[0] < 0xE000 and 0xE000 or -0xE000
|
|
for i = 0, 0xDFFF do b[w.at[0] - w.at[0] % 0xE000 + other + i] = b[w.at[0] - w.at[0] % 0xE000 + i] end
|
|
end), v, "gen3.slotCounter", v .. " both slots with the same counter")
|
|
expectWarn(g3(v, function(x) G3.setBoxMon(x, 5, 1, G3.mon({ pid = 81, lang = 12 })) end), v, "gen3.language",
|
|
v .. " mon with language byte 12")
|
|
expectError(edit(good, function(b) b[sectorOf(w, 3) + 0xFF4] = 2 end), v, "gen3.sectionIds",
|
|
v .. " slot with section 2 twice")
|
|
expectError(edit(good, function(b) b[sectorOf(w, 5) + 0x10] = (b[sectorOf(w, 5) + 0x10] + 1) % 256 end), v,
|
|
"gen3.sectorChecksum", v .. " stale sector checksum")
|
|
expectError(edit(good, function(b) B.le(b, sectorOf(w, 7) + 0xFF8, 0x01121999, 4) end), v, "gen3.signature",
|
|
v .. " Unbound sector signature")
|
|
expectError(edit(good, function(b)
|
|
local off = sectorOf(w, 0)
|
|
b[off + chunks[0] + 2] = 1
|
|
reseal(b, off, chunks[0])
|
|
end), v, "gen3.checksumWindow", v .. " nonzero byte past the section size")
|
|
expectError(g3(v, function(x) B.le(x.sb2, 6, 0, 2) end), v, "gen3.japaneseOt", v .. " OT bytes 6-7 zero")
|
|
expectError(g3(v, function(x) x.sb1[x.F.partyCount] = 7 end), v, "gen3.partyCount", v .. " party of 7")
|
|
expectError(g3(v, function(x)
|
|
G3.setBoxMon(x, 1, 1, G3.mon({ species = 0 }))
|
|
end), v, "gen3.species0", v .. " has-species with species 0 (G3-08)")
|
|
expectError(g3(v, function(x)
|
|
for s = 1, 12 do G3.setBoxMon(x, 4, s, G3.mon({ pid = s * 101, breakChecksum = true })) end
|
|
end), v, "gen3.monChecksumMajority", v .. " most boxed mons fail their checksum")
|
|
expectWarn(g3(v, function(x) G3.setBoxMon(x, 4, 1, G3.mon({ pid = 77, breakChecksum = true })) end), v,
|
|
"gen3.monChecksum", v .. " one bad mon checksum")
|
|
expectWarn(g3(v, function(x) G3.setBoxMon(x, 4, 2, G3.mon({ pid = 78, species = 500 })) end), v,
|
|
"gen3.foreignSpecies", v .. " species id 500")
|
|
expectWarn(g3(v, function(x) G3.setBoxMon(x, 4, 3, G3.mon({ pid = 79, species = 260 })) end), v,
|
|
"gen3.openhomeSpecies", v .. " filler species 260")
|
|
expectWarn(g3(v, function(x) G3.setBoxMon(x, 4, 4, G3.mon({ pid = 80, badEgg = true })) end), v,
|
|
"gen3.badEgg", v .. " bad egg flag")
|
|
expectWarn(edit(good, function(b) B.fill(b, 0x1E000, 0x100, 0x5A) end), v, "gen3.extraSector",
|
|
v .. " sector 30 with a stale checksum (G3-16)")
|
|
expectClean(g3(v, function(x)
|
|
G3.setParty(x, { G3.mon(), G3.mon({ pid = 0x2222, egg = true, nick = "EGG" }) })
|
|
end), v, v .. " party egg is legal")
|
|
end
|
|
|
|
local function frlgKey(fn)
|
|
return g3("firered", function(w)
|
|
w.key = 0
|
|
fn(w)
|
|
end)
|
|
end
|
|
expectError(frlgKey(function(w) B.le(w.sb2, 0xF20, 0, 4); B.le(w.sb2, 0xAF8, 0, 4) end), "firered",
|
|
"gen3.securityKey", "FireRed with key 0 at 0xF20 and 0xAF8 (G3-01)")
|
|
expectError(frlgKey(function(w) B.le(w.sb2, 0xAF8, 0, 4) end), "firered", "gen3.securityKey",
|
|
"FireRed with 0 at 0xAF8 only, which OpenHome reads as the key")
|
|
expectError(g3("firered", function(w) B.le(w.sb2, 0xAC, 0, 4) end), "firered", "gen3.gameCode",
|
|
"FireRed without the 1 marker at 0xAC")
|
|
expectError(g3("emerald", function(w) B.le(w.sb2, 0xAC, 0, 4) end), "emerald", "gen3.gameCode",
|
|
"Emerald with key 0 (G3-02, typed Ruby/Sapphire)")
|
|
expectError(g3("emerald", function(w) B.le(w.sb2, 0xAC, 1, 4) end), "emerald", "gen3.gameCode",
|
|
"Emerald with key 1 (G3-02, typed FireRed/LeafGreen)")
|
|
expectError(g3("emerald", function(w) B.fill(w.sb2, 0x890, 0xF2C - 0x890, 0) end), "emerald", "gen3.emeraldData",
|
|
"Emerald with SB2 0x890-0xF2B empty (G3-17)")
|
|
expectError(string.rep("\255", 0x20000), "firered", "gen3.sectionIds", "an erased flash")
|
|
|
|
for rule in pairs(Compat.RULES) do
|
|
check(fired[rule], "rule " .. rule .. " is exercised by a crafted bad image")
|
|
end
|
|
|
|
local ok, msg, report = Compat.gate(edit(g1(), function(b) b[0x3523] = (b[0x3523] + 1) % 256 end), "red")
|
|
eq(ok, nil, "gate refuses a hard failure")
|
|
check(type(msg) == "string" and msg:find("gen1.mainChecksum", 1, true) ~= nil, "and names the rule -- " .. tostring(msg))
|
|
eq(report.ok, false, "and hands back the report")
|
|
local ok2, warns = Compat.gate(g1({ staleBankChecksums = true }), "red")
|
|
eq(ok2, true, "gate passes a file with warnings only")
|
|
eq(#warns, 2, "and returns the warnings")
|
|
local ok3, msg3 = Compat.gate("x", "nonsense")
|
|
eq(ok3, nil, "an unknown version is refused")
|
|
check(tostring(msg3):find("unknown game version", 1, true) ~= nil, "with a reason -- " .. tostring(msg3))
|
|
|
|
T.finish()
|