mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 23:55:22 -04:00
146 lines
5.3 KiB
Lua
146 lines
5.3 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 K = require("tests.save_compat._codec")
|
|
|
|
if not K.gen1Available() then
|
|
print("gen1_mutation skipped (needs data/generated/ for the Gen 1 save codec)")
|
|
os.exit(0)
|
|
end
|
|
|
|
local GenSave = require("src.save_convert.GenSave")
|
|
local SaveConvert = require("src.save_convert.SaveConvert")
|
|
local G1 = require("tests.fixtures.save.gen1_build")
|
|
local Diff = require("tests.save_compat._diff")
|
|
local Regions = require("src.save_convert.regions.gen1")
|
|
|
|
local O = GenSave.OFFSETS
|
|
local MUTATIONS = tonumber(os.getenv("GEN1_MUTATIONS")) or 1500
|
|
|
|
local function lcg(seed)
|
|
local state = seed
|
|
return function(n)
|
|
state = (state * 1664525 + 1013904223) % 4294967296
|
|
return math.floor(state / 4294967296 * n)
|
|
end
|
|
end
|
|
|
|
local function reseal(bytes)
|
|
local sum = 0
|
|
for i = O.checksumStart + 1, O.checksumEnd do sum = (sum + bytes:byte(i)) % 256 end
|
|
bytes = bytes:sub(1, O.mainChecksum) .. string.char(255 - sum) .. bytes:sub(O.mainChecksum + 2)
|
|
return bytes
|
|
end
|
|
|
|
local regions = Regions.layouts.default
|
|
local idx = Diff.index(regions)
|
|
local base
|
|
do
|
|
local cases = G1.cases()
|
|
for _, c in ipairs(cases) do
|
|
if c.id == "g1.red.full_boxes" then base = c.bytes end
|
|
end
|
|
end
|
|
assert(base)
|
|
|
|
local darkMapIndexes = {}
|
|
do
|
|
local data = K.gen1Data("red")
|
|
local cw = GenSave.crosswalks(data)
|
|
for _, id in ipairs(data.field.darkMaps.maps) do darkMapIndexes[#darkMapIndexes + 1] = cw.mapsIndex[id] end
|
|
end
|
|
|
|
local function invalidInput(bytes)
|
|
local function listBad(countAt, rowsAt, cap)
|
|
local n = 0
|
|
while n < cap and bytes:byte(rowsAt + n * 2 + 1) ~= 0xFF do n = n + 1 end
|
|
return bytes:byte(countAt + 1) ~= n
|
|
end
|
|
if listBad(0x25C9, 0x25CA, 20) or listBad(0x27E6, 0x27E7, 50) then return true end
|
|
if bytes:byte(0x2F2C + 1) > 6 or bytes:byte(0x30C0 + 1) > 20 then return true end
|
|
for b = 0, 11 do
|
|
local base = b < 6 and (0x4000 + b * 0x462) or (0x6000 + (b - 6) * 0x462)
|
|
if bytes:byte(base + 1) > 20 then return true end
|
|
end
|
|
for _, at in ipairs({ 0x25F3, 0x25F4, 0x25F5, 0x2850, 0x2851 }) do
|
|
local b = bytes:byte(at + 1)
|
|
if b % 16 > 9 or math.floor(b / 16) > 9 then return true end
|
|
end
|
|
local darkMap = false
|
|
for _, id in ipairs(darkMapIndexes) do if bytes:byte(0x260A + 1) == id then darkMap = true end end
|
|
if not darkMap and bytes:byte(0x2609 + 1) ~= 0 then return true end
|
|
for _, at in ipairs({ 0x2CEF, 0x2CF0, 0x2CF1 }) do
|
|
if bytes:byte(at + 1) > 59 then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local rand = lcg(20260930)
|
|
local raised, nilResult, identical, differing, normalized = 0, 0, 0, 0, 0
|
|
local byRegion = {}
|
|
for n = 1, MUTATIONS do
|
|
local at = rand(0x7A53)
|
|
local region = idx[at]
|
|
if region and not region.derived and at ~= O.mainChecksum then
|
|
local mutated = base:sub(1, at) .. string.char((base:byte(at + 1) + 1 + rand(255)) % 256) .. base:sub(at + 2)
|
|
mutated = reseal(mutated)
|
|
local ok, save, err = pcall(SaveConvert.importSav, mutated, "red", "red")
|
|
if not ok then
|
|
raised = raised + 1
|
|
check(false, ("mutation at 0x%X (%s): import raised %s"):format(at, region.name, tostring(save)))
|
|
elseif not save then
|
|
nilResult = nilResult + 1
|
|
else
|
|
local ok2, out, xerr = pcall(SaveConvert.exportSav, save, "red")
|
|
if not ok2 then
|
|
raised = raised + 1
|
|
check(false, ("mutation at 0x%X (%s): export raised %s"):format(at, region.name, tostring(out)))
|
|
elseif not out then
|
|
nilResult = nilResult + 1
|
|
else
|
|
local diff = Diff.diff(mutated, out, regions)
|
|
local meaningful = {}
|
|
local terminators = {}
|
|
terminators[O.partyData + 1 + math.min(mutated:byte(O.partyData + 1), 6)] = true
|
|
terminators[O.curBoxData + 1 + math.min(mutated:byte(O.curBoxData + 1), 20)] = true
|
|
for b = 0, 11 do
|
|
local base = b < 6 and (0x4000 + b * 0x462) or (0x6000 + (b - 6) * 0x462)
|
|
terminators[base + 1 + math.min(mutated:byte(base + 1), 20)] = true
|
|
end
|
|
for _, e in ipairs(diff) do
|
|
local atTerminator = e.name:match("%.species$") and e.count == 1 and terminators[e.first]
|
|
if e.name ~= "identityTag" and not atTerminator then
|
|
meaningful[#meaningful + 1] = e
|
|
end
|
|
end
|
|
if #meaningful > 0 and invalidInput(mutated) then
|
|
normalized = normalized + 1
|
|
meaningful = {}
|
|
end
|
|
if #meaningful == 0 then
|
|
identical = identical + 1
|
|
else
|
|
differing = differing + 1
|
|
byRegion[region.name] = byRegion[region.name] or { count = 0, sample = Diff.format(meaningful, 2) }
|
|
byRegion[region.name].count = byRegion[region.name].count + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local names = {}
|
|
for name in pairs(byRegion) do names[#names + 1] = name end
|
|
table.sort(names)
|
|
for _, name in ipairs(names) do
|
|
print((" mutation sink %s x%d: %s"):format(name, byRegion[name].count, byRegion[name].sample))
|
|
end
|
|
print(("gen1_mutation: %d identical, %d normalized invalid inputs, %d differing, %d refused, %d raised"):format(
|
|
identical, normalized, differing, nilResult, raised))
|
|
eq(raised, 0, "no mutation makes the importer or exporter raise")
|
|
eq(differing, 0, "every accepted single-byte mutation reproduces byte for byte")
|
|
|
|
T.finish()
|