Merge pull request #2207 from wild1walker/claude/gen2-cart-save-scope

This commit is contained in:
bryanthaboi
2026-09-09 07:46:50 -04:00
committed by GitHub
2 changed files with 93 additions and 4 deletions
+39 -4
View File
@@ -118,13 +118,34 @@ local function saveNames(version)
-- no save and dropped CONTINUE (and a following SAVE wrote a second copy to
-- the flat path the launcher no longer looks at). Reading through the same
-- slot resolution keeps the in-game load/save and the launcher on one file.
--
-- Resolve the SCOPE the same way as well. A cart's saves are keyed by cart
-- id rather than by version: SaveData resolves every path through
-- `activeScopeKey`, which answers `cart_<id>` while one is active, and the
-- launcher lists, creates and selects a cart's slots from the `cartSlots`
-- registry (RomImporter._refreshSlots / _selectSlot / _newSlot). This
-- module asked in the version's name alone, so a Gen 2 cart read and wrote
-- the BASE GAME's playthrough. Gen 1 was unaffected because it saves
-- through SaveData, which is already cart-scoped.
local ok, SaveData = pcall(require, "src.core.SaveData")
local slot = ok and SaveData.activeSlot and SaveData.activeSlot(version) or nil
SaveData = ok and SaveData or nil
local cart = SaveData and SaveData.getCart and SaveData.getCart() or nil
if type(cart) ~= "string" or cart == "" then cart = nil end
local slot
if cart then
slot = SaveData.activeCartSlot and SaveData.activeCartSlot(cart) or nil
elseif SaveData then
slot = SaveData.activeSlot and SaveData.activeSlot(version) or nil
end
if slot then
local main = "saves/" .. version .. "/" .. slot .. ".lua"
local scope = cart and ("cart_" .. cart) or version
local main = "saves/" .. scope .. "/" .. slot .. ".lua"
return main, main .. ".bak", main .. ".tmp"
end
local main = "save" .. GameVersion.saveSuffix(version) .. ".lua"
-- SaveData.legacyNames names a cart "save_cart_<id>.lua" and a version by
-- its own suffix; this is that, so the two ends agree before slots exist.
local suffix = cart and ("_cart_" .. cart) or GameVersion.saveSuffix(version)
local main = "save" .. suffix .. ".lua"
return main, main .. ".bak", main .. ".tmp"
end
@@ -911,7 +932,21 @@ function Save.save(save)
local version = save.version
do
local ok, SaveData = pcall(require, "src.core.SaveData")
if ok and SaveData.activeSlot and not SaveData.activeSlot(version) then
-- In the CART's registry when one is active, for the same reason
-- saveNames resolves the cart's scope: otherwise the first save inside a
-- cart registers a slot against the base game and makes it active, so the
-- cart's playthrough appears in the launcher's list for the base version
-- and the player's own save there is what it overwrites.
local cart = ok and SaveData.getCart and SaveData.getCart() or nil
if type(cart) ~= "string" or cart == "" then cart = nil end
if ok and cart then
if SaveData.activeCartSlot and not SaveData.activeCartSlot(cart) then
local id = SaveData.createCartSlot and SaveData.createCartSlot(cart)
if id and SaveData.setActiveCartSlot then
SaveData.setActiveCartSlot(cart, id)
end
end
elseif ok and SaveData.activeSlot and not SaveData.activeSlot(version) then
local id = SaveData.createSlot and SaveData.createSlot(version)
if id and SaveData.setActiveSlot then
SaveData.setActiveSlot(version, id)
+54
View File
@@ -59,6 +59,60 @@ check("silver suffix", GameVersion.saveSuffix("silver"), "_silver")
-- Red keeps its historical un-suffixed name, so the two can never collide.
check("red is unsuffixed", GameVersion.saveSuffix("red"), "")
-- --------------------------------------------------------------- cart scope
--
-- A cart's saves are keyed by cart id rather than by version. SaveData
-- resolves every path that way through `activeScopeKey`, and the launcher
-- lists, creates and selects a cart's slots from the `cartSlots` registry --
-- so this module has to ask in the same scope, or a Gen 2 cart reads and
-- writes the BASE GAME's playthrough. Gen 1 never showed it because Gen 1
-- saves through SaveData itself.
do
local SaveData = require("src.core.SaveData")
local CART = "wild_crystal_nightly"
SaveData.setCart(CART, "hash")
-- Before the cart has a slot of its own: SaveData.legacyNames' cart shape.
check("a cart's flat save", (Save.filenames("gold")),
"save_cart_" .. CART .. ".lua")
check("its backup", (select(2, Save.filenames("gold"))),
"save_cart_" .. CART .. ".lua.bak")
-- Save.save registers a slot when the scope has none, and in a cart that
-- has to be the CART's registry: otherwise the cart's playthrough turns up
-- in the launcher's list for the base version, and the player's own save
-- there is what the cart then overwrites.
Save.save(Save.newGame({ playerName = "GOLD" }))
local opts = SaveData.loadOptions()
check("no phantom slot against the base game",
opts.saveSlots and opts.saveSlots.gold, nil)
local reg = opts.cartSlots and opts.cartSlots[CART]
local slot = reg and reg.list and reg.list[1]
check("one slot in the cart's own registry", reg and #reg.list, 1)
-- And it resolves to SaveData.slotDir's cart shape, which is exactly where
-- the launcher's cart slot list looks for it.
check("a cart's slot save", (Save.filenames("gold")),
"saves/cart_" .. CART .. "/" .. slot .. ".lua")
-- A slot the base game has of its own does not win over the cart's, and the
-- cart's does not follow the base game back out of the cart.
local baseSlot = SaveData.createSlot("gold")
SaveData.setActiveSlot("gold", baseSlot)
check("the cart's scope still wins", (Save.filenames("gold")),
"saves/cart_" .. CART .. "/" .. slot .. ".lua")
SaveData.setCart(nil)
check("and the base game keeps its own", (Save.filenames("gold")),
"saves/gold/" .. baseSlot .. ".lua")
-- Leave no registry behind: the checks below expect the flat name, which is
-- what a scope with no slot registered resolves to.
SaveData.deleteCartSlot(CART, slot)
SaveData.deleteSlot("gold", baseSlot)
SaveData.resetSlotState()
check("nothing is left registered", (Save.filenames("gold")), "save_gold.lua")
end
-- ----------------------------------------------------------------- new game
local fresh = Save.newGame({ playerName = "GOLD", rivalName = "SILVER" })