mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
209 lines
10 KiB
Lua
209 lines
10 KiB
Lua
-- Yellow save export/import checks for #838: the codec used to run the
|
|
-- Red/Blue tables unmodified for Yellow, so (1) event flags went through
|
|
-- pokered's bit numbering even though pokeyellow renumbers wEventFlags,
|
|
-- and (2) wPikachuHappiness (pokeyellow d46f, absolute 0x271C in SRAM)
|
|
-- was never encoded or decoded. Yellow offsets are verified against the
|
|
-- pokeyellow symbol file -- no local pokeyellow checkout exists, so
|
|
-- ../pokered can only vouch for the shared R/B layout, which pokeyellow's
|
|
-- sram.asm matches byte for byte. Needs data/generated/, same as
|
|
-- tests/save_convert_tests.lua (its natural eventual home).
|
|
--
|
|
-- Run: luajit tests/save_convert_yellow_bug838_test.lua
|
|
|
|
package.path = "./?.lua;" .. package.path
|
|
_G.love = require("tests.love_stub")
|
|
|
|
local GenSave = require("src.save_convert.GenSave")
|
|
local SaveConvert = require("src.save_convert.SaveConvert")
|
|
local SaveData = require("src.core.SaveData")
|
|
|
|
local checks, failures = 0, 0
|
|
local function check(cond, msg)
|
|
checks = checks + 1
|
|
if not cond then
|
|
failures = failures + 1
|
|
print("FAIL: " .. msg)
|
|
end
|
|
end
|
|
|
|
GenSave.setCharmap(loadfile("src/save_convert/data/charmap.lua")())
|
|
|
|
local redFlags = loadfile("src/save_convert/data/event_flags.lua")()
|
|
local yellowFlags = loadfile("src/save_convert/data/event_flags_yellow.lua")()
|
|
|
|
-- Red/Blue and Yellow crosswalk sets over the same generated tables; the
|
|
-- only differences the codec keys off are the event-flag numbering and the
|
|
-- gameVersion tag (SaveConvert.ensureData stamps the same shape, #838).
|
|
local shared = {
|
|
pokemon = loadfile("data/generated/pokemon.lua")(),
|
|
moves = loadfile("data/generated/moves.lua")(),
|
|
items = loadfile("data/generated/items.lua")(),
|
|
maps = loadfile("data/generated/maps.lua")(),
|
|
}
|
|
local redData = {
|
|
pokemon = shared.pokemon, moves = shared.moves, items = shared.items,
|
|
maps = shared.maps, eventFlags = redFlags,
|
|
}
|
|
local yellowData = {
|
|
pokemon = shared.pokemon, moves = shared.moves, items = shared.items,
|
|
maps = shared.maps, eventFlags = yellowFlags, gameVersion = "yellow",
|
|
}
|
|
|
|
local OFF = GenSave.OFFSETS
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- the Yellow event-flag table itself: pokeyellow's renumbering, not a
|
|
-- copy of the Red table under a new filename
|
|
-- ------------------------------------------------------------------
|
|
|
|
check(yellowFlags.count == 2560,
|
|
"yellow table covers the full 2560-bit wEventFlags array")
|
|
check(type(yellowFlags.byName) == "table" and type(yellowFlags.byBit) == "table",
|
|
"yellow table has the byName/byBit shape the codec reads")
|
|
|
|
-- shared names on DIFFERENT bits: Yellow inserts events ahead of them
|
|
check(redFlags.byName.EVENT_GOT_DOME_FOSSIL == 1406
|
|
and yellowFlags.byName.EVENT_GOT_DOME_FOSSIL == 1400,
|
|
"EVENT_GOT_DOME_FOSSIL sits on red bit 1406 vs yellow bit 1400")
|
|
check(redFlags.byName.EVENT_BEAT_MT_MOON_3_TRAINER_0 == 1402
|
|
and yellowFlags.byName.EVENT_BEAT_MT_MOON_3_TRAINER_0 == 1403,
|
|
"the Mt Moon 3 trainer block shifts +1 in yellow (Jessie & James insert)")
|
|
check(redFlags.byName.EVENT_BEAT_SILPH_CO_11F_TRAINER_0 == 1924
|
|
and yellowFlags.byName.EVENT_BEAT_SILPH_CO_11F_TRAINER_0 == 1925,
|
|
"the Silph Co 11F trainer block shifts +1 in yellow")
|
|
|
|
-- yellow-only names the port's Yellow scripts set (data/scripts/
|
|
-- yellow_jessie_james.lua and the catch-training tutorial): absent from
|
|
-- the Red table, so exporting through it silently dropped them
|
|
check(yellowFlags.byName.EVENT_BEAT_MT_MOON_3_JESSIE_JAMES == 1402
|
|
and redFlags.byName.EVENT_BEAT_MT_MOON_3_JESSIE_JAMES == nil,
|
|
"EVENT_BEAT_MT_MOON_3_JESSIE_JAMES is yellow bit 1402, unknown to red")
|
|
check(yellowFlags.byName.EVENT_COMPLETED_CATCH_TRAINING == 45
|
|
and redFlags.byName.EVENT_COMPLETED_CATCH_TRAINING == nil,
|
|
"EVENT_COMPLETED_CATCH_TRAINING is yellow bit 45, unknown to red")
|
|
check(yellowFlags.byName.EVENT_GOT_SQUIRTLE_FROM_OFFICER_JENNY ~= nil
|
|
and redFlags.byName.EVENT_GOT_SQUIRTLE_FROM_OFFICER_JENNY == nil,
|
|
"the Officer Jenny Squirtle event exists only in the yellow table")
|
|
|
|
-- byBit/byName agree on the renumbered entries
|
|
check(yellowFlags.byBit[1400] == "EVENT_GOT_DOME_FOSSIL"
|
|
and yellowFlags.byBit[1402] == "EVENT_BEAT_MT_MOON_3_JESSIE_JAMES",
|
|
"yellow byBit resolves the renumbered bits back to their names")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- wPikachuHappiness offset: d46f - wMainDataStart d2f6 = 377 past
|
|
-- sMainData, absolute 0x271C (per the pokeyellow symbol file)
|
|
-- ------------------------------------------------------------------
|
|
|
|
check(OFF.pikachuHappiness == 10012,
|
|
"OFFSETS.pikachuHappiness is absolute 0x271C (got "
|
|
.. tostring(OFF.pikachuHappiness) .. ")")
|
|
check(OFF.pikachuHappiness == OFF.mainData + 377,
|
|
"pikachuHappiness sits 377 bytes past sMainData (wram d46f - d2f6)")
|
|
-- the byte is INSIDE the checksummed main-data window, so writing it
|
|
-- without recomputing the checksum would brick the save on a cartridge
|
|
check(OFF.pikachuHappiness >= OFF.checksumStart
|
|
and OFF.pikachuHappiness < OFF.checksumEnd,
|
|
"pikachuHappiness lies inside the main checksum window")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- encode/decode gate: yellow data writes and reads the byte, R/B data
|
|
-- leaves it alone (in Red/Blue it is current-map scratch)
|
|
-- ------------------------------------------------------------------
|
|
|
|
local save = SaveData.newGame({ playerName = "RED", rivalName = "BLUE" })
|
|
save.pikachuHappiness = 200
|
|
-- the follower seeds happiness at 90 (src/world/PikachuFollower.lua), so
|
|
-- 200 can only come from this table -- no default could fake the check
|
|
|
|
local yBytes = GenSave.encode(save, yellowData, nil)
|
|
check(#yBytes == GenSave.SAVE_SIZE, "yellow encode produces exactly 32768 bytes")
|
|
check(yBytes:byte(OFF.pikachuHappiness + 1) == 200,
|
|
"yellow encode writes pikachuHappiness to 0x271C (got "
|
|
.. yBytes:byte(OFF.pikachuHappiness + 1) .. ")")
|
|
check(GenSave.mainChecksumValid(yBytes),
|
|
"yellow encode still emits a valid main-data checksum")
|
|
local yDec = GenSave.decode(yBytes, yellowData)
|
|
check(yDec.pikachuHappiness == 200,
|
|
"yellow decode reads pikachuHappiness back (got "
|
|
.. tostring(yDec.pikachuHappiness) .. ")")
|
|
|
|
-- a yellow save whose table never held the field falls back to the
|
|
-- follower's seed value instead of exporting friendship 0
|
|
local noHap = SaveData.newGame({ playerName = "RED", rivalName = "BLUE" })
|
|
noHap.pikachuHappiness = nil
|
|
local seedBytes = GenSave.encode(noHap, yellowData, nil)
|
|
check(seedBytes:byte(OFF.pikachuHappiness + 1) == 90,
|
|
"a missing pikachuHappiness exports as the follower seed 90, not 0")
|
|
|
|
-- R/B output unchanged: same save through the red data set leaves the
|
|
-- scratch byte zero-filled and decode never invents the field
|
|
local rBytes = GenSave.encode(save, redData, nil)
|
|
check(rBytes:byte(OFF.pikachuHappiness + 1) == 0,
|
|
"red/blue encode leaves the 0x271C scratch byte zero-filled")
|
|
check(GenSave.decode(rBytes, redData).pikachuHappiness == nil,
|
|
"red/blue decode does not fabricate a pikachuHappiness field")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- event flags land on pokeyellow bits. Independent LSB-first flag_array
|
|
-- read (pokered home FlagAction convention: byte N/8, bit N%8) so the
|
|
-- assertions cannot inherit a codec bit-order bug.
|
|
-- ------------------------------------------------------------------
|
|
|
|
local bit = require("bit")
|
|
local function flagBit(bytes, index)
|
|
local b = bytes:byte(OFF.eventFlags + math.floor(index / 8) + 1)
|
|
return bit.band(bit.rshift(b, index % 8), 1) == 1
|
|
end
|
|
|
|
local fsave = SaveData.newGame({ playerName = "ASH", rivalName = "GARY" })
|
|
fsave.flags = {
|
|
EVENT_GOT_DOME_FOSSIL = true,
|
|
EVENT_BEAT_MT_MOON_3_JESSIE_JAMES = true,
|
|
EVENT_COMPLETED_CATCH_TRAINING = true,
|
|
}
|
|
|
|
local yfBytes = GenSave.encode(fsave, yellowData, nil)
|
|
check(flagBit(yfBytes, 1400) and not flagBit(yfBytes, 1406),
|
|
"yellow export puts EVENT_GOT_DOME_FOSSIL on bit 1400, not red's 1406")
|
|
check(flagBit(yfBytes, 1402),
|
|
"yellow export carries EVENT_BEAT_MT_MOON_3_JESSIE_JAMES on bit 1402")
|
|
check(flagBit(yfBytes, 45),
|
|
"yellow export carries EVENT_COMPLETED_CATCH_TRAINING on bit 45")
|
|
local yfDec = GenSave.decode(yfBytes, yellowData)
|
|
check(yfDec.flags.EVENT_GOT_DOME_FOSSIL
|
|
and yfDec.flags.EVENT_BEAT_MT_MOON_3_JESSIE_JAMES
|
|
and yfDec.flags.EVENT_COMPLETED_CATCH_TRAINING,
|
|
"yellow-numbered flags round-trip through decode")
|
|
|
|
-- the pre-fix failure mode, pinned so it can never quietly return: the
|
|
-- red table lands the fossil on the wrong yellow bit and drops the
|
|
-- yellow-only names entirely
|
|
local rfBytes = GenSave.encode(fsave, redData, nil)
|
|
check(flagBit(rfBytes, 1406) and not flagBit(rfBytes, 1400),
|
|
"the red table writes the fossil on 1406, which yellow reads as another event")
|
|
check(not flagBit(rfBytes, 1402) and not flagBit(rfBytes, 45),
|
|
"the red table silently drops both yellow-only flags")
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- SaveConvert.ensureData substitutes the yellow flag table (and stamps
|
|
-- gameVersion) when the caller names yellow; the versionless set still
|
|
-- resolves red numbering, per-version cached separately (#420 pattern)
|
|
-- ------------------------------------------------------------------
|
|
|
|
local yData, yErr = SaveConvert.loadData("yellow")
|
|
check(yData ~= nil, "SaveConvert.loadData('yellow') resolves (" .. tostring(yErr) .. ")")
|
|
check(yData and yData.gameVersion == "yellow",
|
|
"loadData('yellow') stamps gameVersion for the codec's byte gate")
|
|
check(yData and yData.eventFlags.byName.EVENT_GOT_DOME_FOSSIL == 1400
|
|
and yData.eventFlags.byName.EVENT_BEAT_MT_MOON_3_JESSIE_JAMES == 1402,
|
|
"loadData('yellow') serves the pokeyellow flag numbering")
|
|
local dData = SaveConvert.loadData()
|
|
check(dData and dData.eventFlags.byName.EVENT_GOT_DOME_FOSSIL == 1406
|
|
and dData.gameVersion == nil,
|
|
"versionless loadData still serves the red numbering, untagged")
|
|
|
|
print(string.format("save convert yellow #838: %d/%d checks passed",
|
|
checks - failures, checks))
|
|
if failures > 0 then os.exit(1) end
|