mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #334 from johnjohto/fix-export-fidelity
Export fidelity: name padding, wOptions, as-caught catchRate (#206)
This commit is contained in:
@@ -75,6 +75,10 @@ function Pokemon.new(data, species, level, rng)
|
||||
statExp = { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 },
|
||||
stats = stats,
|
||||
hp = stats.hp,
|
||||
-- the Gen1 catch-rate byte freezes at catch time: evolution does NOT
|
||||
-- update it, so PKHeX expects a preevolution's rate on an evolved mon
|
||||
-- (#206). Evolution.apply mutates in place and never touches this.
|
||||
catchRate = def.catchRate,
|
||||
status = nil, -- "SLP"|"PSN"|"BRN"|"FRZ"|"PAR"
|
||||
moves = moves,
|
||||
}
|
||||
|
||||
@@ -250,12 +250,17 @@ local function encodeName(buf, off, len, text)
|
||||
setByte(buf, off + i, charmap.byToken[ch] or charmap.byToken["?"] or 0x50)
|
||||
i, pos = i + 1, pos + clen
|
||||
end
|
||||
-- Write exactly ONE $50 terminator and then STOP. The bytes after it are
|
||||
-- left untouched: when encoding over a template they stay as the original
|
||||
-- save's post-terminator padding (so an unchanged name round-trips
|
||||
-- byte-identical), and on a templateless export they stay zero-filled. The
|
||||
-- game reads a name only up to the first $50, so whatever follows is inert.
|
||||
-- Write exactly ONE $50 terminator and then $50-pad the rest of the
|
||||
-- field: every real save the naming screen ever wrote fills the tail
|
||||
-- with $50, and a zero tail is what PKHeX renders as garbage glyphs
|
||||
-- after the name ("JOHN{}", #206). Template bytes that are NOT zero
|
||||
-- stay untouched, so an unchanged name still round-trips
|
||||
-- byte-identical and stale template data survives.
|
||||
if i < len then setByte(buf, off + i, 0x50) end
|
||||
for j = i + 1, len - 1 do
|
||||
local cur = buf[off + j + 1]
|
||||
if cur == nil or cur:byte() == 0 then setByte(buf, off + j, 0x50) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
@@ -734,6 +739,19 @@ function GenSave.encode(save, data, template)
|
||||
encodeName(buf, O.playerName, NAME_LENGTH, (save.player and save.player.name) or "RED")
|
||||
encodeName(buf, O.rivalName, NAME_LENGTH, (save.player and save.player.rival) or "BLUE")
|
||||
setU16be(buf, O.playerId, (save.player and save.player.id) or 0)
|
||||
-- wOptions (engine/menus/main_menu.asm InitOptions): bit 7 = battle
|
||||
-- effects OFF, bit 6 = SET style, bits 2-0 = text speed -- the recomp's
|
||||
-- textSpeed 1/3/5 are pokered's exact FAST/MEDIUM/SLOW values
|
||||
-- (SaveData.defaultOptions). Templateless exports only: with a
|
||||
-- template the byte survives untouched (the round-trip invariant), and
|
||||
-- decode() never reads it back anyway.
|
||||
if not src then
|
||||
local opts = save.options or {}
|
||||
local ob = (tonumber(opts.textSpeed) or 3) % 8
|
||||
if opts.battleStyle == "set" then ob = bit.bor(ob, 0x40) end
|
||||
if opts.animations == false then ob = bit.bor(ob, 0x80) end
|
||||
setByte(buf, O.options, ob)
|
||||
end
|
||||
setBcd(buf, O.money, 3, math.min(save.money or 0, 999999))
|
||||
setBcd(buf, O.coins, 2, math.min(save.coins or 0, 9999))
|
||||
|
||||
|
||||
@@ -493,5 +493,101 @@ else
|
||||
math.floor(rs.playTime / 3600), math.floor(rs.playTime / 60) % 60))
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- #206 export fidelity, found while auditing the reporter's PKHeX shots:
|
||||
--
|
||||
-- 1. Name fields on a templateless (engine-origin) export must be
|
||||
-- $50-PADDED after the terminator, like every real save the naming
|
||||
-- screen ever wrote -- a zero tail is what PKHeX rendered as "JOHN{}".
|
||||
-- 2. wOptions (0x2601) must carry the text speed / battle style / battle
|
||||
-- effects bits; it was never written at all.
|
||||
-- 3. The party/box catch-rate byte freezes at catch time in Gen1
|
||||
-- (evolution does NOT update it), so a mon carries its
|
||||
-- as-caught catchRate and encode() must write that, not blindly
|
||||
-- re-derive from the current species (PKHeX: "Expected a preevolution
|
||||
-- catch rate").
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
-- (1) templateless export pads every name tail with $50
|
||||
do
|
||||
local function nameField(bytes, off, label)
|
||||
local term
|
||||
for i = 0, 10 do
|
||||
if bytes:byte(off + i + 1) == 0x50 then term = i break end
|
||||
end
|
||||
check(term ~= nil, label .. ": a $50 terminator exists")
|
||||
if term then
|
||||
for i = term + 1, 10 do
|
||||
if bytes:byte(off + i + 1) ~= 0x50 then
|
||||
check(false, label .. ": tail byte " .. i .. " is $50 padding, got $"
|
||||
.. ("%02X"):format(bytes:byte(off + i + 1)))
|
||||
return
|
||||
end
|
||||
end
|
||||
check(true, label .. ": the tail is $50-padded")
|
||||
end
|
||||
end
|
||||
-- bytes2 is templateless (ASH/GARY, MEW in the party, PIKACHU boxed)
|
||||
nameField(bytes2, OFF.playerName, "player name")
|
||||
nameField(bytes2, OFF.rivalName, "rival name")
|
||||
nameField(bytes2, OFF.partyMonOT, "party OT")
|
||||
nameField(bytes2, OFF.partyMonNicks, "party nickname")
|
||||
local box3 = OFF.box1 + 2 * GenSave.BOX_REGION_SIZE
|
||||
nameField(bytes2, box3 + 22 + 20 * 33, "box OT")
|
||||
nameField(bytes2, box3 + 22 + 20 * (33 + 11), "box nickname")
|
||||
-- ...while a template's nonzero stale bytes still survive (round-trip):
|
||||
-- the template-aware block above already proves byte-identical tails.
|
||||
end
|
||||
|
||||
-- (2) wOptions carries text speed (bits 2-0), battle style (bit 6) and
|
||||
-- battle effects off (bit 7)
|
||||
do
|
||||
local o = SaveData.newGame({ playerName = "RED" })
|
||||
o.options = SaveData.mergeOptions({ textSpeed = 1, battleStyle = "set",
|
||||
animations = false })
|
||||
local ob = GenSave.encode(o, data, nil)
|
||||
check(ob:byte(OFF.options + 1) == 0x80 + 0x40 + 1,
|
||||
("wOptions packs effects-off + set style + fast text ($%02X)"):format(
|
||||
ob:byte(OFF.options + 1)))
|
||||
local o2 = SaveData.newGame({ playerName = "RED" })
|
||||
o2.options = SaveData.defaultOptions() -- textSpeed 3, shift, animations on
|
||||
local ob2 = GenSave.encode(o2, data, nil)
|
||||
check(ob2:byte(OFF.options + 1) == 3,
|
||||
("wOptions defaults to medium text, shift style, effects on ($%02X)"):format(
|
||||
ob2:byte(OFF.options + 1)))
|
||||
-- ...but with a template the cartridge's own byte wins (the round-trip
|
||||
-- invariant): an imported save's FAST+SET options must not be flattened
|
||||
-- to the session defaults on re-export
|
||||
local tpl = {}
|
||||
for i = 1, GenSave.SAVE_SIZE do tpl[i] = string.char(0) end
|
||||
tpl[OFF.options + 1] = string.char(0xC1)
|
||||
local ob3 = GenSave.encode(o2, data, table.concat(tpl))
|
||||
check(ob3:byte(OFF.options + 1) == 0xC1,
|
||||
("wOptions survives from the template on re-export ($%02X)"):format(
|
||||
ob3:byte(OFF.options + 1)))
|
||||
end
|
||||
|
||||
-- (3) catchRate: Pokemon.new stamps the as-caught byte, evolution keeps it,
|
||||
-- encode prefers the mon's byte over the current species def
|
||||
do
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Evolution = require("src.pokemon.Evolution")
|
||||
local mon = Pokemon.new(data, "NIDORAN_F", 16)
|
||||
check(mon.catchRate == data.pokemon.NIDORAN_F.catchRate,
|
||||
"Pokemon.new stamps the species catchRate (the as-caught byte)")
|
||||
local game = { data = data, save = { pokedex = { seen = {}, owned = {} } } }
|
||||
Evolution.apply(game, mon, "NIDORINA")
|
||||
check(mon.species == "NIDORINA" and mon.catchRate == data.pokemon.NIDORAN_F.catchRate,
|
||||
"evolution preserves the as-caught catchRate byte (Gen1 quirk)")
|
||||
local s = SaveData.newGame({ playerName = "RED" })
|
||||
s.party = { mon }
|
||||
for i = 1, 12 do s.boxes = s.boxes or {}; s.boxes[i] = {} end
|
||||
local sb = GenSave.encode(s, data, nil)
|
||||
check(sb:byte(OFF.partyMons + 7 + 1) == data.pokemon.NIDORAN_F.catchRate,
|
||||
"encode writes the mon's catchRate, not the evolved species' ("
|
||||
.. sb:byte(OFF.partyMons + 7 + 1) .. " vs "
|
||||
.. data.pokemon.NIDORAN_F.catchRate .. ")")
|
||||
end
|
||||
|
||||
print(string.format("save convert: %d/%d checks passed", checks - failures, checks))
|
||||
if failures > 0 then os.exit(1) end
|
||||
|
||||
Reference in New Issue
Block a user