Files
gen1recomp/tests/gen2_version_fixes_test.lua
T

234 lines
8.6 KiB
Lua

-- The faithfulness convention: Gold and Silver keep their original cart bugs
-- wherever the bug is not hardware-dependent, and Crystal gets the fixes
-- Crystal shipped. GameVersion.fixes(id) names the FIX, so an absent table
-- reads as "bugged like the cart".
-- luajit tests/gen2_version_fixes_test.lua
--
-- Four of pokegold's seven documented bugs are engine behaviour this port can
-- express. This file pins the two that are reachable from Lua state:
--
-- luckyNumberBoxes the Lucky Number Show's loop bound
-- halloffame the first-save PC corruption, which is NOT reachable
-- here and is pinned as such rather than emulated
--
-- surfOntoNpc lives with the field-move unit and reflectOverflow with the
-- battle unit; only the flag names are asserted here.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 version fixes")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GameVersion = require("src.core.GameVersion")
local HallOfFame = require("src.core.gen2.HallOfFame")
local Save = require("src.core.gen2.Save")
local Specials = require("src.script.gen2.Specials")
local priorVersion = GameVersion.get()
-- ---- CT-10: the API shape -------------------------------------------------
eq(type(GameVersion.fixes), "function", "GameVersion.fixes is an accessor")
eq(type(GameVersion.fixes("crystal")), "table", "and it returns a table")
for _, id in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
eq(next(GameVersion.fixes(id)), nil, id .. " fixes nothing")
end
eq(next(GameVersion.fixes("nonesuch")), nil, "an unknown id reads as {}")
for _, name in ipairs({ "luckyNumberBoxes", "surfOntoNpc", "reflectOverflow",
"sideWallArms" }) do
eq(GameVersion.fixes("crystal")[name], true, "crystal fixes " .. name)
end
do
local count = 0
for _ in pairs(GameVersion.fixes("crystal")) do count = count + 1 end
eq(count, 4, "and carries no fix name a consumer was not told about")
end
GameVersion.set("gold")
eq(next(GameVersion.fixes()), nil, "the active version is the default subject")
GameVersion.set("crystal")
eq(GameVersion.fixes().luckyNumberBoxes, true, "which follows GameVersion.set")
-- Gold's row must stay bug-shaped even if someone later hangs data off it.
GameVersion.set("gold")
check(GameVersion.info("gold").fixes == nil, "the gold row has no fixes table")
check(GameVersion.info("silver").fixes == nil, "nor does silver")
-- ---- the Lucky Number Show, boxes 10-14 -----------------------------------
--
-- pokegold/engine/events/lucky_number.asm:100-102 bounds .BoxesLoop with
-- NUM_BOXES_JP (9, pokegold/constants/pokemon_data_constants.asm:123) where
-- pokecrystal/engine/events/lucky_number.asm:99 uses NUM_BOXES (14). The OPEN
-- box is walked out of sBox before that loop starts and the loop then skips
-- it, so the current box is searched on both carts whatever its number.
local function boxSet(order)
local seen = {}
for _, index in ipairs(order) do seen[index] = true end
return seen
end
GameVersion.set("gold")
do
local order = Specials.luckyNumberBoxOrder({ currentBox = 1 })
eq(#order, 9, "Gold walks nine boxes")
eq(order[1], 1, "starting with the open one")
local seen = boxSet(order)
for index = 1, 9 do check(seen[index], "gold searches box " .. index) end
for index = 10, 14 do
check(not seen[index], "gold skips box " .. index .. " (the cart bug)")
end
end
do
local order = Specials.luckyNumberBoxOrder({ currentBox = 12 })
eq(order[1], 12, "the open box is searched first even at 12")
eq(#order, 10, "on top of the nine .BoxesLoop reaches")
check(boxSet(order)[12], "so box 12 is not skipped while it is open")
end
GameVersion.set("crystal")
do
local order = Specials.luckyNumberBoxOrder({ currentBox = 1 })
eq(#order, 14, "Crystal walks all fourteen")
local seen = boxSet(order)
for index = 1, 14 do check(seen[index], "crystal searches box " .. index) end
end
do
local order = Specials.luckyNumberBoxOrder({})
eq(order[1], 1, "a save with no currentBox opens box 1")
eq(#order, 14, "and still walks the full set")
end
-- The handler itself, over the same fixture on both editions. The party mon
-- shares three trailing digits (second prize, wScriptVar 2); the mon in box 12
-- is an exact match (first prize, wScriptVar 1), and the BEST match wins.
local function luckyVm(record)
return {
specials = {
save = function() return record end,
party = function() return record.party end,
monName = function(species) return species end,
},
setStringBuffer = function(self, value) self.stringBuffer = value end,
}
end
local function luckyFixture(boxIndex, currentBox)
local record = {
luckyNumber = 12345,
currentBox = currentBox or 1,
party = { { species = "CHIKORITA", otId = 99345 } },
boxes = {},
}
record.boxes[boxIndex] = { { species = "MAGIKARP", otId = 12345 } }
return record
end
GameVersion.set("gold")
do
local vm = luckyVm(luckyFixture(3))
Specials.HANDLERS.CheckForLuckyNumberWinners(vm)
eq(vm.scriptVar, 1, "Gold finds an exact match in box 3")
check(vm.luckyNumberInBox, "and reports it came from a box")
end
do
local vm = luckyVm(luckyFixture(12))
Specials.HANDLERS.CheckForLuckyNumberWinners(vm)
eq(vm.scriptVar, 2, "Gold never sees the same mon in box 12")
check(not vm.luckyNumberInBox, "so the party's partial match wins instead")
end
do
local vm = luckyVm(luckyFixture(12, 12))
Specials.HANDLERS.CheckForLuckyNumberWinners(vm)
eq(vm.scriptVar, 1, "unless box 12 is the box the PC has open")
end
GameVersion.set("crystal")
do
local vm = luckyVm(luckyFixture(12))
Specials.HANDLERS.CheckForLuckyNumberWinners(vm)
eq(vm.scriptVar, 1, "Crystal finds box 12 without opening it")
check(vm.luckyNumberInBox, "and reports it came from a box")
end
do
local vm = luckyVm(luckyFixture(14))
Specials.HANDLERS.CheckForLuckyNumberWinners(vm)
eq(vm.scriptVar, 1, "and box 14, the last one")
end
-- ---- the Hall of Fame, first-save PC corruption ---------------------------
--
-- pokegold/engine/events/halloffame.asm:17 is the bug marker; Crystal farcalls
-- HallOfFame_InitSaveIfNeeded there (pokecrystal/engine/menus/save.asm:470-475)
-- to run ErasePreviousSave over uninitialised SRAM. This port has no SRAM, so
-- the induction is pinned as harmless here rather than emulated.
GameVersion.set("gold")
do
local save = Save.newGame({ name = "GOLD", gender = "male" })
save.boxes[7] = { { species = "SUDOWOODO", level = 20, otId = 111 } }
save.boxNames[7] = "TREES"
save.currentBox = 7
eq(HallOfFame.count(save), 0, "a fresh save has never been inducted")
check(not HallOfFame.hasEntered(save), "and carries no status flag")
local party = { { species = "TYPHLOSION", level = 55, otId = 222,
nickname = "TYPHLO" } }
local entry, wasEntered = HallOfFame.induct(save, party)
check(entry ~= nil, "induction on a never-saved file returns a row")
check(not wasEntered, "and reports the player was not already a champion")
eq(HallOfFame.count(save), 1, "the win counter reaches one")
eq(#save.boxes[7], 1, "box 7 still holds its mon")
eq(save.boxes[7][1].species, "SUDOWOODO", "unchanged")
eq(save.boxNames[7], "TREES", "its name survives too")
eq(save.currentBox, 7, "and the open box is still open")
for index = 1, Save.NUM_BOXES do
local box = save.boxes[index]
check(box == nil or type(box) == "table",
"box " .. index .. " is a table or absent, never garbage")
end
end
-- ---- the Coin Case terminator ---------------------------------------------
--
-- pokegold/data/text/common_3.asm:341 ends _CoinCaseCountText with `done` ($57)
-- where pokecrystal/data/text/common_3.asm:1308 uses `text_end`, and
-- pokegold/home/text.asm:590-593 stops only on TX_END. The port decodes the
-- stream once at import (decodeGen2Text breaks on $57), so the cache is the
-- evidence that nothing runs past the string.
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
local textPath = cache .. "/data/generated/rom_text.lua"
local tf = io.open(textPath, "r")
if not tf then
check(true, "rom_text.lua absent : no gold cache (SKIP cache facts)")
else
tf:close()
local romText = assert(loadfile(textPath))()
eq((tostring(romText._CoinCaseCountText or ""):gsub("{DONE}$", "")),
"Coins:\n{NUM}",
"the `done` terminator still ends the decoded string")
check(not tostring(romText._CoinCaseCountText or ""):find("Raise the PP"),
"and nothing from the next label runs into it")
end
GameVersion.set(priorVersion)
S.finish()