diff --git a/src/core/gen2/Save.lua b/src/core/gen2/Save.lua index ac804e3c..fe5cc7d0 100644 --- a/src/core/gen2/Save.lua +++ b/src/core/gen2/Save.lua @@ -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_` 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_.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) diff --git a/tests/gen2_save_test.lua b/tests/gen2_save_test.lua index 4ebbc859..5c3eaee3 100644 --- a/tests/gen2_save_test.lua +++ b/tests/gen2_save_test.lua @@ -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" })