Files
gen1recomp/tests/engine/gen2_sram_parity_test.lua
T

364 lines
14 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 Gen2Save = require("src.save_convert.Gen2Save")
local Gen2Layout = require("src.save_convert.Gen2Layout")
local Gen2MapContext = require("src.save_convert.Gen2MapContext")
local function fixture()
return {
pokemon = {
PIKACHU = { index = 25 },
CYNDAQUIL = { index = 155 },
TOTODILE = { index = 158 },
CHIKORITA = { index = 152 },
},
moves = {
TACKLE = { index = 33, pp = 35 },
GROWL = { index = 45, pp = 40 },
},
items = {
POKE_BALL = { index = 4, pocket = "BALL" },
POTION = { index = 20, pocket = "ITEM" },
FLOWER_MAIL = { index = 158, pocket = "ITEM" },
},
maps = { PLAYERS_HOUSE_2F = {
group = 24, map = 7, objectEventsAddr = 0x5CF0,
width = 4, height = 3,
blocks = { 4, 1, 3, 2, 5, 6, 5, 5, 5, 5, 7, 5 },
objects = {},
} },
}
end
local function u8(bytes, at) return bytes:byte(at + 1) end
local function be(bytes, at, n)
local v = 0
for i = 0, n - 1 do v = v * 256 + bytes:byte(at + i + 1) end
return v
end
print("[test] 1. Active Box (sBox) in Bank 1 serialization & parity")
do
for _, version in ipairs({ "gold", "crystal" }) do
local L = Gen2Save.layoutFor(version)
local save = {
generation = 2,
player = { name = "ASH", id = 12345, money = 5000 },
rival = { name = "GARY" }, mom = { name = "MOM" },
position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 },
party = {},
currentBox = 2,
boxes = {
[1] = {
{ species = "CYNDAQUIL", level = 5, moves = { { id = "TACKLE" } } }
},
[2] = {
{ species = "TOTODILE", level = 10, nickname = "TOTO", ot = "ASH", otId = 12345,
moves = { { id = "TACKLE", pp = 35, ppUps = 0 } } },
{ species = "PIKACHU", level = 12, nickname = "PIKA", ot = "ASH", otId = 12345,
moves = { { id = "GROWL", pp = 40, ppUps = 1 } } }
}
},
playTime = { hours = 10, minutes = 25, seconds = 42, frames = 15 }
}
local bytes, err = Gen2Save.encode(save, version, nil, fixture())
check(bytes ~= nil, version .. ": save encodes with active box -- " .. tostring(err))
eq(#bytes, Gen2Save.SAVE_SIZE, version .. ": 32 KB save size")
-- Check sBox in Bank 1
eq(u8(bytes, L.sBox), 2, version .. ": sBox count matches active box (box 2)")
eq(u8(bytes, L.sBox + 1), 158, version .. ": sBox species 1 is TOTODILE (158)")
eq(u8(bytes, L.sBox + 2), 25, version .. ": sBox species 2 is PIKACHU (25)")
eq(u8(bytes, L.sBox + 3), 0xFF, version .. ": sBox species list terminated with 0xFF")
-- sBox in Bank 1 matches sBox2 in Bank 2 byte-for-byte
local box2Base = L.boxes[2]
local sBoxSlice = bytes:sub(L.sBox + 1, L.sBox + 1102)
local box2Slice = bytes:sub(box2Base + 1, box2Base + 1102)
eq(sBoxSlice, box2Slice, version .. ": sBox in Bank 1 is byte-identical to active sBox2 in Bank 2")
-- Check primary checksum validity
eq(Gen2Save.checksumValid(bytes, L), true, version .. ": primary checksum is valid")
end
end
print("[test] 2. Crystal Bank 0 Backup Save & Checksums")
do
local L = Gen2Save.layoutFor("crystal")
local save = {
generation = 2,
player = { name = "KRIS", id = 54321, money = 9999, gender = "female" },
rival = { name = "SILVER" }, mom = { name = "MOM" },
position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 },
party = {
{ species = "CHIKORITA", level = 5, moves = { { id = "TACKLE" } } }
},
boxes = {},
playTime = { hours = 1, minutes = 2, seconds = 3, frames = 4 }
}
local bytes, err = Gen2Save.encode(save, "crystal", nil, fixture())
check(bytes ~= nil, "crystal: encode succeeds")
-- Verify Backup Check Values in Bank 0
eq(u8(bytes, L.backup.sCheckValue1), 0x63, "sBackupCheckValue1 at 0x1208 is 0x63")
eq(u8(bytes, L.backup.sCheckValue2), 0x7F, "sBackupCheckValue2 at 0x1F0F is 0x7F")
-- Verify backup checksum passes Gen2Save.checksumValid on backup layout
eq(Gen2Save.checksumValid(bytes, L.backup), true, "backup save checksum validates cleanly in Bank 0")
-- Verify player name in backup
local backName = {}
for i = 0, 10 do
local c = u8(bytes, L.backup.wPlayerName + i)
if c == 0x50 then break end
backName[#backName + 1] = Gen2Layout.charmap[c]
end
eq(table.concat(backName), "KRIS", "backup wPlayerName matches primary")
end
print("[test] 3. String Padding & 0x50 Fill (No Buffer Overrun)")
do
local L = Gen2Save.layoutFor("gold")
local save = {
generation = 2,
player = { name = "RED", id = 100 },
rival = { name = "BLUE" }, mom = { name = "MOMMY" },
position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 },
party = {
{ species = "PIKACHU", nickname = "SPARK", ot = "RED", otId = 100 }
},
boxNames = { [1] = "MAIN" },
boxes = {},
}
local bytes, err = Gen2Save.encode(save, "gold", nil, fixture())
check(bytes ~= nil, "encode succeeds")
-- Verify 11-byte player name: 'R', 'E', 'D', followed by eight 0x50 bytes
eq(u8(bytes, L.wPlayerName + 0), 0x91, "P1 'R'")
eq(u8(bytes, L.wPlayerName + 1), 0x84, "P2 'E'")
eq(u8(bytes, L.wPlayerName + 2), 0x83, "P3 'D'")
for i = 3, 10 do
eq(u8(bytes, L.wPlayerName + i), 0x50, "Player name padded with 0x50 at byte " .. i)
end
-- Verify 9-byte box name: 'M', 'A', 'I', 'N', followed by five 0x50 bytes
eq(u8(bytes, L.wBoxNames + 0), 0x8C, "B1 'M'")
eq(u8(bytes, L.wBoxNames + 1), 0x80, "B2 'A'")
eq(u8(bytes, L.wBoxNames + 2), 0x88, "B3 'I'")
eq(u8(bytes, L.wBoxNames + 3), 0x8D, "B4 'N'")
for i = 4, 8 do
eq(u8(bytes, L.wBoxNames + i), 0x50, "Box name padded with 0x50 at byte " .. i)
end
-- Verify party mon nickname padding
local nickBase = L.wPartyMonNicknames
eq(u8(bytes, nickBase + 0), 0x92, "N1 'S'")
eq(u8(bytes, nickBase + 1), 0x8F, "N2 'P'")
eq(u8(bytes, nickBase + 2), 0x80, "N3 'A'")
eq(u8(bytes, nickBase + 3), 0x91, "N4 'R'")
eq(u8(bytes, nickBase + 4), 0x8A, "N5 'K'")
for i = 5, 10 do
eq(u8(bytes, nickBase + i), 0x50, "Nickname padded with 0x50 at byte " .. i)
end
end
print("[test] 4. Play Time Seconds & Frames Roundtrip")
do
local save = {
generation = 2,
player = { name = "TIME", id = 999 },
position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 },
party = {}, boxes = {},
playTime = { hours = 123, minutes = 45, seconds = 56, frames = 29 }
}
local bytes = Gen2Save.encode(save, "gold", nil, fixture())
local decoded = Gen2Save.decode(bytes, "gold")
eq(decoded.playTime.hours, 123, "hours roundtrip")
eq(decoded.playTime.minutes, 45, "minutes roundtrip")
eq(decoded.playTime.seconds, 56, "seconds roundtrip")
eq(decoded.playTime.frames, 29, "frames roundtrip")
end
print("[test] 5. Default Base Happiness to 70")
do
local save = {
generation = 2,
player = { name = "HAP", id = 111 },
position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 },
party = {
{ species = "CYNDAQUIL", level = 5 } -- happiness is nil
},
boxes = {
[1] = {
{ species = "TOTODILE", level = 5 } -- happiness is nil
}
}
}
local bytes = Gen2Save.encode(save, "gold", nil, fixture())
local decoded = Gen2Save.decode(bytes, "gold")
eq(decoded.party[1].happiness, 70, "party mon unset happiness defaults to 70")
eq(decoded.boxes[1][1].happiness, 70, "box mon unset happiness defaults to 70")
end
print("[test] 6. Bank 0 Party Mail Struct Synchronization")
do
local save = {
generation = 2,
player = { name = "MAILER", id = 7777 },
position = { map = "PLAYERS_HOUSE_2F", x = 3, y = 3 },
party = {
{ species = "PIKACHU", item = "FLOWER_MAIL", ot = "MAILER", otId = 7777 },
{ species = "TOTODILE", item = "POTION" } -- not holding mail
},
mail = {
party = {
[1] = {
type = "FLOWER_MAIL",
message = "HELLO WORLD",
author = "MAILER",
authorId = 7777,
species = "PIKACHU",
}
}
},
boxes = {},
}
local bytes = Gen2Save.encode(save, "crystal", nil, fixture())
-- Slot 1 Mail in Bank 0 (0x0000)
eq(be(bytes, 0x0000 + 43, 2), 7777, "Slot 1 mail authorId matches")
eq(u8(bytes, 0x0000 + 45), 25, "Slot 1 mail species matches PIKACHU (25)")
eq(u8(bytes, 0x0000 + 46), 158, "Slot 1 mail type matches FLOWER_MAIL (158)")
-- Slot 2 (no mail) is zeroed in Bank 0 (0x002F)
eq(u8(bytes, 47), 0, "Slot 2 (no mail) is 0x00")
end
print("[test] 7. Full Save Roundtrip & Parity Verification")
do
local fullFixture = {
pokemon = {
CHIKORITA = { index = 152 },
BAYLEEF = { index = 153 },
CYNDAQUIL = { index = 155 },
TYPHLOSION = { index = 157 },
TOTODILE = { index = 158 },
PIKACHU = { index = 25 },
TOGEPI = { index = 175 },
MAREEP = { index = 179 },
AMPHAROS = { index = 181 },
},
moves = {
TACKLE = { index = 33, pp = 35 },
GROWL = { index = 45, pp = 40 },
EMBER = { index = 52, pp = 25 },
THUNDERSHOCK = { index = 84, pp = 30 },
},
items = {
MASTER_BALL = { index = 1, pocket = "BALL" },
POKE_BALL = { index = 4, pocket = "BALL" },
POTION = { index = 20, pocket = "ITEM" },
BERRY = { index = 77, pocket = "ITEM" },
FLOWER_MAIL = { index = 158, pocket = "ITEM" },
BICYCLE = { index = 7, pocket = "KEY_ITEM" },
TM01 = { index = 191, pocket = "TM_HM", tmNumber = 1 },
HM01 = { index = 241, pocket = "TM_HM", tmNumber = 51 },
},
maps = { PLAYERS_HOUSE_2F = {
group = 24, map = 7, objectEventsAddr = 0x5CF0,
width = 4, height = 3,
blocks = { 4, 1, 3, 2, 5, 6, 5, 5, 5, 5, 7, 5 },
objects = {},
} },
}
for _, version in ipairs({ "gold", "crystal" }) do
local richSave = {
generation = 2,
player = {
name = "GOLD", id = 42105, gender = "female", money = 87654, coins = 1250,
badges = { ZEPHYR = true, HIVE = true, PLAIN = true, FOG = true },
kantoBadges = { BOULDER = true, CASCADE = true },
},
rival = { name = "SILVER" }, mom = { name = "MOM" },
position = { map = "PLAYERS_HOUSE_2F", mapGroup = 24, mapNumber = 7, x = 3, y = 3 },
playTime = { hours = 48, minutes = 32, seconds = 17, frames = 45 },
currentBox = 2,
boxNames = { [1] = "STARTERS", [2] = "ELECTRIC" },
party = {
{
species = "TYPHLOSION", level = 42, nickname = "CINDER", ot = "GOLD", otId = 42105,
item = "BERRY", experience = 84500, hp = 135, maxHp = 135,
stats = { hp = 135, attack = 95, defense = 88, speed = 110, specialAttack = 119, specialDefense = 95 },
statExp = { hp = 1200, attack = 1500, defense = 900, speed = 2100, special = 1800 },
dvs = { attack = 14, defense = 12, speed = 15, special = 13 },
moves = { { id = "EMBER", pp = 25, ppUps = 0 } },
happiness = 220, pokerus = 0, caughtData = 0x4815,
},
},
boxes = {
[1] = {
{ species = "CHIKORITA", level = 5, nickname = "CHIQUI", ot = "GOLD", otId = 42105,
moves = { { id = "TACKLE", pp = 35, ppUps = 0 } }, dvs = { attack = 12, defense = 11, speed = 10, special = 14 } },
},
[2] = {
{ species = "PIKACHU", level = 20, nickname = "SPARK", ot = "ASH", otId = 10001,
moves = { { id = "THUNDERSHOCK", pp = 30, ppUps = 0 } }, dvs = { attack = 15, defense = 15, speed = 15, special = 15 } },
},
},
inventory = {
POTION = 5, BERRY = 10, MASTER_BALL = 1, POKE_BALL = 20,
BICYCLE = 1, TM01 = 1, HM01 = 1,
},
pokedex = {
caught = { CYNDAQUIL = true, TYPHLOSION = true, PIKACHU = true },
seen = { CHIKORITA = true, BAYLEEF = true, CYNDAQUIL = true, TYPHLOSION = true, PIKACHU = true },
},
events = { [0] = 0x55, [1] = 0xAA },
}
local savBytes, encErr = Gen2Save.encode(richSave, version, nil, fullFixture)
check(savBytes ~= nil, version .. ": full save encode succeeds -- " .. tostring(encErr))
local decoded, decErr = Gen2Save.decode(savBytes, version, fullFixture)
check(decoded ~= nil, version .. ": full save decode succeeds -- " .. tostring(decErr))
eq(decoded.player.name, richSave.player.name, version .. ": player name matches")
eq(decoded.player.id, richSave.player.id, version .. ": player ID matches")
eq(decoded.player.money, richSave.player.money, version .. ": player money matches")
eq(decoded.player.coins, richSave.player.coins, version .. ": player coins matches")
if version == "crystal" then
eq(decoded.player.gender, richSave.player.gender, version .. ": player gender matches")
end
eq(decoded.playTime.hours, richSave.playTime.hours, version .. ": play hours match")
eq(decoded.playTime.minutes, richSave.playTime.minutes, version .. ": play minutes match")
eq(decoded.playTime.seconds, richSave.playTime.seconds, version .. ": play seconds match")
eq(decoded.playTime.frames, richSave.playTime.frames, version .. ": play frames match")
eq(decoded.currentBox, richSave.currentBox, version .. ": current box matches")
eq(decoded.boxNames[1], "STARTERS", version .. ": box 1 name matches")
eq(decoded.boxNames[2], "ELECTRIC", version .. ": box 2 name matches")
eq(#decoded.party, 1, version .. ": party length matches")
eq(decoded.party[1].species, "TYPHLOSION", version .. ": party 1 species matches")
eq(decoded.party[1].nickname, "CINDER", version .. ": party 1 nickname matches")
eq(decoded.party[1].happiness, 220, version .. ": party 1 happiness matches")
eq(decoded.boxes[1][1].species, "CHIKORITA", version .. ": box 1 mon matches")
eq(decoded.boxes[2][1].species, "PIKACHU", version .. ": active box mon matches")
for item, count in pairs(richSave.inventory) do
eq(decoded.inventory[item], count, version .. ": inventory " .. item .. " count matches")
end
for mon in pairs(richSave.pokedex.caught) do
eq(decoded.pokedex.caught[mon], true, version .. ": dex caught " .. mon .. " matches")
end
end
end
T.finish()