fix(save/gen2): auto-recover orphaned save slots and fix PartyMenu field moves

- Auto-recover save slots: Scan saves/<version>/ on boot to re-index orphaned slot files if options.lua is reset or loses its slot registry.
- Fix Gen 2 field moves: Make PartyMenu field actions (Strength, Surf, Cut, Flash) generation-aware so they execute safely in Gold.
- Fortify platform stability: Guard native controller/hidapi init against Android 14 receiver exceptions and ensure safe Windows file replacement.
- Add unit test coverage for slot auto-recovery in save_editor_gen2_tests.lua.
This commit is contained in:
1jamie
2026-08-14 22:16:17 -05:00
parent 72f126ae26
commit 8dfbd1daae
4 changed files with 277 additions and 5 deletions
+49 -2
View File
@@ -832,9 +832,41 @@ local function tryMigrateLegacy(version, fs)
return id
end
-- Scan the filesystem for orphaned slot files under saves/<version>/ when options.lua
-- has no registered slots for this version (e.g. options.lua was reset or lost).
local function scanDiskSlots(version, fs)
if not fs then return nil end
local dir = "saves/" .. version
local slots = {}
if fs.getDirectoryItems and fs.getInfo and fs.getInfo(dir) then
local items = pcall(fs.getDirectoryItems, dir) and fs.getDirectoryItems(dir) or {}
local numbers = {}
for _, item in ipairs(items) do
local slotId = item:match("^(slot%d+)%.lua$")
if slotId then
local n = tonumber(slotId:match("%d+"))
table.insert(numbers, { id = slotId, num = n or 0 })
end
end
table.sort(numbers, function(a, b) return a.num < b.num end)
for _, item in ipairs(numbers) do
table.insert(slots, item.id)
end
else
for i = 1, 30 do
local slotId = "slot" .. i
local path = dir .. "/" .. slotId .. ".lua"
if fs.getInfo and fs.getInfo(path) then
table.insert(slots, slotId)
end
end
end
return #slots > 0 and slots or nil
end
-- Resolve (once per version per process) which slot in-game saves use: an
-- existing registry wins; otherwise a lazy legacy migration may create
-- slot1; otherwise false, meaning the flat legacy path.
-- slot1; otherwise auto-recover disk slots; otherwise false (flat legacy path).
local function ensureVersionSlots(version, fs)
if slotsChecked[version] then return end
slotsChecked[version] = true
@@ -848,7 +880,22 @@ local function ensureVersionSlots(version, fs)
activeSlotCache[version] = reg.active or reg.list[1]
return
end
activeSlotCache[version] = tryMigrateLegacy(version, fs) or false
local migrated = tryMigrateLegacy(version, fs)
if migrated then
activeSlotCache[version] = migrated
return
end
-- Auto-recovery: if options.lua lost its slot registry, scan disk for orphaned slot files
local recovered = scanDiskSlots(version, fs)
if recovered and #recovered > 0 then
opts.saveSlots = opts.saveSlots or {}
opts.saveSlots[version] = { list = recovered, active = recovered[1] }
SaveData.saveOptions(opts, fs)
activeSlotCache[version] = recovered[1]
Logger.info("auto-recovered %d save slot(s) for %s from disk", #recovered, version)
return
end
activeSlotCache[version] = false
end
-- (body for the forward-declared saveNames.) Resolves the ACTIVE slot for