feat(nx): saves inbox scan with AppleDouble/retain guards

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-03 08:01:14 -03:00
parent 8bf99c3318
commit 3c628140f7
2 changed files with 378 additions and 0 deletions
+95
View File
@@ -342,6 +342,7 @@ end
local IMPORTS_DIR = "imports"
local MODS_INBOX_DIR = "imports/mods"
local SAVES_INBOX_DIR = "imports/saves"
local ROM_BYTES = 1024 * 1024
-- Strip only a validated sdmc:/ prefix for OpenMTP/DBI relative paths.
@@ -374,6 +375,19 @@ function RomImporter:ensureModsInboxDir()
return false
end
-- NX raw .sav inbox (separate from ROM dumps + mod zips). Parent imports/
-- first — love.filesystem.createDirectory does not create nested parents.
function RomImporter:ensureSavesInboxDir()
self:ensureImportsDir()
local info = love.filesystem.getInfo(SAVES_INBOX_DIR)
if info and info.type == "directory" then return true end
if info then return false end
if love.filesystem.createDirectory then
return love.filesystem.createDirectory(SAVES_INBOX_DIR)
end
return false
end
function RomImporter:_setNxInboxNotice(version)
version = version or self.tab or "red"
local saveDir = love.filesystem.getSaveDirectory()
@@ -397,6 +411,25 @@ function RomImporter:_setNxModsInboxNotice()
}
end
function RomImporter:_resolveSaveVersion(version)
version = version or self.panelVersion or self.tab
if GameVersion.VERSIONS[version] then return version end
return self:_savedropTarget()
end
function RomImporter:_setNxSavesInboxNotice(version)
version = self:_resolveSaveVersion(version)
local saveDir = love.filesystem.getSaveDirectory()
local rel = RomImporter.mtpHintPath(saveDir)
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
self.saveNotice = self.saveNotice or {}
self.saveNotice[version] = {
ok = true,
text = Strings("Copy your .sav into:\n%s/imports/saves/\nDBI MTP → 1: SD Card/%simports/saves/",
saveDir, rel),
}
end
local function listRomPaths(dir)
local paths = {}
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
@@ -429,6 +462,22 @@ local function listZipPaths(dir)
return paths
end
local function listSavPaths(dir)
local paths = {}
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
-- Skip AppleDouble / hidden junk from Mac MTP (._foo.sav ends in .sav
-- but is not a real battery save — import would fail and invent noise).
if name:sub(1, 1) ~= "." then
local path = (dir == "" or dir == "/") and name or (dir .. "/" .. name)
if name:lower():match("%.sav$")
and love.filesystem.getInfo(path, "file") then
paths[#paths + 1] = path
end
end
end
return paths
end
function RomImporter:scanInbox(ready)
ready = ready or self.ready
local paths = {}
@@ -448,6 +497,12 @@ function RomImporter:scanModsInbox()
return listZipPaths(MODS_INBOX_DIR)
end
-- NX saves inbox: only non-hidden *.sav under imports/saves/.
function RomImporter:scanSavesInbox()
self:ensureSavesInboxDir()
return listSavPaths(SAVES_INBOX_DIR)
end
-- Rescan imports/mods/: install each .zip via _installMod / installZip.
-- Never deletes inbox zips (success or failure). Empty inbox → MTP notice.
function RomImporter:rescanModsAction()
@@ -491,6 +546,46 @@ function RomImporter:rescanModsAction()
end
end
-- Rescan imports/saves/: import each .sav via _importSave. Never deletes
-- inbox .sav files (success or failure). Empty / AppleDouble-only → MTP notice.
function RomImporter:rescanSavesAction(version)
if self.workState == "working" then return end
version = self:_resolveSaveVersion(version)
self:ensureSavesInboxDir()
local candidates = self:scanSavesInbox()
if #candidates == 0 then
self:_setNxSavesInboxNotice(version)
return
end
local anyOk = false
local lastOk = nil
local lastFail = nil
local failCount = 0
for _, path in ipairs(candidates) do
self:_importSave(version, path)
local notice = self.saveNotice and self.saveNotice[version]
if notice and notice.ok then
anyOk = true
lastOk = notice
else
failCount = failCount + 1
lastFail = notice
end
end
if anyOk and lastFail then
local okText = (lastOk and lastOk.text) or "Imported"
local failText = (lastFail and lastFail.text) or "unknown error"
self.saveNotice[version] = {
ok = true,
text = Strings("%s\n(%d failed: %s)", okText, failCount, failText),
}
elseif anyOk then
self.saveNotice[version] = lastOk
elseif lastFail then
self.saveNotice[version] = lastFail
end
end
function RomImporter:rescanAction(version)
if self.workState == "working" then return end
version = version or self.tab or "red"