mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
1905261c5b
CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
354 lines
15 KiB
Lua
354 lines
15 KiB
Lua
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
love = love or require("tests.love_stub")
|
|
|
|
local SaveSerializer = require("src.core.SaveSerializer")
|
|
local SaveData = require("src.core.SaveData")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
|
|
local realFS = love.filesystem
|
|
|
|
local function memfs(files)
|
|
return {
|
|
files = files,
|
|
write = function(path, content) files[path] = content return true end,
|
|
read = function(path) return files[path] end,
|
|
remove = function(path) files[path] = nil return true end,
|
|
getInfo = function(path)
|
|
if files[path] then return { type = "file" } end
|
|
return nil
|
|
end,
|
|
}
|
|
end
|
|
|
|
local function fresh()
|
|
local files = {}
|
|
love.filesystem = memfs(files)
|
|
SaveData.resetSlotState()
|
|
GameVersion.set("red")
|
|
return files
|
|
end
|
|
|
|
local function options(files)
|
|
return SaveSerializer.decode(files["options.lua"] or "") or {}
|
|
end
|
|
|
|
local function plainSave(name, hash)
|
|
return {
|
|
version = "red",
|
|
meta = hash and { cartHash = hash } or nil,
|
|
player = { name = name, map = "PALLET_TOWN", x = 1, y = 1 },
|
|
pokedex = { seen = {}, owned = {} },
|
|
inventory = {},
|
|
playTime = 0,
|
|
}
|
|
end
|
|
|
|
do
|
|
fresh()
|
|
T.eq(SaveData.getCart(), nil, "no cart is active by default")
|
|
T.eq(SaveData.setCart("nuzlocke", "abc123"), "nuzlocke", "setCart returns the id")
|
|
T.eq(SaveData.getCart(), "nuzlocke", "getCart reads the active cart back")
|
|
T.eq(SaveData.getCartHash(), "abc123", "the build hash rides along")
|
|
T.eq(SaveData.setCart(nil), nil, "setCart(nil) returns to vanilla play")
|
|
T.eq(SaveData.getCart(), nil, "and getCart says so")
|
|
T.eq(SaveData.getCartHash(), nil, "clearing the cart clears its build hash")
|
|
|
|
T.eq(SaveData.setCart("../evil"), nil, "a path-climbing cart id is refused")
|
|
T.eq(SaveData.setCart(".."), nil, "so is a bare parent reference")
|
|
T.eq(SaveData.setCart("has spaces"), nil, "so is a non-word id")
|
|
T.eq(SaveData.setCart(42), nil, "so is a non-string id")
|
|
T.eq(SaveData.getCart(), nil, "none of them become the active cart")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
T.eq(#SaveData.listCartSlots("nuzlocke"), 0, "a cart starts with no slots")
|
|
T.eq(SaveData.activeCartSlot("nuzlocke"), nil, "and no active slot")
|
|
T.eq(SaveData.slotCartHash("nuzlocke", "slot1"), nil, "and no stamped hash")
|
|
T.eq(files["options.lua"], nil, "listing an empty cart writes no options file")
|
|
|
|
T.eq(#SaveData.listCartSlots("../evil"), 0, "an unusable cart id lists nothing")
|
|
T.eq(SaveData.createCartSlot("../evil"), nil, "and can allocate no slot")
|
|
local ok, err = SaveData.deleteCartSlot("../evil", "slot1")
|
|
T.check(not ok, "and deleting from it fails")
|
|
T.check(tostring(err):find("unknown cart", 1, true) ~= nil,
|
|
"with a user-presentable reason")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
T.eq(SaveData.createCartSlot("nuzlocke"), "slot1", "first cart slot is slot1")
|
|
T.eq(SaveData.createCartSlot("nuzlocke"), "slot2", "ids increment as they do per version")
|
|
T.eq(SaveData.setActiveCartSlot("nuzlocke", "slot2"), "slot2",
|
|
"setActiveCartSlot returns the chosen id")
|
|
T.eq(SaveData.activeCartSlot("nuzlocke"), "slot2", "and the choice is live")
|
|
|
|
local opts = options(files)
|
|
T.eq(opts.cartSlots.nuzlocke.active, "slot2", "the active id persists in options.lua")
|
|
T.eq(opts.cartSlots.nuzlocke.list[1], "slot1", "the slot list persists with it")
|
|
T.eq(opts.cartSlots.nuzlocke.list[2], "slot2", "in allocation order")
|
|
T.eq(opts.saveSlots, nil, "no per-version registry is created by cart work")
|
|
|
|
T.check(SaveData.renameCartSlot("nuzlocke", "slot1", " Hardcore "),
|
|
"renameCartSlot labels a registered slot")
|
|
T.eq(options(files).cartSlots.nuzlocke.names.slot1, "Hardcore",
|
|
"the label is trimmed and persisted")
|
|
T.eq(SaveData.listCartSlots("nuzlocke")[1].label, "Hardcore",
|
|
"listCartSlots carries the label")
|
|
T.check(SaveData.renameCartSlot("nuzlocke", "slot1", ""), "an empty name clears it")
|
|
T.eq(options(files).cartSlots.nuzlocke.names, nil,
|
|
"the names table leaves the registry once empty")
|
|
|
|
local bad, badErr = SaveData.renameCartSlot("nuzlocke", "slot99", "x")
|
|
T.check(not bad, "renaming an unregistered cart slot fails")
|
|
T.check(tostring(badErr):find("not registered", 1, true) ~= nil,
|
|
"unknown-slot rename error is user-presentable")
|
|
|
|
T.check(SaveData.writeCartSlot("nuzlocke", "slot2", plainSave("NUZ")),
|
|
"seed the active cart slot")
|
|
T.check(files["saves/cart_nuzlocke/slot2.lua"] ~= nil,
|
|
"the bytes land in the cart's own directory")
|
|
|
|
T.check(SaveData.deleteCartSlot("nuzlocke", "slot2"), "deleteCartSlot removes it")
|
|
T.eq(files["saves/cart_nuzlocke/slot2.lua"], nil, "the slot file is gone")
|
|
opts = options(files)
|
|
T.eq(#opts.cartSlots.nuzlocke.list, 1, "the id is dropped from the registry")
|
|
T.eq(opts.cartSlots.nuzlocke.active, "slot1",
|
|
"active falls back to the remaining slot")
|
|
T.eq(SaveData.activeCartSlot("nuzlocke"), "slot1", "and the live cache follows")
|
|
|
|
T.check(SaveData.deleteCartSlot("nuzlocke", "slot1"), "deleting the last slot works")
|
|
T.eq(#SaveData.listCartSlots("nuzlocke"), 0, "the cart is empty again")
|
|
T.eq(options(files).cartSlots.nuzlocke.active, nil, "active clears with the list")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
T.eq(SaveData.createCartSlot("alpha"), "slot1", "alpha allocates its own slot1")
|
|
T.eq(SaveData.createCartSlot("beta"), "slot1", "beta allocates its own slot1")
|
|
T.check(SaveData.writeCartSlot("alpha", "slot1", plainSave("AAA", "aaa111")),
|
|
"seed alpha's slot")
|
|
T.check(SaveData.writeCartSlot("beta", "slot1", plainSave("BBB", "bbb222")),
|
|
"seed beta's slot")
|
|
|
|
T.eq(#SaveData.listCartSlots("alpha"), 1, "alpha lists only its own slot")
|
|
T.eq(#SaveData.listCartSlots("beta"), 1, "beta lists only its own slot")
|
|
T.eq(SaveData.listCartSlots("alpha")[1].name, "AAA", "alpha reads its own save")
|
|
T.eq(SaveData.listCartSlots("beta")[1].name, "BBB", "beta reads its own save")
|
|
T.check(files["saves/cart_alpha/slot1.lua"] ~= files["saves/cart_beta/slot1.lua"],
|
|
"two carts with the same slot id write different files")
|
|
T.eq(SaveData.slotCartHash("alpha", "slot1"), "aaa111", "alpha's build stamp")
|
|
T.eq(SaveData.slotCartHash("beta", "slot1"), "bbb222", "beta's build stamp")
|
|
|
|
T.check(SaveData.deleteCartSlot("alpha", "slot1"), "delete alpha's only slot")
|
|
T.eq(#SaveData.listCartSlots("alpha"), 0, "alpha is empty")
|
|
T.eq(#SaveData.listCartSlots("beta"), 1, "beta is untouched")
|
|
T.check(files["saves/cart_beta/slot1.lua"] ~= nil, "beta's file survives")
|
|
T.eq(SaveData.slotCartHash("beta", "slot1"), "bbb222", "as does its stamp")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
T.eq(SaveData.createSlot("red"), "slot1", "red allocates a version slot")
|
|
T.eq(SaveData.createCartSlot("red"), "slot1",
|
|
"a cart may even be named after a version")
|
|
T.check(SaveData.writeSlot("red", "slot1", plainSave("VANILLA")),
|
|
"seed the version slot")
|
|
T.check(SaveData.writeCartSlot("red", "slot1", plainSave("CARTRED")),
|
|
"seed the cart slot")
|
|
|
|
T.eq(#SaveData.listSlots("red"), 1, "the version lists only its own slot")
|
|
T.eq(SaveData.listSlots("red")[1].name, "VANILLA", "with the vanilla save in it")
|
|
T.eq(#SaveData.listCartSlots("red"), 1, "the cart lists only its own slot")
|
|
T.eq(SaveData.listCartSlots("red")[1].name, "CARTRED", "with the cart save in it")
|
|
T.check(files["saves/red/slot1.lua"] ~= nil, "the version path is saves/red/")
|
|
T.check(files["saves/cart_red/slot1.lua"] ~= nil, "the cart path is saves/cart_red/")
|
|
T.eq(SaveData.listSlots("red")[1].cartHash, nil,
|
|
"a version slot carries no cart hash")
|
|
|
|
T.check(SaveData.deleteCartSlot("red", "slot1"), "uninstall-style cart slot delete")
|
|
T.eq(#SaveData.listSlots("red"), 1, "the vanilla slot is still registered")
|
|
T.check(files["saves/red/slot1.lua"] ~= nil, "and its file is not orphaned")
|
|
T.eq(options(files).saveSlots.red.list[1], "slot1",
|
|
"the version registry is independent of the cart one")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
SaveData.setCart("nuzlocke", "abc123")
|
|
T.eq(SaveData.saveFilename("red"), "save_cart_nuzlocke.lua",
|
|
"with no cart slot yet, the flat path follows the version suffix scheme")
|
|
|
|
SaveData.createCartSlot("nuzlocke")
|
|
SaveData.setActiveCartSlot("nuzlocke", "slot1")
|
|
T.eq(SaveData.saveFilename("red"), "saves/cart_nuzlocke/slot1.lua",
|
|
"the active cart slot owns the save path")
|
|
|
|
local save = SaveData.newGame()
|
|
save.player.name = "NUZ"
|
|
T.check(SaveData.save(save, {}), "an in-game save under a cart writes")
|
|
T.check(files["saves/cart_nuzlocke/slot1.lua"] ~= nil, "into the cart's slot")
|
|
T.eq(files["save.lua"], nil, "never into the base game's flat file")
|
|
T.eq(files["saves/red/slot1.lua"], nil, "never into the base game's slot")
|
|
T.eq(#SaveData.listSlots("red"), 0, "and the base game still has no slots")
|
|
|
|
local loaded = SaveData.load("red")
|
|
T.check(loaded and loaded.player.name == "NUZ", "load reads the cart's slot back")
|
|
T.eq(loaded.meta.cartHash, "abc123", "the save records the build it was made under")
|
|
T.eq(SaveData.slotCartHash("nuzlocke", "slot1"), "abc123",
|
|
"and the registry mirrors it for a listing with no save decode")
|
|
T.eq(SaveData.listCartSlots("nuzlocke")[1].cartHash, "abc123",
|
|
"listCartSlots surfaces the stamp")
|
|
end
|
|
|
|
do
|
|
fresh()
|
|
SaveData.setCart("nuzlocke", "abc123")
|
|
SaveData.createCartSlot("nuzlocke")
|
|
SaveData.setActiveCartSlot("nuzlocke", "slot1")
|
|
T.check(SaveData.save(SaveData.newGame(), {}), "first save under build abc123")
|
|
|
|
local loaded = SaveData.load("red")
|
|
T.check(SaveData.save(loaded, {}), "a re-save rebuilds meta")
|
|
T.eq(SaveData.load("red").meta.cartHash, "abc123",
|
|
"buildMeta carries the stamp instead of dropping it")
|
|
|
|
SaveData.setCartHash("def456")
|
|
T.eq(SaveData.slotCartHash("nuzlocke", "slot1"), "abc123",
|
|
"installing an update does not restamp the slot")
|
|
loaded = SaveData.load("red")
|
|
T.eq(loaded.meta.cartHash, "abc123", "nor the in-progress save")
|
|
T.check(SaveData.save(loaded, {}), "save under the new build")
|
|
T.eq(SaveData.load("red").meta.cartHash, "def456", "now the save carries it")
|
|
T.eq(SaveData.slotCartHash("nuzlocke", "slot1"), "def456", "and so does the registry")
|
|
|
|
local ok, err = SaveData.setSlotCartHash("nuzlocke", "slot9", "zzz")
|
|
T.check(not ok, "an unregistered slot cannot be stamped")
|
|
T.check(tostring(err):find("not registered", 1, true) ~= nil,
|
|
"with a user-presentable reason")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
SaveData.createSlot("red")
|
|
SaveData.setActiveSlot("red", "slot1")
|
|
local save = SaveData.newGame()
|
|
save.player.name = "VANILLA"
|
|
T.check(SaveData.save(save, {}), "a vanilla save with no cart set")
|
|
|
|
local bytes = files["saves/red/slot1.lua"]
|
|
T.check(bytes:find("cartHash", 1, true) == nil, "records no cartHash")
|
|
T.check(files["options.lua"]:find("cartSlots", 1, true) == nil,
|
|
"and options.lua grows no cart registry")
|
|
T.eq(options(files).cartSlots, nil, "which decodes as absent, not empty")
|
|
|
|
local loaded = SaveData.load("red")
|
|
T.check(SaveData.save(loaded), "re-save the migrated shape once")
|
|
local before = files["saves/red/slot1.lua"]
|
|
SaveData.setCart("nuzlocke", "abc123")
|
|
T.eq(SaveData.saveFilename("red"), "save_cart_nuzlocke.lua",
|
|
"the cart takes over the path while it is active")
|
|
SaveData.setCart(nil)
|
|
T.eq(SaveData.saveFilename("red"), "saves/red/slot1.lua",
|
|
"and hands it straight back")
|
|
T.check(SaveData.save(loaded), "re-save after the cart round trip")
|
|
T.eq(files["saves/red/slot1.lua"], before, "the vanilla bytes are identical")
|
|
T.check(files["options.lua"]:find("cartSlots", 1, true) == nil,
|
|
"and options.lua still carries no cart registry")
|
|
|
|
local again = SaveData.load("red")
|
|
T.check(again and again.player.name == "VANILLA", "the vanilla save still loads")
|
|
T.eq(again.meta.cartHash, nil, "with no cart stamp on it")
|
|
end
|
|
|
|
-- ------- cart-scoped settings
|
|
|
|
do
|
|
local files = fresh()
|
|
local base = SaveData.loadOptions()
|
|
base.textSpeed = 5
|
|
base.musicVol = 2
|
|
base.speedBattle = 1
|
|
T.check(SaveData.saveOptions(base), "the base game writes its own settings")
|
|
local plain = files["options.lua"]
|
|
|
|
SaveData.setCart("nuzlocke", "hash1")
|
|
local inCart = SaveData.loadOptions()
|
|
T.eq(inCart.textSpeed, 5, "a cart starts from the global value")
|
|
T.eq(inCart.musicVol, 2, "for every key, scoped or not")
|
|
inCart.textSpeed = 1
|
|
inCart.musicVol = 7
|
|
inCart.speedBattle = 4
|
|
T.check(SaveData.saveOptions(inCart), "change some settings inside the cart")
|
|
|
|
local stored = options(files)
|
|
T.eq(stored.textSpeed, 5, "the global text speed is untouched")
|
|
T.eq(stored.cartOptions.nuzlocke.textSpeed, 1, "the cart keeps its own")
|
|
T.eq(stored.cartOptions.nuzlocke.speedBattle, 4, "and its own battle speed")
|
|
T.eq(stored.musicVol, 7, "an unscoped setting is written globally")
|
|
T.eq(stored.cartOptions.nuzlocke.musicVol, nil, "and never lands in the cart")
|
|
|
|
T.eq(SaveData.loadOptions().textSpeed, 1, "the cart reads its value back")
|
|
T.eq(SaveData.loadOptions().musicVol, 7, "and the machine's shared volume")
|
|
|
|
SaveData.setCart("marathon", "hash2")
|
|
T.eq(SaveData.loadOptions().textSpeed, 5,
|
|
"another cart does not see the first cart's setting")
|
|
local other = SaveData.loadOptions()
|
|
other.textSpeed = 3
|
|
T.check(SaveData.saveOptions(other), "the second cart sets its own")
|
|
T.eq(options(files).cartOptions.nuzlocke.textSpeed, 1,
|
|
"which leaves the first cart's alone")
|
|
|
|
SaveData.setCart(nil)
|
|
T.eq(SaveData.loadOptions().textSpeed, 5, "and the base game keeps the global")
|
|
T.eq(SaveData.loadOptions().speedBattle, 1, "for every scoped key")
|
|
|
|
T.check(plain:find("cartOptions", 1, true) ~= nil,
|
|
"the options file declares the overlay")
|
|
T.check(plain:find("nuzlocke", 1, true) == nil,
|
|
"but an install that never ran a cart names no cart in it")
|
|
SaveData.setCart(nil)
|
|
local vanilla = SaveData.loadOptions()
|
|
vanilla.textSpeed = 4
|
|
T.check(SaveData.saveOptions(vanilla), "a base-game write after playing carts")
|
|
T.eq(options(files).cartOptions.nuzlocke.textSpeed, 1,
|
|
"leaves every cart's overlay intact")
|
|
T.eq(options(files).cartOptions.marathon.textSpeed, 3, "for every cart")
|
|
end
|
|
|
|
do
|
|
local files = fresh()
|
|
SaveData.setCart("author_cart", "hash3")
|
|
T.eq(SaveData.seedCartOptions({ textSpeed = 1, animations = false,
|
|
musicVol = 0, nonesuch = 3 }), true, "a cart seeds its shipped settings")
|
|
local seeded = SaveData.loadOptions()
|
|
T.eq(seeded.textSpeed, 1, "the author's text speed applies")
|
|
T.eq(seeded.animations, false, "and the author's battle animation choice")
|
|
T.eq(seeded.musicVol, 7, "a machine setting is never seeded")
|
|
T.eq(options(files).cartOptions.author_cart.musicVol, nil, "not even into the cart")
|
|
T.eq(options(files).cartOptions.author_cart.nonesuch, nil,
|
|
"and a key outside the cart-scoped set is ignored")
|
|
T.eq(options(files).cartOptionsSeeded.author_cart, true, "the seed is recorded")
|
|
|
|
local player = SaveData.loadOptions()
|
|
player.textSpeed = 5
|
|
T.check(SaveData.saveOptions(player), "the player then picks their own speed")
|
|
T.eq(SaveData.seedCartOptions({ textSpeed = 1, animations = false }), false,
|
|
"a second boot re-seeds nothing")
|
|
T.eq(SaveData.loadOptions().textSpeed, 5, "so the player's change survives")
|
|
|
|
SaveData.setCart(nil)
|
|
T.eq(SaveData.loadOptions().textSpeed, 3,
|
|
"and the base game still has the shipped default of its own")
|
|
T.eq(SaveData.seedCartOptions({ textSpeed = 1 }), false,
|
|
"seeding with no cart active does nothing")
|
|
end
|
|
|
|
love.filesystem = realFS
|
|
|
|
T.finish("cart_saves")
|