import picker for custom cart

This commit is contained in:
bryanthaboi
2026-09-25 13:18:59 -04:00
parent 584350e9de
commit 389184789f
14 changed files with 449 additions and 51 deletions
@@ -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;
+6 -3
View File
@@ -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:)
+1 -1
View File
@@ -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
View File
@@ -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
+1 -2
View File
@@ -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
View File
@@ -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()
+3 -1
View File
@@ -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
+3 -1
View File
@@ -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)
+89
View File
@@ -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")
+4 -4
View File
@@ -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")