mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-01 07:40:29 -04:00
import picker for custom cart
This commit is contained in:
@@ -192,6 +192,8 @@ bool System::pickFile(const char *kind, const char *destination) const
|
||||
dest = "picked_mod.zip";
|
||||
else if (strcmp(kind, "sav") == 0 || strcmp(kind, "save") == 0)
|
||||
dest = "picked_save.sav";
|
||||
else if (strcmp(kind, "cart") == 0)
|
||||
dest = "picked_cart.g1rcart";
|
||||
else if (strcmp(kind, "required_import") == 0)
|
||||
dest = (destination != nullptr && destination[0] != '\0')
|
||||
? destination : "picked_required_import.bin";
|
||||
@@ -212,7 +214,7 @@ bool System::pickFile(const char *kind, const char *destination) const
|
||||
const char *System::pickFileKinds() const
|
||||
{
|
||||
#ifdef LOVE_ANDROID
|
||||
return "rom,mod,sav,required_import";
|
||||
return "rom,mod,sav,cart,required_import";
|
||||
#else
|
||||
return "";
|
||||
#endif
|
||||
|
||||
@@ -113,6 +113,7 @@ public:
|
||||
*
|
||||
* @param kind Optional pick kind: nullptr/"rom" -> picked_rom.gb,
|
||||
* "mod" -> picked_mod.zip, "sav"/"save" -> picked_save.sav,
|
||||
* "cart" -> picked_cart.g1rcart,
|
||||
* "required_import" -> picked_required_import.bin.
|
||||
* @return Whether the picker was shown.
|
||||
**/
|
||||
|
||||
@@ -610,8 +610,8 @@ public class GameActivity extends SDLActivity {
|
||||
* stays picker-agnostic and unchanged.
|
||||
*
|
||||
* @param destFilename basename under the app save identity (e.g.
|
||||
* picked_rom.gb, picked_mod.zip, picked_save.sav, or
|
||||
* picked_required_import.bin)
|
||||
* picked_rom.gb, picked_mod.zip, picked_save.sav,
|
||||
* picked_cart.g1rcart, or picked_required_import.bin)
|
||||
*/
|
||||
private static boolean isTelevision(Context context) {
|
||||
if (context == null) return false;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
// without updating that patch (see mobile/ios/patch_love_src.py).
|
||||
//
|
||||
// Contract (mirrors love-android's GameActivity.showFilePicker):
|
||||
// love.system.pickFile("rom"|"mod"|"sav"|"required_import") -> copies the user's pick into
|
||||
// love.system.pickFile("rom"|"mod"|"sav"|"cart"|"required_import") -> copies the user's pick into
|
||||
// the LÖVE save directory as picked_rom.gb / picked_mod.zip /
|
||||
// picked_save.sav; RomImporter's pending-file scan consumes it.
|
||||
// picked_save.sav / picked_cart.g1rcart; RomImporter's pending-file scan consumes it.
|
||||
// love.system.createFile(name) -> exports save dir's pending_export.sav
|
||||
// through the system picker, then writes export_done.flag.
|
||||
|
||||
@@ -330,6 +330,9 @@ public final class GRPickerBridge: NSObject {
|
||||
types = [.zip]
|
||||
case "sav":
|
||||
destName = "picked_save.sav"
|
||||
case "cart":
|
||||
destName = "picked_cart.g1rcart"
|
||||
if let t = UTType(filenameExtension: "g1rcart") { types.append(t) }
|
||||
case "required_import":
|
||||
if let requestedDestination,
|
||||
isDirectRequiredDestination(requestedDestination),
|
||||
@@ -396,7 +399,7 @@ public final class GRPickerBridge: NSObject {
|
||||
// Kept beside the switch it describes, because the two drifting apart is
|
||||
// the only way this can lie.
|
||||
@objc public static func supportedPickerKinds() -> NSString {
|
||||
return "rom,mod,sav,stadium,required_import" as NSString
|
||||
return "rom,mod,sav,cart,stadium,required_import" as NSString
|
||||
}
|
||||
|
||||
@objc(presentExportWithName:saveDir:)
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@ The build uses LÖVE 11.5, LuaJIT, SDL2, and ANGLE. Keep the DLLs and import lib
|
||||
|
||||
The normal package build consumes these files directly. Rebuilding the backend is a separate dependency maintenance task.
|
||||
|
||||
The ROM picker in commit `3f51bf0` offers `.gb` and `.gbc` only. `love.dll` in this directory has a small in-place patch that adds a third filter for `.gba` (the staged copy is still `picked_rom.gb`). `scripts/xbox-uwp/rebuild_dependencies.ps1` applies `patches/gba-file-picker.patch` on a from-source rebuild so the same filter is compiled in instead of patched afterward.
|
||||
The ROM picker in commit `3f51bf0` offers `.gb` and `.gbc` only. `love.dll` in this directory has a small in-place patch that adds a third filter for `.gba` (the staged copy is still `picked_rom.gb`). `scripts/xbox-uwp/rebuild_dependencies.ps1` applies `patches/gba-file-picker.patch` on a from-source rebuild so the same filter is compiled in instead of patched afterward. Apply `patches/uwp-cart-file-picker.patch` after that one so `pickFile("cart")` offers `.g1rcart` and stages `picked_cart.g1rcart`. A DLL built before that patch refuses the cart kind; the launcher then falls back to the save-folder hint.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
diff --git a/src/modules/system/uwp/FilePicker.cpp b/src/modules/system/uwp/FilePicker.cpp
|
||||
--- a/src/modules/system/uwp/FilePicker.cpp
|
||||
+++ b/src/modules/system/uwp/FilePicker.cpp
|
||||
@@ -56,6 +56,11 @@
|
||||
filters.Append(L".sav");
|
||||
destination = "picked_save.sav";
|
||||
}
|
||||
+ else if (kind == "cart")
|
||||
+ {
|
||||
+ filters.Append(L".g1rcart");
|
||||
+ destination = "picked_cart.g1rcart";
|
||||
+ }
|
||||
else
|
||||
{
|
||||
filters.Append(L".gb");
|
||||
@@ -100,7 +105,8 @@
|
||||
bool pickFile(const char *kind)
|
||||
{
|
||||
std::string requestedKind = kind ? kind : "rom";
|
||||
- if (requestedKind != "rom" && requestedKind != "mod" && requestedKind != "sav")
|
||||
+ if (requestedKind != "rom" && requestedKind != "mod" && requestedKind != "sav"
|
||||
+ && requestedKind != "cart")
|
||||
return false;
|
||||
|
||||
{
|
||||
+20
-9
@@ -60,14 +60,18 @@ function FilePicker.matches(name, kind)
|
||||
return false
|
||||
end
|
||||
|
||||
function FilePicker.open(prompt, kind)
|
||||
-- Shell commands for one pick, in the order this OS should try them. Windows
|
||||
-- copies the pick to a plain-ASCII temp name: the console codepage would
|
||||
-- mangle a non-ASCII path, and io.open there needs ANSI bytes.
|
||||
function FilePicker.commands(prompt, kind)
|
||||
if not Platform.canSpawnProcess() then return nil end
|
||||
local title = shellSafe(prompt)
|
||||
local platform = love.system.getOS()
|
||||
if platform == "OS X" then
|
||||
return commandOutput(
|
||||
return {
|
||||
([[osascript -e 'POSIX path of (choose file with prompt "%s" of type {%s})' 2>/dev/null]])
|
||||
:format(title, appleTypes(kind.exts)))
|
||||
:format(title, appleTypes(kind.exts)),
|
||||
}
|
||||
elseif platform == "Windows" then
|
||||
local script = table.concat({
|
||||
HostPicker.WIN_OPEN_DIALOG,
|
||||
@@ -81,15 +85,22 @@ function FilePicker.open(prompt, kind)
|
||||
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
||||
"[Console]::Write($t)}",
|
||||
})
|
||||
return commandOutput(
|
||||
'powershell -NoProfile -STA -Command "' .. script .. '"')
|
||||
return { 'powershell -NoProfile -STA -Command "' .. script .. '"' }
|
||||
elseif platform == "Linux" then
|
||||
local path = commandOutput(
|
||||
return {
|
||||
([[zenity --file-selection --title="%s" --file-filter="%s | %s" 2>/dev/null]])
|
||||
:format(title, kind.label, globPatterns(kind.exts)))
|
||||
:format(title, kind.label, globPatterns(kind.exts)),
|
||||
([[kdialog --getopenfilename "$HOME" "%s|%s" 2>/dev/null]])
|
||||
:format(globPatterns(kind.exts), kind.label),
|
||||
}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function FilePicker.open(prompt, kind)
|
||||
for _, command in ipairs(FilePicker.commands(prompt, kind) or {}) do
|
||||
local path = commandOutput(command)
|
||||
if path then return path end
|
||||
return commandOutput(([[kdialog --getopenfilename "$HOME" "%s|%s" 2>/dev/null]])
|
||||
:format(globPatterns(kind.exts), kind.label))
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -4642,8 +4642,7 @@ local function buildCartModal(imp, m)
|
||||
end
|
||||
local available = inner - Kit.scrollGutter(m.s) - 2 * gap
|
||||
local cols = math.max(1, math.min(3, math.floor((available + gap) / (actionMin + gap))))
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
local importLabel = FilePicker.available() and Strings("Import a cart") or Strings("Get more carts")
|
||||
local importLabel = imp:_cartImportButtonLabel()
|
||||
local importW = chipWidth(importLabel, m) + iconExtra
|
||||
local closeW = chipWidth(Strings("Close"), m)
|
||||
local footerStacked = importW + gap + closeW > inner
|
||||
|
||||
+287
-26
@@ -450,6 +450,8 @@ local IMPORTS_DIR = "imports"
|
||||
local BASE_ROMS_DIR = "baseroms"
|
||||
local MODS_INBOX_DIR = "imports/mods"
|
||||
local SAVES_INBOX_DIR = "imports/saves"
|
||||
local CARTS_INBOX_DIR = "imports/carts"
|
||||
local PICKED_CART = "picked_cart.g1rcart"
|
||||
local ROM_BYTES_GEN1 = 1024 * 1024
|
||||
local ROM_BYTES_GEN2 = 2 * 1024 * 1024
|
||||
local ROM_BYTES_GEN3 = 16 * 1024 * 1024
|
||||
@@ -517,6 +519,17 @@ end
|
||||
|
||||
-- NX mod zip inbox (separate from ROM imports/). Parent imports/ first --
|
||||
-- love.filesystem.createDirectory does not create nested parents.
|
||||
function RomImporter:ensureCartsInboxDir()
|
||||
self:ensureImportsDir()
|
||||
local info = love.filesystem.getInfo(CARTS_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(CARTS_INBOX_DIR)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function RomImporter:ensureModsInboxDir()
|
||||
self:ensureImportsDir()
|
||||
local info = love.filesystem.getInfo(MODS_INBOX_DIR)
|
||||
@@ -567,6 +580,15 @@ function RomImporter:_setNxInboxNotice(version)
|
||||
}
|
||||
end
|
||||
|
||||
function RomImporter:_setNxCartsInboxNotice()
|
||||
local saveDir = love.filesystem.getSaveDirectory()
|
||||
local rel = RomImporter.mtpHintPath(saveDir)
|
||||
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
|
||||
self._cartNotice = Strings(
|
||||
"Copy your .g1rcart into:\n%s/imports/carts/\nDBI MTP → 1: SD Card/%simports/carts/",
|
||||
saveDir, rel)
|
||||
end
|
||||
|
||||
function RomImporter:_setNxModsInboxNotice()
|
||||
local saveDir = love.filesystem.getSaveDirectory()
|
||||
local rel = RomImporter.mtpHintPath(saveDir)
|
||||
@@ -724,6 +746,26 @@ function RomImporter:scanModsInbox()
|
||||
return listZipPaths(MODS_INBOX_DIR)
|
||||
end
|
||||
|
||||
local function listCartPaths(dir)
|
||||
local paths = {}
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
|
||||
if name:sub(1, 1) ~= "." then
|
||||
local path = (dir == "" or dir == "/") and name or (dir .. "/" .. name)
|
||||
if name:lower():match("%.g1rcart$")
|
||||
and love.filesystem.getInfo(path, "file") then
|
||||
paths[#paths + 1] = path
|
||||
end
|
||||
end
|
||||
end
|
||||
return paths
|
||||
end
|
||||
|
||||
-- NX cart inbox: only *.g1rcart under imports/carts/.
|
||||
function RomImporter:scanCartsInbox()
|
||||
self:ensureCartsInboxDir()
|
||||
return listCartPaths(CARTS_INBOX_DIR)
|
||||
end
|
||||
|
||||
-- NX saves inbox: only non-hidden *.sav under imports/saves/<version>/.
|
||||
function RomImporter:scanSavesInbox(version)
|
||||
version = self:_resolveSaveVersion(version)
|
||||
@@ -1048,6 +1090,24 @@ local function findPendingMod(preferAny, skip)
|
||||
end
|
||||
|
||||
-- Same pattern as findPendingMod for battery saves (picked_save.sav / *.sav).
|
||||
-- Android/iOS SAF writes a cart pick to picked_cart.g1rcart. USB copies may
|
||||
-- use any .g1rcart basename at the save-dir root. preferAny=true also accepts
|
||||
-- those copies (Import). Focus only consumes the SAF basename so a leftover
|
||||
-- cart file is never auto-installed on every refocus.
|
||||
local function findPendingCart(preferAny, skip)
|
||||
if love.filesystem.getInfo(PICKED_CART, "file") then
|
||||
return PICKED_CART
|
||||
end
|
||||
if not preferAny then return nil end
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems("")) do
|
||||
if name:lower():match("%.g1rcart$") and not (skip and skip[name])
|
||||
and love.filesystem.getInfo(name, "file") then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function findPendingSav(preferAny, skip)
|
||||
local preferred = "picked_save.sav"
|
||||
if love.filesystem.getInfo(preferred, "file") then
|
||||
@@ -1751,6 +1811,11 @@ function RomImporter:focus(f)
|
||||
local version = self.androidPendingVersion or self:_savedropTarget()
|
||||
self.androidPendingVersion = nil
|
||||
self.saveNotice[version] = { ok = false, text = text }
|
||||
elseif pickError:find("picked_cart", 1, true)
|
||||
or self.pickerPendingKind == "cart" then
|
||||
self.pickerPendingKind = nil
|
||||
self.pickerPendingVersion = nil
|
||||
self._cartNotice = text
|
||||
else
|
||||
self:setError(text)
|
||||
end
|
||||
@@ -1819,6 +1884,15 @@ function RomImporter:focus(f)
|
||||
self.modNotice and self.modNotice.ok)
|
||||
return
|
||||
end
|
||||
local cartName = findPendingCart(false, self.pickSkip)
|
||||
if cartName then
|
||||
local version = self.pickerPendingVersion or self._cartPopup or self.tab
|
||||
self.pickerPendingKind = nil
|
||||
self.pickerPendingVersion = nil
|
||||
local installed = self:_installCartFile(cartName, version)
|
||||
consumePick(self, cartName, PICKED_CART, installed)
|
||||
return
|
||||
end
|
||||
local savName = findPendingSav(false, self.pickSkip)
|
||||
if savName then
|
||||
local version = self.androidPendingVersion or self:_savedropTarget()
|
||||
@@ -2354,6 +2428,10 @@ function RomImporter:filedropped(file)
|
||||
self:_importSave(self:_savedropTarget(), file)
|
||||
return
|
||||
end
|
||||
if name:lower():match("%.g1rcart$") then
|
||||
self:_installCartFile(file, self._cartPopup or self.tab)
|
||||
return
|
||||
end
|
||||
local data, readError = readDroppedFile(file)
|
||||
if not data then
|
||||
self:setError("Could not read the dropped file: " .. tostring(readError))
|
||||
@@ -2538,12 +2616,18 @@ function RomImporter:_pumpHostPick()
|
||||
local state, output, err = HostPicker.poll(job.id)
|
||||
if state == "pending" then return end
|
||||
self._hostPick = nil
|
||||
if self.modNotice == job.notice then self.modNotice = nil end
|
||||
if job.kind == "cart" then
|
||||
if self._cartNotice == job.notice then self._cartNotice = nil end
|
||||
elseif self.modNotice == job.notice then
|
||||
self.modNotice = nil
|
||||
end
|
||||
output = trim(output)
|
||||
if output ~= "" then
|
||||
self:_installMod(output)
|
||||
if job.kind == "cart" then self:_installCartFile(output, job.version)
|
||||
else self:_installMod(output) end
|
||||
elseif err then
|
||||
self:_openModBrowser()
|
||||
if job.kind == "cart" then self:_openCartBrowser(job.version)
|
||||
else self:_openModBrowser() end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3269,9 +3353,15 @@ function RomImporter:_pollPickedFiles(dt)
|
||||
if pickError then
|
||||
love.filesystem.remove("pick_error.txt")
|
||||
self.pickPending = nil
|
||||
self.modNotice = { ok = false, text = pickError }
|
||||
self.notice = { version = self.chooseVersion or "red",
|
||||
status = "File import failed:", detail = pickError }
|
||||
if self.pickerPendingKind == "cart" then
|
||||
self.pickerPendingKind = nil
|
||||
self.pickerPendingVersion = nil
|
||||
self._cartNotice = pickError
|
||||
else
|
||||
self.modNotice = { ok = false, text = pickError }
|
||||
self.notice = { version = self.chooseVersion or "red",
|
||||
status = "File import failed:", detail = pickError }
|
||||
end
|
||||
return
|
||||
end
|
||||
local found = love.filesystem.getInfo("export_done.flag", "file") ~= nil
|
||||
@@ -3281,6 +3371,7 @@ function RomImporter:_pollPickedFiles(dt)
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems("")) do
|
||||
local n = name:lower()
|
||||
if isRomFilename(n) or n == "picked_mod.zip" or n == "picked_save.sav"
|
||||
or n == PICKED_CART
|
||||
or n == "picked_required_import.bin" or n == "picked_stadium.z64"
|
||||
or n:match("^picked_importer_[%l%d_%-]+%.bin$") then
|
||||
found = true
|
||||
@@ -3507,6 +3598,13 @@ function RomImporter:update(dt)
|
||||
if Platform.isUWP() and self.saveNotice[target] and self.saveNotice[target].ok then
|
||||
os.remove(path)
|
||||
end
|
||||
elseif kind == "cart" then
|
||||
if Platform.isUWP() then
|
||||
local installed = self:_installCartFile(path, version)
|
||||
if installed then os.remove(path) end
|
||||
else
|
||||
self:_installCartFile(path, version)
|
||||
end
|
||||
else
|
||||
self:startPath(path)
|
||||
if Platform.isUWP() then os.remove(path) end
|
||||
@@ -3527,6 +3625,8 @@ function RomImporter:update(dt)
|
||||
self._skinNotice = { ok = false, text = errorText }
|
||||
elseif kind == "sav" then
|
||||
self.saveNotice[version] = { ok = false, text = errorText }
|
||||
elseif kind == "cart" then
|
||||
self._cartNotice = errorText
|
||||
else
|
||||
self:setError(errorText)
|
||||
end
|
||||
@@ -5157,8 +5257,6 @@ end
|
||||
|
||||
-- The view's open-folder affordance needs the same file:// encoding the old
|
||||
-- notice line used.
|
||||
-- Desktop picks a .cart file; everywhere else CartStore's stray scan already
|
||||
-- adopts anything dropped in the folder, so we just point at it.
|
||||
function RomImporter:_resyncPointerAfterDialog()
|
||||
self._mouseAt = nil
|
||||
self._clickPt = nil
|
||||
@@ -5166,23 +5264,60 @@ function RomImporter:_resyncPointerAfterDialog()
|
||||
and love.mouse.isDown(1)) and true or false
|
||||
end
|
||||
|
||||
function RomImporter:importCartFile(version)
|
||||
local CartStore = require("src.carts.CartStore")
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
if not FilePicker.available() then
|
||||
local dir = self:cartsDir()
|
||||
self._cartNotice = dir
|
||||
and Strings("Drop .cart files in %s, then reopen this list.", dir)
|
||||
or Strings("No filesystem available to import from.")
|
||||
return false
|
||||
local CART_PICK = { label = "Cart", exts = { "g1rcart" } }
|
||||
|
||||
local function hostAbsolute(path)
|
||||
return type(path) == "string" and (
|
||||
path:match("^/")
|
||||
or path:match("^%a:[/\\]")
|
||||
or path:match("^[Ss][Dd][Mm][Cc]:"))
|
||||
end
|
||||
|
||||
local function readCartSource(source)
|
||||
local t = type(source)
|
||||
if (t == "userdata" or t == "table") and type(source.open) == "function" then
|
||||
return readDroppedFile(source)
|
||||
end
|
||||
local path = FilePicker.open("Choose a cart",
|
||||
{ label = "Cart", exts = { CartStore.EXT:gsub("^%.", "") } })
|
||||
self:_resyncPointerAfterDialog()
|
||||
if not path then return false end
|
||||
local bytes = FilePicker.read(path)
|
||||
if t ~= "string" then return nil end
|
||||
if not hostAbsolute(source) and love and love.filesystem then
|
||||
local data = love.filesystem.read(source)
|
||||
if type(data) == "string" and data ~= "" then return data end
|
||||
end
|
||||
local file = io.open(source, "rb")
|
||||
if file then
|
||||
local data = file:read("*a")
|
||||
file:close()
|
||||
if type(data) == "string" and data ~= "" then return data end
|
||||
end
|
||||
if love and love.filesystem then
|
||||
local data = love.filesystem.read(source)
|
||||
if type(data) == "string" and data ~= "" then return data end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function cartSourceName(source)
|
||||
if type(source) == "string" then
|
||||
return source:match("([^/\\]+)$") or source
|
||||
end
|
||||
if source and source.getFilename then
|
||||
return source:getFilename() or "cart"
|
||||
end
|
||||
return "cart"
|
||||
end
|
||||
|
||||
local function handheldHost()
|
||||
return os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||
end
|
||||
|
||||
function RomImporter:_installCartFile(source, version)
|
||||
local CartStore = require("src.carts.CartStore")
|
||||
local name = cartSourceName(source)
|
||||
local bytes = readCartSource(source)
|
||||
if not bytes then
|
||||
self._cartNotice = Strings("Could not read %s", FilePicker.basename(path))
|
||||
self._cartNotice = Strings("Could not read %s", name)
|
||||
return false
|
||||
end
|
||||
local cart, err = CartStore.install(bytes)
|
||||
@@ -5191,12 +5326,138 @@ function RomImporter:importCartFile(version)
|
||||
tostring(err))
|
||||
return false
|
||||
end
|
||||
self:_refreshCarts(version)
|
||||
self._cartNotice = Strings("Imported %s. It is in this list now.",
|
||||
tostring(cart.title or cart.id))
|
||||
if cart.base then self:_refreshCarts(cart.base) end
|
||||
if version and version ~= cart.base then self:_refreshCarts(version) end
|
||||
local title = tostring(cart.title or cart.id)
|
||||
if version and cart.base and cart.base ~= version then
|
||||
local info = GameVersion.info(cart.base)
|
||||
self._cartNotice = Strings("Imported %s for %s.", title,
|
||||
(info and info.displayName) or cart.base)
|
||||
else
|
||||
self._cartNotice = Strings("Imported %s. It is in this list now.", title)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function RomImporter:_openCartBrowser(version)
|
||||
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||
if okKit and Kit.FileBrowser then
|
||||
self._padCursorActive = false
|
||||
Kit.FileBrowser.open({
|
||||
title = "Select Cart (.g1rcart)",
|
||||
mode = "cart",
|
||||
onSelect = function(pickedPath)
|
||||
self:_installCartFile(pickedPath, version)
|
||||
end,
|
||||
})
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function RomImporter:rescanCartsAction(version)
|
||||
if self.workState == "working" then return false end
|
||||
self:ensureCartsInboxDir()
|
||||
local candidates = self:scanCartsInbox()
|
||||
if #candidates == 0 then
|
||||
self:_setNxCartsInboxNotice()
|
||||
return false
|
||||
end
|
||||
local anyOk, fails = false, 0
|
||||
local lastOk, lastFail
|
||||
for _, path in ipairs(candidates) do
|
||||
if self:_installCartFile(path, version) then
|
||||
anyOk = true
|
||||
lastOk = self._cartNotice
|
||||
else
|
||||
fails = fails + 1
|
||||
lastFail = self._cartNotice
|
||||
end
|
||||
end
|
||||
if anyOk and lastFail then
|
||||
self._cartNotice = Strings("%s (%d could not be imported.)", lastOk, fails)
|
||||
elseif not anyOk then
|
||||
self._cartNotice = lastFail
|
||||
end
|
||||
return anyOk
|
||||
end
|
||||
|
||||
function RomImporter:_cartImportButtonLabel()
|
||||
if self.isNX then return Strings("Scan again") end
|
||||
return Strings("Import .g1rcart")
|
||||
end
|
||||
|
||||
-- "Import .g1rcart" on the Custom Carts modal. Same platform split as ROM,
|
||||
-- mod, and save import: NX rescans imports/carts/, Android and iOS stage
|
||||
-- picked_cart.g1rcart through love.system.pickFile("cart"), UWP returns the
|
||||
-- pick from getPickedFile, handhelds use the in-launcher browser, and desktop
|
||||
-- opens the OS dialog (async on Windows so the window keeps drawing).
|
||||
function RomImporter:importCartFile(version)
|
||||
if self.workState == "working" then return false end
|
||||
if self._hostPick then return false end
|
||||
if self.isNX then
|
||||
self:ensureCartsInboxDir()
|
||||
return self:rescanCartsAction(version)
|
||||
end
|
||||
if self.nativePicker and love.system.getPickedFile then
|
||||
if pickerHasKind("cart") and pickFile("cart") then
|
||||
self.pickerPendingKind = "cart"
|
||||
self.pickerPendingVersion = version
|
||||
return true
|
||||
end
|
||||
self.pickerPendingKind = nil
|
||||
local dir = self:cartsDir()
|
||||
self._cartNotice = dir
|
||||
and Strings("Drop .g1rcart files in %s, then reopen this list.", dir)
|
||||
or Strings("Could not open the file picker.")
|
||||
return false
|
||||
end
|
||||
if self.android then
|
||||
local name = findPendingCart(true, self.pickSkip)
|
||||
if name then
|
||||
local installed = self:_installCartFile(name, version)
|
||||
consumePick(self, name, PICKED_CART, installed)
|
||||
return installed
|
||||
end
|
||||
if pickerHasKind("cart") and pickFile("cart") then
|
||||
self.pickerPendingKind = "cart"
|
||||
self.pickerPendingVersion = version
|
||||
self.pickPending = true
|
||||
self.pickTimer = 0
|
||||
return true
|
||||
end
|
||||
local dir = love.filesystem.getSaveDirectory()
|
||||
self._cartNotice = Strings(
|
||||
"Could not open the file picker. Copy a .g1rcart into:\n%s", dir)
|
||||
return false
|
||||
end
|
||||
if handheldHost() and self:_openCartBrowser(version) then return true end
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
if love.system.getOS() == "Windows" and Platform.canSpawnProcess() then
|
||||
local id = HostPicker.start(FilePicker.commands(
|
||||
Strings("Choose a .g1rcart cart"), CART_PICK))
|
||||
if id then
|
||||
local notice = Strings(
|
||||
"Choose a .g1rcart in the file dialog. The launcher waits until it closes.")
|
||||
self._hostPick = { id = id, kind = "cart", version = version, notice = notice }
|
||||
self._cartNotice = notice
|
||||
return true
|
||||
end
|
||||
end
|
||||
if FilePicker.available() then
|
||||
local path = FilePicker.open(Strings("Choose a .g1rcart cart"), CART_PICK)
|
||||
self:_resyncPointerAfterDialog()
|
||||
if not path then return false end
|
||||
return self:_installCartFile(path, version)
|
||||
end
|
||||
if self:_openCartBrowser(version) then return true end
|
||||
local dir = self:cartsDir()
|
||||
self._cartNotice = dir
|
||||
and Strings("Drop .g1rcart files in %s, then reopen this list.", dir)
|
||||
or Strings("No filesystem available to import from.")
|
||||
return false
|
||||
end
|
||||
|
||||
-- The real OS path of the cart folder, so the launcher can open it and so a
|
||||
-- player can drop a .cart in by hand.
|
||||
function RomImporter:cartsDir()
|
||||
|
||||
@@ -24,7 +24,7 @@ end
|
||||
local FileBrowser = {
|
||||
active = false,
|
||||
title = "Select File",
|
||||
mode = "rom", -- "rom", "save", "mod", "all"
|
||||
mode = "rom", -- "rom", "save", "mod", "cart", "all"
|
||||
currentDir = "/",
|
||||
entries = {},
|
||||
selectedIdx = 1,
|
||||
@@ -100,6 +100,8 @@ local function isMatchingFilter(name, isDir, mode)
|
||||
return (ext == "sav" or ext == "lua")
|
||||
elseif mode == "mod" then
|
||||
return (ext == "zip")
|
||||
elseif mode == "cart" then
|
||||
return (ext == "g1rcart")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -120,7 +120,9 @@ systemFile:close()
|
||||
check(system:find('strcmp(kind, "required_import")', 1, true)
|
||||
and system:find('destination != nullptr', 1, true)
|
||||
and system:find('"picked_required_import.bin"', 1, true)
|
||||
and system:find('return "rom,mod,sav,required_import"', 1, true),
|
||||
and system:find('strcmp(kind, "cart")', 1, true)
|
||||
and system:find('"picked_cart.g1rcart"', 1, true)
|
||||
and system:find('return "rom,mod,sav,cart,required_import"', 1, true),
|
||||
"native Android bridge advertises and routes required imports")
|
||||
check(source:find('normalized.startsWith("mods/")', 1, true)
|
||||
and source:find('/baseroms/', 1, true)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
-- Custom Carts "Import .g1rcart" follows the same platform split as ROM, mod,
|
||||
-- and save import. Self-contained: luajit tests/engine/cart_import_picker.lua
|
||||
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("cart import picker")
|
||||
local eq, check = S.eq, S.check
|
||||
|
||||
local Platform = require("src.core.Platform")
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
|
||||
love.system = love.system or {}
|
||||
local saved = {
|
||||
getOS = love.system.getOS,
|
||||
pickFile = love.system.pickFile,
|
||||
pickFileKinds = love.system.pickFileKinds,
|
||||
getPickedFile = love.system.getPickedFile,
|
||||
}
|
||||
|
||||
local picks = {}
|
||||
local function androidImporter()
|
||||
picks = {}
|
||||
love.system.getOS = function() return "Android" end
|
||||
love.system.pickFile = function(kind)
|
||||
picks[#picks + 1] = kind
|
||||
return true
|
||||
end
|
||||
love.system.pickFileKinds = function()
|
||||
return "rom,mod,sav,cart,required_import"
|
||||
end
|
||||
Platform._resetForTests()
|
||||
local imp = RomImporter.new(function() end, { launcher = true })
|
||||
imp._installCartFile = function(self, source)
|
||||
self.installed = source
|
||||
self._cartNotice = "Imported Picked One. It is in this list now."
|
||||
return true
|
||||
end
|
||||
return imp
|
||||
end
|
||||
|
||||
local imp = androidImporter()
|
||||
imp._cartPopup = "red"
|
||||
eq(imp:importCartFile("red"), true, "Android Import .g1rcart opens the picker")
|
||||
eq(picks[1], "cart", "and asks for the cart kind, not a ROM")
|
||||
eq(imp.pickerPendingKind, "cart", "focus knows the staged file is a cart")
|
||||
|
||||
love.filesystem.write("picked_cart.g1rcart", "cart-bytes")
|
||||
imp:focus(true)
|
||||
eq(imp.installed, "picked_cart.g1rcart", "focus installs the staged cart")
|
||||
check(love.filesystem.getInfo("picked_cart.g1rcart") == nil,
|
||||
"and retires the staged file")
|
||||
|
||||
love.system.pickFileKinds = function() return "rom,mod,sav" end
|
||||
Platform._resetForTests()
|
||||
local legacy = RomImporter.new(function() end, { launcher = true })
|
||||
legacy._installCartFile = imp._installCartFile
|
||||
picks = {}
|
||||
eq(legacy:importCartFile("red"), false,
|
||||
"an Android build that predates the cart kind does not open a ROM picker")
|
||||
eq(#picks, 0, "and never calls pickFile")
|
||||
check(type(legacy._cartNotice) == "string" and legacy._cartNotice:find(".g1rcart", 1, true),
|
||||
"it tells the player to copy a .g1rcart instead")
|
||||
|
||||
love.system.getOS = function() return "NX" end
|
||||
love.system.pickFile = nil
|
||||
Platform._resetForTests()
|
||||
local nx = RomImporter.new(function() end, { launcher = true })
|
||||
nx._installCartFile = function(self, source)
|
||||
self.installed = source
|
||||
self._cartNotice = "Imported inbox."
|
||||
return true
|
||||
end
|
||||
love.filesystem.write("imports/carts/inbox.g1rcart", "cart-bytes")
|
||||
eq(nx:importCartFile("red"), true, "NX Import scans imports/carts/")
|
||||
eq(nx.installed, "imports/carts/inbox.g1rcart", "and installs the inbox file")
|
||||
love.filesystem.remove("imports/carts/inbox.g1rcart")
|
||||
|
||||
nx.installed = nil
|
||||
eq(nx:importCartFile("red"), false, "an empty NX inbox does not invent a cart")
|
||||
check(nx._cartNotice:find("imports/carts/", 1, true) ~= nil,
|
||||
"and the notice names the MTP folder")
|
||||
|
||||
love.system.getOS = saved.getOS
|
||||
love.system.pickFile = saved.pickFile
|
||||
love.system.pickFileKinds = saved.pickFileKinds
|
||||
love.system.getPickedFile = saved.getPickedFile
|
||||
Platform._resetForTests()
|
||||
|
||||
print("ok cart import picker")
|
||||
@@ -141,9 +141,9 @@ check(picker:find("Johto Lite", 1, true) == nil,
|
||||
check(picker:find("v1.2.0", 1, true) ~= nil, "a cart row carries its version")
|
||||
check(picker:find("sealed", 1, true) ~= nil, "a cart row carries its seal state")
|
||||
check(picker:find("open", 1, true) ~= nil, "including an open one")
|
||||
check(picker:find("Import a cart", 1, true) ~= nil
|
||||
or picker:find("Get more carts", 1, true) ~= nil,
|
||||
"the last row imports a cart, by picker or by folder")
|
||||
check(picker:find("Import .g1rcart", 1, true) ~= nil
|
||||
or picker:find("Scan again", 1, true) ~= nil,
|
||||
"the last row imports a .g1rcart, by picker or by inbox scan")
|
||||
|
||||
imp._cartPopup = nil
|
||||
local vanillaColors = drawColors(imp)
|
||||
@@ -1115,7 +1115,7 @@ for _, size in ipairs(SIZES) do
|
||||
auditFrame(("%dx%d picker"):format(W, H), "Export")
|
||||
for _, r in ipairs(Kit.audit or {}) do
|
||||
local label = tostring(r.label)
|
||||
if label == "Get more carts" or label == "Close" then
|
||||
if label == "Import .g1rcart" or label == "Scan again" or label == "Close" then
|
||||
check(r.y >= -0.5 and r.y + r.h <= H + 0.5,
|
||||
("%dx%d picker: %q stays inside the window"):format(W, H, label))
|
||||
end
|
||||
|
||||
@@ -19,7 +19,10 @@ check(bridge:find('case "required_import":', 1, true)
|
||||
check(bridge:find("types.append(.data)", 1, true)
|
||||
and bridge:find("types.append(.item)", 1, true),
|
||||
"iOS required imports accept user-owned binary ROM files")
|
||||
check(bridge:find('"rom,mod,sav,stadium,required_import"', 1, true),
|
||||
check(bridge:find('case "cart":', 1, true)
|
||||
and bridge:find('destName = "picked_cart.g1rcart"', 1, true),
|
||||
"iOS routes cart files to their own staged filename")
|
||||
check(bridge:find('"rom,mod,sav,cart,stadium,required_import"', 1, true),
|
||||
"iOS advertises required_import to Lua before opening the picker")
|
||||
|
||||
local patch = read("mobile/ios/patch_love_src.py")
|
||||
|
||||
Reference in New Issue
Block a user