mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
Gen 2: a cart saves into its own scope, not the base game's
A cart's saves are keyed by cart id, not 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). src/core/gen2/Save.lua asked in the version's name alone. saveNames built saves/<version>/<slot>.lua or save_<suffix>.lua from the version in both branches and never consulted the active cart, so a cart on Gold, Silver or Crystal read and wrote the BASE GAME's playthrough. Gen 1 was unaffected because it saves through SaveData itself, which is already cart-scoped -- so this only showed on a Gen 2 cart. It was worse than sharing one file. Save.save opens by asking activeSlot(version) and, on nil, calling createSlot + setActiveSlot in the version's name, so the first save inside a cart registered a slot in the base game's registry and made it active: the cart's playthrough appeared in the launcher's list for the base version, and the player's own save there was what the cart then overwrote. Both sites now resolve the cart scope the way SaveData does, and the names they build are SaveData's own -- slotDir's saves/cart_<id>/ and legacyNames' save_cart_<id>.lua -- so the in-game save layer and the launcher land on one file again. tests/gen2_save_test.lua covers the cart's flat name, its slot name, the slot going into the cart's registry rather than the base game's, the cart's scope winning over a base slot, and the base game keeping its own once the cart is cleared. Four of them fail on the unpatched module. Reported as "when I select Wild Crystal to launch it loads my save from regular Crystal". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-4
@@ -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)
|
||||
|
||||
@@ -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" })
|
||||
|
||||
Reference in New Issue
Block a user