Merge pull request #2538 from 1Jamie/grandpas-garage

fix(save_convert): Gen 2 cartridge SRAM export parity and PKHeX compatibility
This commit is contained in:
bryanthaboi
2026-09-28 13:41:49 -04:00
committed by GitHub
4 changed files with 496 additions and 12 deletions
+12
View File
@@ -46,6 +46,8 @@ Gen2Layout.goldSilver = {
wVariableSprites = 0x23BD,
wGameTimeHours = 0x2053,
wGameTimeMinutes = 0x2055,
wGameTimeSeconds = 0x2056,
wGameTimeFrames = 0x2057,
wRedsName = 0x202C,
wGreensName = 0x2037,
wSavedAtLeastOnce = 0x2042,
@@ -64,6 +66,8 @@ Gen2Layout.goldSilver = {
wDecoBed = 0x27C1,
wDecoPoster = 0x27C4,
wScreenSave = 0x286C,
sBox = 0x2D6C,
sPartyMail = 0x0000,
sOptions = 0x2000,
sMysteryGiftUnlocked = 0x0BE3,
-- The 14 archived boxes, listed rather than strided (see BOX_COUNT).
@@ -174,6 +178,8 @@ Gen2Layout.crystal = {
wVariableSprites = 0x23BC,
wGameTimeHours = 0x2052,
wGameTimeMinutes = 0x2054,
wGameTimeSeconds = 0x2055,
wGameTimeFrames = 0x2056,
wRedsName = 0x202C,
wGreensName = 0x2037,
wSavedAtLeastOnce = 0x2042,
@@ -193,6 +199,8 @@ Gen2Layout.crystal = {
wDecoPoster = 0x27A0,
wScreenSave = 0x2847,
wPlayerGender = 0x3E3D,
sBox = 0x2D10,
sPartyMail = 0x0000,
sOptions = 0x2000,
sMysteryGiftUnlocked = 0x0BE3,
-- The 14 archived boxes, listed rather than strided (see BOX_COUNT).
@@ -323,6 +331,8 @@ Gen2Layout.crystal = {
wVariableSprites = 0x15BC,
wGameTimeHours = 0x1252,
wGameTimeMinutes = 0x1254,
wGameTimeSeconds = 0x1255,
wGameTimeFrames = 0x1256,
wRedsName = 0x122C,
wGreensName = 0x1237,
wSavedAtLeastOnce = 0x1242,
@@ -342,6 +352,8 @@ Gen2Layout.crystal = {
wDecoPoster = 0x19A0,
wScreenSave = 0x1A47,
wPlayerGender = 0x3E3D,
sBox = 0x2D10,
sPartyMail = 0x0000,
sOptions = 0x2000,
sMysteryGiftUnlocked = 0x0BE3,
boxes = { 0x4000, 0x4450, 0x48A0, 0x4CF0, 0x5140, 0x5590, 0x59E0, 0x6000, 0x6450, 0x68A0, 0x6CF0, 0x7140, 0x7590, 0x79E0 },
+120 -12
View File
@@ -461,6 +461,14 @@ function Gen2Save.decode(bytes, gameVersion, data)
party = party,
boxes = boxes,
currentBox = u8(bytes, L.wCurBox) % 16 + 1,
boxNames = (function()
if not L.wBoxNames then return nil end
local names = {}
for i = 1, 14 do
names[i] = text(bytes, L.wBoxNames + (i - 1) * 9, 9)
end
return names
end)(),
inventory = inventory,
-- Species ids, 1-based, so the set keys match save.pokedex.caught[species].
pokedex = {
@@ -481,7 +489,8 @@ function Gen2Save.decode(bytes, gameVersion, data)
playTime = {
hours = be(bytes, L.wGameTimeHours, 2),
minutes = u8(bytes, L.wGameTimeMinutes),
seconds = 0, frames = 0,
seconds = L.wGameTimeSeconds and u8(bytes, L.wGameTimeSeconds) or 0,
frames = L.wGameTimeFrames and u8(bytes, L.wGameTimeFrames) or 0,
},
}
end
@@ -567,7 +576,9 @@ local function putText(t, at, str, n)
end
written = written + 1
end
t[at + written] = 0x50
for k = written, n - 1 do
t[at + k] = 0x50
end
end
local function putBadges(t, at, owned, order)
@@ -603,7 +614,7 @@ local function putSharedMon(t, o, mon, x)
putU8(t, o + 0x17 + i, (mon.ppRaw or {})[i + 1] or (mon.pp or {})[i + 1] or 0)
end
end
putU8(t, o + 0x1B, mon.happiness or 0)
putU8(t, o + 0x1B, mon.happiness or mon.friendship or 70)
-- 0x1C-0x1E belong to the mon, not to the slot: leaving them to the template
-- means reordering the party gives slot 1 the previous occupant's pokerus
-- and caught data.
@@ -612,6 +623,22 @@ local function putSharedMon(t, o, mon, x)
putU8(t, o + 0x1F, mon.level or 0)
end
local function putBoxStruct(t, base, box, x)
box = box or {}
local count = math.min(#box, Gen2Save.BOX_CAPACITY)
putU8(t, base, count)
for i = 1, count do
local mon = box[i]
putU8(t, base + BOX_SPECIES + i - 1, indexOf(x.pokemonIndex, mon.species))
putSharedMon(t, base + BOX_MONS + (i - 1) * Gen2Save.BOX_MON_STRUCT, mon, x)
putText(t, base + BOX_OTS + (i - 1) * Gen2Save.NAME_LENGTH,
mon.ot or "", Gen2Save.NAME_LENGTH)
putText(t, base + BOX_NICKS + (i - 1) * Gen2Save.NAME_LENGTH,
mon.nickname or "", Gen2Save.NAME_LENGTH)
end
putU8(t, base + BOX_SPECIES + count, 0xFF)
end
-- The bag back into its four pockets, by each item's own `pocket`.
local function putBag(t, L, inventory, x)
local buckets = { ITEM = {}, KEY_ITEM = {}, BALL = {}, TM_HM = {} }
@@ -768,6 +795,18 @@ function Gen2Save.blankImage(gameVersion, save, data)
end
end
-- Initialize active box in Bank 1 and archived boxes in Banks 2 & 3
if L.sBox then
putU8(t, L.sBox, 0)
putU8(t, L.sBox + BOX_SPECIES, 0xFF)
end
if L.boxes then
for _, base in ipairs(L.boxes) do
putU8(t, base, 0)
putU8(t, base + BOX_SPECIES, 0xFF)
end
end
for i = 1, #PLAYER_MAP_OBJECT do
t[O.mapObjects + i - 1] = PLAYER_MAP_OBJECT[i]
end
@@ -786,6 +825,28 @@ function Gen2Save.blankImage(gameVersion, save, data)
putU8(t, L.wMapGroup, group)
putU8(t, L.wMapNumber, number)
-- Crystal backup initialization
if L.backup then
if L.backup.sOptions then
for i = 1, #DEFAULT_OPTIONS do t[L.backup.sOptions + i - 1] = DEFAULT_OPTIONS[i] end
end
if L.backup.wSavedAtLeastOnce then putU8(t, L.backup.wSavedAtLeastOnce, 1) end
if L.backup.wRedsName then putText(t, L.backup.wRedsName, "RED", Gen2Save.NAME_LENGTH) end
if L.backup.wGreensName then putText(t, L.backup.wGreensName, "GREEN", Gen2Save.NAME_LENGTH) end
if L.backup.wMomItemTriggerBalance then putBE(t, L.backup.wMomItemTriggerBalance, 2300, 3) end
if L.backup.wNumPCItems then
putU8(t, L.backup.wNumPCItems, 0)
putU8(t, L.backup.wNumPCItems + 1, 0xFF)
end
if L.backup.wBoxNames then
for i = 1, 14 do
putText(t, L.backup.wBoxNames + (i - 1) * 9, "BOX" .. i, 9)
end
end
if L.backup.sCheckValue1 then putU8(t, L.backup.sCheckValue1, 0x63) end
if L.backup.sCheckValue2 then putU8(t, L.backup.sCheckValue2, 0x7F) end
end
local out = {}
for i = 0, Gen2Save.SAVE_SIZE - 1 do out[i + 1] = string.char(t[i]) end
return table.concat(out)
@@ -860,16 +921,38 @@ function Gen2Save.encode(save, gameVersion, template, data)
if #box > Gen2Save.BOX_CAPACITY then
return nil, ("box %d holds %d, which a cartridge cannot"):format(index, #box)
end
putU8(t, base, #box)
for i, mon in ipairs(box) do
putU8(t, base + BOX_SPECIES + i - 1, indexOf(x.pokemonIndex, mon.species))
putSharedMon(t, base + BOX_MONS + (i - 1) * Gen2Save.BOX_MON_STRUCT, mon, x)
putText(t, base + BOX_OTS + (i - 1) * Gen2Save.NAME_LENGTH,
mon.ot or "", Gen2Save.NAME_LENGTH)
putText(t, base + BOX_NICKS + (i - 1) * Gen2Save.NAME_LENGTH,
mon.nickname or "", Gen2Save.NAME_LENGTH)
putBoxStruct(t, base, box, x)
end
-- Write active box in Bank 1
local curBoxIdx = math.max(1, math.min(save.currentBox or 1, 14))
local activeBox = (save.boxes or {})[curBoxIdx] or {}
if L.sBox then
putBoxStruct(t, L.sBox, activeBox, x)
end
-- Bank 0 Party Mail
local MAIL_STRUCT_LEN = 47
for i = 1, Gen2Save.PARTY_LENGTH do
local mon = party[i]
local slotBase = (L.sPartyMail or 0) + (i - 1) * MAIL_STRUCT_LEN
if mon and (mon.item and (mon.item:find("_MAIL$") or mon.item == "LITEBLUEMAIL" or mon.item == "PORTRAITMAIL")) then
local entry = (save.mail and save.mail.party and save.mail.party[i]) or {}
local msg = entry.message or ""
local author = entry.author or mon.ot or p.name or ""
local authorId = entry.authorId or mon.otId or p.id or 0
local speciesId = indexOf(x.pokemonIndex, mon.species)
local itemId = indexOf(x.itemIndex, mon.item)
putText(t, slotBase, msg, 32)
putText(t, slotBase + 32, author, 11)
putBE(t, slotBase + 43, authorId, 2)
putU8(t, slotBase + 45, speciesId)
putU8(t, slotBase + 46, itemId)
else
for k = 0, MAIL_STRUCT_LEN - 1 do
t[slotBase + k] = 0
end
end
putU8(t, base + BOX_SPECIES + #box, 0xFF)
end
-- Refuse rather than drop. Without item defs every item buckets into ITEM,
@@ -953,6 +1036,8 @@ function Gen2Save.encode(save, gameVersion, template, data)
if pt then
putBE(t, L.wGameTimeHours, pt.hours or 0, 2)
putU8(t, L.wGameTimeMinutes, pt.minutes or 0)
if L.wGameTimeSeconds then putU8(t, L.wGameTimeSeconds, pt.seconds or 0) end
if L.wGameTimeFrames then putU8(t, L.wGameTimeFrames, pt.frames or 0) end
end
putU8(t, L.sCheckValue1, 0x63)
@@ -962,6 +1047,29 @@ function Gen2Save.encode(save, gameVersion, template, data)
putU8(t, L.sChecksum, sum % 256)
putU8(t, L.sChecksum + 1, math.floor(sum / 256) % 256)
-- Crystal Backup mirroring & checksum in Bank 0
if L.backup and L.backup.sGameData and L.backup.sGameDataEnd then
local delta = L.backup.sGameData - L.sGameData
for i = L.sGameData, L.sGameDataEnd - 1 do
t[i + delta] = t[i]
end
if L.backup.sOptions and L.sOptions then
for i = 1, #DEFAULT_OPTIONS do
t[L.backup.sOptions + i - 1] = t[L.sOptions + i - 1]
end
end
if L.backup.sCheckValue1 then putU8(t, L.backup.sCheckValue1, 0x63) end
if L.backup.sCheckValue2 then putU8(t, L.backup.sCheckValue2, 0x7F) end
local bsum = 0
for i = L.backup.sGameData, L.backup.sGameDataEnd - 1 do
bsum = (bsum + t[i]) % 65536
end
if L.backup.sChecksum then
putU8(t, L.backup.sChecksum, bsum % 256)
putU8(t, L.backup.sChecksum + 1, math.floor(bsum / 256) % 256)
end
end
local out = {}
for i = 0, Gen2Save.SAVE_SIZE - 1 do out[i + 1] = string.char(t[i]) end
-- Whatever followed the 32 KiB of SRAM is the cart's RTC footer. Dropping it
+363
View File
@@ -0,0 +1,363 @@
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()
+1
View File
@@ -33,6 +33,7 @@ FIELDS = [
"wXCoord", "wYCoord", "wEventFlags", "wPlayerState",
"wStatusFlags", "wStatusFlags2", "wPokegearFlags", "wVisitedSpawns",
"wVariableSprites", "wGameTimeHours", "wGameTimeMinutes",
"wGameTimeSeconds", "wGameTimeFrames",
# engine/menus/intro_menu.asm:28 _ResetWRAM, engine/overworld/player_object.asm:19
"wRedsName", "wGreensName", "wSavedAtLeastOnce", "wSpawnAfterChampion",
"wCenteredObject", "wPlayerStruct", "wMapObjects", "wNumPCItems",