mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
refactor(import): separate NX capabilities from Android flags
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+149
-21
@@ -328,6 +328,114 @@ local function commandOutput(command)
|
|||||||
return result ~= "" and result or nil
|
return result ~= "" and result or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local IMPORTS_DIR = "imports"
|
||||||
|
local ROM_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
-- Strip only a validated sdmc:/ prefix for OpenMTP/DBI relative paths.
|
||||||
|
function RomImporter.mtpHintPath(saveDir)
|
||||||
|
if type(saveDir) ~= "string" then return "" end
|
||||||
|
if saveDir:sub(1, 6) == "sdmc:/" then return saveDir:sub(7) end
|
||||||
|
return saveDir
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:ensureImportsDir()
|
||||||
|
local info = love.filesystem.getInfo(IMPORTS_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(IMPORTS_DIR)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:_setNxInboxNotice(version)
|
||||||
|
version = version or self.tab or "red"
|
||||||
|
local saveDir = love.filesystem.getSaveDirectory()
|
||||||
|
local rel = RomImporter.mtpHintPath(saveDir)
|
||||||
|
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
|
||||||
|
self.notice = {
|
||||||
|
version = version,
|
||||||
|
status = Strings("Copy your .gb/.gbc into:"),
|
||||||
|
detail = Strings("%s/imports/\nDBI MTP → 1: SD Card/%simports/", saveDir, rel),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function listRomPaths(dir)
|
||||||
|
local paths = {}
|
||||||
|
for _, name in ipairs(love.filesystem.getDirectoryItems(dir)) do
|
||||||
|
local path = (dir == "" or dir == "/") and name or (dir .. "/" .. name)
|
||||||
|
if name:lower():match("%.gbc?$")
|
||||||
|
and love.filesystem.getInfo(path, "file") then
|
||||||
|
paths[#paths + 1] = path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return paths
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:scanInbox(ready)
|
||||||
|
ready = ready or self.ready
|
||||||
|
local paths = {}
|
||||||
|
for _, path in ipairs(listRomPaths(IMPORTS_DIR)) do
|
||||||
|
paths[#paths + 1] = path
|
||||||
|
end
|
||||||
|
for _, path in ipairs(listRomPaths("")) do
|
||||||
|
-- Root scan is second; imports/ entries were already collected above.
|
||||||
|
paths[#paths + 1] = path
|
||||||
|
end
|
||||||
|
return paths
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:rescanAction(version)
|
||||||
|
if self.workState == "working" then return end
|
||||||
|
version = version or self.tab or "red"
|
||||||
|
self.chooseVersion = version
|
||||||
|
self:ensureImportsDir()
|
||||||
|
local ready = self.ready
|
||||||
|
local candidates = self:scanInbox(ready)
|
||||||
|
local sawReadyOnly = false
|
||||||
|
for _, path in ipairs(candidates) do
|
||||||
|
local data = love.filesystem.read(path)
|
||||||
|
local displayName = path:match("[^/\\]+$") or path
|
||||||
|
if type(data) ~= "string" then
|
||||||
|
self:setError("The file could not be read: " .. displayName, version)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if #data ~= ROM_BYTES then
|
||||||
|
self:startData(data, displayName)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local romVersion = GameVersion.forSha1(sha1(data))
|
||||||
|
if not romVersion then
|
||||||
|
self:startData(data, displayName)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if ready[romVersion] then
|
||||||
|
sawReadyOnly = true
|
||||||
|
else
|
||||||
|
self:startData(data, displayName)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if sawReadyOnly and #candidates > 0 then
|
||||||
|
self.notice = {
|
||||||
|
version = version,
|
||||||
|
status = Strings("No new ROM found."),
|
||||||
|
detail = Strings("Already-imported dumps are ignored. Add another version or "
|
||||||
|
.. "delete the copy when finished."),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:_setNxInboxNotice(version)
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:_romAction(version)
|
||||||
|
if self.isNX then
|
||||||
|
if self.ready[version] then self:reimport(version)
|
||||||
|
else self:rescanAction(version) end
|
||||||
|
elseif self.ready[version] then self:reimport(version)
|
||||||
|
else self:choose(version) end
|
||||||
|
end
|
||||||
|
|
||||||
-- LOVE 11.5 on Android has no native file picker (love.window.showFileDialog
|
-- LOVE 11.5 on Android has no native file picker (love.window.showFileDialog
|
||||||
-- is a LOVE 12 nightly-only addition) and never fires love.filedropped, so
|
-- is a LOVE 12 nightly-only addition) and never fires love.filedropped, so
|
||||||
-- neither desktop path below works there. conf.lua points the Android save
|
-- neither desktop path below works there. conf.lua points the Android save
|
||||||
@@ -567,8 +675,13 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- pending-file scan plus love.system.pickFile / createFile, provided
|
-- pending-file scan plus love.system.pickFile / createFile, provided
|
||||||
-- natively by the Swift GRPickerBridge (mobile/ios/native/). The flag
|
-- natively by the Swift GRPickerBridge (mobile/ios/native/). The flag
|
||||||
-- keeps its historical name so every Android call site stays untouched.
|
-- keeps its historical name so every Android call site stays untouched.
|
||||||
|
-- NX uses a separate save-directory inbox (isNX / romImportMode) and must
|
||||||
|
-- never set android or take the mobile delete-after-import path.
|
||||||
local mobileOS = love.system.getOS()
|
local mobileOS = love.system.getOS()
|
||||||
local android = mobileOS == "Android" or mobileOS == "iOS"
|
local isNX = Platform.isNX()
|
||||||
|
local romImportMode = Platform.romImportMode()
|
||||||
|
local mobileFileBridge = mobileOS == "Android" or mobileOS == "iOS"
|
||||||
|
local android = mobileFileBridge
|
||||||
local CacheFs = require("src.import.CacheFs")
|
local CacheFs = require("src.import.CacheFs")
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
onComplete = onComplete,
|
onComplete = onComplete,
|
||||||
@@ -576,6 +689,9 @@ function RomImporter.new(onComplete, opts)
|
|||||||
forceImport = opts.forceImport or false,
|
forceImport = opts.forceImport or false,
|
||||||
onEditSave = opts.onEditSave,
|
onEditSave = opts.onEditSave,
|
||||||
onEditTouchControls = opts.onEditTouchControls,
|
onEditTouchControls = opts.onEditTouchControls,
|
||||||
|
isNX = isNX,
|
||||||
|
romImportMode = romImportMode,
|
||||||
|
mobileFileBridge = mobileFileBridge,
|
||||||
android = android,
|
android = android,
|
||||||
ios = mobileOS == "iOS",
|
ios = mobileOS == "iOS",
|
||||||
-- One startup poll pass on both mobiles. iOS: files dropped through the
|
-- One startup poll pass on both mobiles. iOS: files dropped through the
|
||||||
@@ -587,14 +703,14 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- pick never arrives. The file is sitting in the save dir either way, so
|
-- pick never arrives. The file is sitting in the save dir either way, so
|
||||||
-- boot armed and let the first poll tick consume it, rather than making the
|
-- boot armed and let the first poll tick consume it, rather than making the
|
||||||
-- player tap Import a second time to trigger the scan by hand (#553).
|
-- player tap Import a second time to trigger the scan by hand (#553).
|
||||||
pickPending = android or nil,
|
pickPending = mobileFileBridge or nil,
|
||||||
-- Android drag: the launcher is handed no move events at all (main.lua
|
-- Android drag: the launcher is handed no move events at all (main.lua
|
||||||
-- forwards neither touchmoved nor mousemoved while it is up), and its mouse
|
-- forwards neither touchmoved nor mousemoved while it is up), and its mouse
|
||||||
-- emulation is what "no reliable pointer polling" below refers to.
|
-- emulation is what "no reliable pointer polling" below refers to.
|
||||||
-- love.touch IS pollable, so where it exists a touch drag can be resolved
|
-- love.touch IS pollable, so where it exists a touch drag can be resolved
|
||||||
-- inside draw the same way the desktop mouse is. Where it does not, every
|
-- inside draw the same way the desktop mouse is. Where it does not, every
|
||||||
-- Android path stays exactly as it was: act on press, never arm.
|
-- Android path stays exactly as it was: act on press, never arm.
|
||||||
touchPollable = android and love.touch ~= nil
|
touchPollable = mobileFileBridge and love.touch ~= nil
|
||||||
and love.touch.getTouches ~= nil and love.touch.getPosition ~= nil,
|
and love.touch.getTouches ~= nil and love.touch.getPosition ~= nil,
|
||||||
tab = "red", -- active launcher tab: "red"/"blue"/"yellow"/"mods"
|
tab = "red", -- active launcher tab: "red"/"blue"/"yellow"/"mods"
|
||||||
logo = love.graphics.newImage("assets/logo/logo.png"),
|
logo = love.graphics.newImage("assets/logo/logo.png"),
|
||||||
@@ -674,7 +790,7 @@ function RomImporter.new(onComplete, opts)
|
|||||||
for _, version in ipairs(GameVersion.ORDER) do
|
for _, version in ipairs(GameVersion.ORDER) do
|
||||||
if not self.ready[version] then needRom = true; break end
|
if not self.ready[version] then needRom = true; break end
|
||||||
end
|
end
|
||||||
if android and needRom then
|
if mobileFileBridge and needRom then
|
||||||
local name, data = findPendingRom(self.ready)
|
local name, data = findPendingRom(self.ready)
|
||||||
if name then
|
if name then
|
||||||
self:startData(data, name)
|
self:startData(data, name)
|
||||||
@@ -683,6 +799,9 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- is up, so a rejected pick can outlive the focus handler (#442).
|
-- is up, so a rejected pick can outlive the focus handler (#442).
|
||||||
consumePickedRomError(self)
|
consumePickedRomError(self)
|
||||||
end
|
end
|
||||||
|
elseif self.isNX and self.launcher then
|
||||||
|
self:ensureImportsDir()
|
||||||
|
self:_setNxInboxNotice()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Mouse-wheel scroll for the save-slot / mods lists. main.lua (off limits)
|
-- Mouse-wheel scroll for the save-slot / mods lists. main.lua (off limits)
|
||||||
@@ -901,10 +1020,14 @@ function RomImporter:startData(data, displayName)
|
|||||||
and (displayName:match("[^/\\]+$") or displayName)) or self.romName[version]
|
and (displayName:match("[^/\\]+$") or displayName)) or self.romName[version]
|
||||||
-- Android: drop the consumed save-dir .gb/.gbc (picked_rom.gb or a USB copy)
|
-- Android: drop the consumed save-dir .gb/.gbc (picked_rom.gb or a USB copy)
|
||||||
-- so the next Choose / focus cannot treat it as a fresh pending ROM.
|
-- so the next Choose / focus cannot treat it as a fresh pending ROM.
|
||||||
if self.android and type(displayName) == "string"
|
if self.mobileFileBridge and type(displayName) == "string"
|
||||||
and not displayName:find("[/\\]") then
|
and not displayName:find("[/\\]") then
|
||||||
love.filesystem.remove(displayName)
|
love.filesystem.remove(displayName)
|
||||||
end
|
end
|
||||||
|
if self.isNX and type(displayName) == "string" then
|
||||||
|
self.detail = Strings("%s imported. You may delete the copy from "
|
||||||
|
.. "imports/ when finished.", displayName)
|
||||||
|
end
|
||||||
self.importing = nil
|
self.importing = nil
|
||||||
self.workState = "complete"
|
self.workState = "complete"
|
||||||
self.completeVersion = version
|
self.completeVersion = version
|
||||||
@@ -1157,15 +1280,12 @@ end
|
|||||||
function RomImporter:choose(version)
|
function RomImporter:choose(version)
|
||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
self.chooseVersion = version or "red"
|
self.chooseVersion = version or "red"
|
||||||
if Platform.isNX() then
|
if self.isNX then
|
||||||
self.notice = {
|
self:ensureImportsDir()
|
||||||
version = self.chooseVersion,
|
self:_setNxInboxNotice(self.chooseVersion)
|
||||||
status = "Copy your .gb/.gbc into:",
|
|
||||||
detail = love.filesystem.getSaveDirectory() .. "/imports/",
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self.android then
|
if self.mobileFileBridge or self.android then
|
||||||
-- Prefer a not-yet-imported .gb/.gbc already in the save dir (USB copy, or
|
-- Prefer a not-yet-imported .gb/.gbc already in the save dir (USB copy, or
|
||||||
-- a fresh SAF pick). Never reuse an already-imported cart's file -- that
|
-- a fresh SAF pick). Never reuse an already-imported cart's file -- that
|
||||||
-- was the #167 failure mode (second Choose just re-extracted Red).
|
-- was the #167 failure mode (second Choose just re-extracted Red).
|
||||||
@@ -1407,7 +1527,7 @@ function RomImporter:gamepadpressed(_, button)
|
|||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
local version = self.tab
|
local version = self.tab
|
||||||
if GameVersion.VERSIONS[version] then
|
if GameVersion.VERSIONS[version] then
|
||||||
if self.ready[version] then self:play(version) else self:choose(version) end
|
if self.ready[version] then self:play(version) else self:_romAction(version) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2702,8 +2822,7 @@ function RomImporter:mousepressed(x, y, button)
|
|||||||
self:play(self.panelVersion); return
|
self:play(self.panelVersion); return
|
||||||
end
|
end
|
||||||
if inside(self.romButtonRect, x, y) then
|
if inside(self.romButtonRect, x, y) then
|
||||||
local version = self.panelVersion
|
self:_romAction(self.panelVersion)
|
||||||
if self.ready[version] then self:reimport(version) else self:choose(version) end
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- SAVE FILES card: Import save / Export save, and the open-folder affordance
|
-- SAVE FILES card: Import save / Export save, and the open-folder affordance
|
||||||
@@ -2908,7 +3027,7 @@ function RomImporter:keypressed(key)
|
|||||||
-- open its picker. The mods tab has no keyboard action.
|
-- open its picker. The mods tab has no keyboard action.
|
||||||
local version = self.tab
|
local version = self.tab
|
||||||
if GameVersion.VERSIONS[version] then
|
if GameVersion.VERSIONS[version] then
|
||||||
if self.ready[version] then self:play(version) else self:choose(version) end
|
if self.ready[version] then self:play(version) else self:_romAction(version) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3204,8 +3323,14 @@ function RomImporter:_drawGamePanel(version, x, y, w, h, paged)
|
|||||||
local rightX = twoCol and (x + colW + colGap) or x
|
local rightX = twoCol and (x + colW + colGap) or x
|
||||||
|
|
||||||
-- ROM card contents by state (rehomes the existing import flow)
|
-- ROM card contents by state (rehomes the existing import flow)
|
||||||
local dropHint = self.android and "Copy the .gb/.gbc via USB."
|
local dropHint
|
||||||
or Strings("Or drop the .gb/.gbc file here.")
|
if self.isNX then
|
||||||
|
dropHint = Strings("Copy your .gb/.gbc into the imports folder (see path above).")
|
||||||
|
elseif self.android then
|
||||||
|
dropHint = "Copy the .gb/.gbc via USB."
|
||||||
|
else
|
||||||
|
dropHint = Strings("Or drop the .gb/.gbc file here.")
|
||||||
|
end
|
||||||
local accent = version == "yellow" and PAL.gold
|
local accent = version == "yellow" and PAL.gold
|
||||||
or (version == "red" and PAL.red or PAL.blue)
|
or (version == "red" and PAL.red or PAL.blue)
|
||||||
local romState, romDetail, romBtnLabel, romBtnEnabled, romProgress
|
local romState, romDetail, romBtnLabel, romBtnEnabled, romProgress
|
||||||
@@ -3227,11 +3352,13 @@ function RomImporter:_drawGamePanel(version, x, y, w, h, paged)
|
|||||||
elseif erroring then
|
elseif erroring then
|
||||||
romState = "Import failed"
|
romState = "Import failed"
|
||||||
romDetail = self.detail or Strings("That ROM could not be imported.")
|
romDetail = self.detail or Strings("That ROM could not be imported.")
|
||||||
romBtnLabel, romBtnEnabled = "Import ROM", true
|
romBtnLabel = self.isNX and "Procurar novamente" or "Import ROM"
|
||||||
|
romBtnEnabled = true
|
||||||
elseif notice then
|
elseif notice then
|
||||||
romState = "No ROM imported"
|
romState = "No ROM imported"
|
||||||
romDetail = trim((notice.status or "") .. " " .. (notice.detail or ""))
|
romDetail = trim((notice.status or "") .. " " .. (notice.detail or ""))
|
||||||
romBtnLabel, romBtnEnabled = "Import ROM", true
|
romBtnLabel = self.isNX and "Procurar novamente" or "Import ROM"
|
||||||
|
romBtnEnabled = true
|
||||||
elseif self.returning[version] then
|
elseif self.returning[version] then
|
||||||
romState = "Update required"
|
romState = "Update required"
|
||||||
romDetail = "This build needs a few more things from your "
|
romDetail = "This build needs a few more things from your "
|
||||||
@@ -3240,7 +3367,8 @@ function RomImporter:_drawGamePanel(version, x, y, w, h, paged)
|
|||||||
else
|
else
|
||||||
romState = "No ROM imported"
|
romState = "No ROM imported"
|
||||||
romDetail = "The ROM is verified before any files are created. " .. dropHint
|
romDetail = "The ROM is verified before any files are created. " .. dropHint
|
||||||
romBtnLabel, romBtnEnabled = "Import ROM", true
|
romBtnLabel = self.isNX and "Procurar novamente" or "Import ROM"
|
||||||
|
romBtnEnabled = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
-- NX RomImporter must not inherit Android mobile-file-bridge semantics (SWNX-03).
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
|
||||||
|
local S = require("tests.harness").suite("rom importer NX flags")
|
||||||
|
local eq = S.eq
|
||||||
|
local check = S.check
|
||||||
|
|
||||||
|
love.system = love.system or {}
|
||||||
|
love.filesystem = love.filesystem or {}
|
||||||
|
local saved = {
|
||||||
|
getOS = love.system.getOS,
|
||||||
|
getSaveDirectory = love.filesystem.getSaveDirectory,
|
||||||
|
createDirectory = love.filesystem.createDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
love.system.getOS = function() return "NX" end
|
||||||
|
love.filesystem.getSaveDirectory = function() return "sdmc:/switch/gen1recomp/pokemon-love2d" end
|
||||||
|
love.filesystem.createDirectory = function() return true end
|
||||||
|
|
||||||
|
package.loaded["src.core.Platform"] = nil
|
||||||
|
package.loaded["src.import.RomImporter"] = nil
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
|
||||||
|
local ri = RomImporter.new(function() end, { launcher = true })
|
||||||
|
eq(ri.isNX, true, "NX importer sets isNX")
|
||||||
|
eq(ri.android, false, "NX importer does not set android")
|
||||||
|
eq(ri.mobileFileBridge, false, "NX importer does not set mobileFileBridge")
|
||||||
|
eq(ri.romImportMode, "save-directory", "NX importer exposes save-directory mode")
|
||||||
|
check(ri.pickPending == nil, "NX importer does not arm mobile pick polling")
|
||||||
|
|
||||||
|
love.system.getOS = function() return "Android" end
|
||||||
|
package.loaded["src.core.Platform"] = nil
|
||||||
|
package.loaded["src.import.RomImporter"] = nil
|
||||||
|
RomImporter = require("src.import.RomImporter")
|
||||||
|
ri = RomImporter.new(function() end, { launcher = true })
|
||||||
|
eq(ri.isNX, false, "Android importer is not NX")
|
||||||
|
eq(ri.android, true, "Android importer keeps android mobile path")
|
||||||
|
eq(ri.mobileFileBridge, true, "Android importer sets mobileFileBridge")
|
||||||
|
|
||||||
|
love.system.getOS = saved.getOS
|
||||||
|
love.filesystem.getSaveDirectory = saved.getSaveDirectory
|
||||||
|
love.filesystem.createDirectory = saved.createDirectory
|
||||||
|
package.loaded["src.core.Platform"] = nil
|
||||||
|
package.loaded["src.import.RomImporter"] = nil
|
||||||
|
|
||||||
|
S.finish()
|
||||||
Reference in New Issue
Block a user