mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 23:55:22 -04:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user