mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
Merge pull request #1353 from anxiousintrovert/fix/required-import-platform-pickers
This commit is contained in:
@@ -16,6 +16,7 @@ save directory as:
|
|||||||
| nil / `"rom"` | `picked_rom.gb` (open) |
|
| nil / `"rom"` | `picked_rom.gb` (open) |
|
||||||
| `"mod"` | `picked_mod.zip` (open) |
|
| `"mod"` | `picked_mod.zip` (open) |
|
||||||
| `"sav"` / `"save"` | `picked_save.sav` (open) |
|
| `"sav"` / `"save"` | `picked_save.sav` (open) |
|
||||||
|
| `"required_import"` | `picked_required_import.bin` (open) |
|
||||||
|
|
||||||
Export uses a separate API: `love.system.createFile(suggestedName)` →
|
Export uses a separate API: `love.system.createFile(suggestedName)` →
|
||||||
`GameActivity.showCreateDocument` (`ACTION_CREATE_DOCUMENT`), which copies
|
`GameActivity.showCreateDocument` (`ACTION_CREATE_DOCUMENT`), which copies
|
||||||
|
|||||||
@@ -148,6 +148,18 @@ older iOS-only `"stadium"` picker kind remains temporarily for compatibility.
|
|||||||
Android now returns `false` for unknown picker kinds instead of treating them
|
Android now returns `false` for unknown picker kinds instead of treating them
|
||||||
as game-ROM picks.
|
as game-ROM picks.
|
||||||
|
|
||||||
|
### Platform import flow
|
||||||
|
|
||||||
|
The same per-mod validation and private `mods/<mod-id>/baseroms/` destination
|
||||||
|
applies on every supported platform. Windows, macOS, and Linux use the
|
||||||
|
launcher file chooser. Android uses the Storage Access Framework, and iOS uses
|
||||||
|
the Files document picker; both stage the choice as `picked_required_import.bin`
|
||||||
|
before validation. Xbox/UWP uses its native picker and hands the launcher a
|
||||||
|
temporary path. Switch/NX has no host picker, so the player copies a file to
|
||||||
|
`imports/baseroms/` over MTP and chooses the import again. No platform grants
|
||||||
|
the mod a host filesystem path or bypasses the manifest's size, format, and MD5
|
||||||
|
checks.
|
||||||
|
|
||||||
## Mods and Gold (Gen 2)
|
## Mods and Gold (Gen 2)
|
||||||
|
|
||||||
The mod API is one API across both generations, but Gold runs its own battle
|
The mod API is one API across both generations, but Gold runs its own battle
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ public class GameActivity extends SDLActivity {
|
|||||||
private static final String PICKED_ROM_FILENAME = "picked_rom.gb";
|
private static final String PICKED_ROM_FILENAME = "picked_rom.gb";
|
||||||
private static final String PICKED_MOD_FILENAME = "picked_mod.zip";
|
private static final String PICKED_MOD_FILENAME = "picked_mod.zip";
|
||||||
private static final String PICKED_SAVE_FILENAME = "picked_save.sav";
|
private static final String PICKED_SAVE_FILENAME = "picked_save.sav";
|
||||||
|
// Kept separate from the game-ROM destination so a dependency pick can
|
||||||
|
// never be mistaken for a game import when the picker returns on Android.
|
||||||
|
private static final String PICKED_REQUIRED_IMPORT_FILENAME = "picked_required_import.bin";
|
||||||
private static final String PENDING_EXPORT_FILENAME = "pending_export.sav";
|
private static final String PENDING_EXPORT_FILENAME = "pending_export.sav";
|
||||||
private static final String EXPORT_DONE_FILENAME = "export_done.flag";
|
private static final String EXPORT_DONE_FILENAME = "export_done.flag";
|
||||||
// Written when a SAF pick cannot be read at all, with the destination
|
// Written when a SAF pick cannot be read at all, with the destination
|
||||||
@@ -504,7 +507,8 @@ public class GameActivity extends SDLActivity {
|
|||||||
* picker-agnostic and unchanged.
|
* picker-agnostic and unchanged.
|
||||||
*
|
*
|
||||||
* @param destFilename basename under the app save identity (e.g.
|
* @param destFilename basename under the app save identity (e.g.
|
||||||
* picked_rom.gb, picked_mod.zip, picked_save.sav)
|
* picked_rom.gb, picked_mod.zip, picked_save.sav, or
|
||||||
|
* picked_required_import.bin)
|
||||||
*/
|
*/
|
||||||
/** Legacy single-argument entry; resolves the save dir itself. */
|
/** Legacy single-argument entry; resolves the save dir itself. */
|
||||||
@Keep
|
@Keep
|
||||||
@@ -535,6 +539,11 @@ public class GameActivity extends SDLActivity {
|
|||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
|
// The Storage Access Framework grants the returned content URI
|
||||||
|
// directly to this activity. Request the read grant explicitly as
|
||||||
|
// well: Android 13's scoped storage deliberately does not expose
|
||||||
|
// arbitrary paths or require broad media/storage permissions.
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
try {
|
try {
|
||||||
self.startActivityForResult(intent, FILE_PICKER_REQUEST_CODE);
|
self.startActivityForResult(intent, FILE_PICKER_REQUEST_CODE);
|
||||||
return true;
|
return true;
|
||||||
@@ -547,6 +556,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
try {
|
try {
|
||||||
self.startActivityForResult(
|
self.startActivityForResult(
|
||||||
Intent.createChooser(intent, "Choose a file"),
|
Intent.createChooser(intent, "Choose a file"),
|
||||||
@@ -576,6 +586,12 @@ public class GameActivity extends SDLActivity {
|
|||||||
return showFilePicker(PICKED_SAVE_FILENAME);
|
return showFilePicker(PICKED_SAVE_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Required-mod-file wrapper used by love.system.pickFile("required_import"). */
|
||||||
|
@Keep
|
||||||
|
public static boolean showRequiredImportFilePicker() {
|
||||||
|
return showFilePicker(PICKED_REQUIRED_IMPORT_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relaunches the whole app for love.system.restartApp, used by
|
* Relaunches the whole app for love.system.restartApp, used by
|
||||||
* src/core/HostShell.lua when a mod toggle needs a cold boot (#575).
|
* src/core/HostShell.lua when a mod toggle needs a cold boot (#575).
|
||||||
|
|||||||
@@ -55,4 +55,24 @@ check(not source:find("QuestActivity", 1, true) and
|
|||||||
not source:find("QuestBridge", 1, true),
|
not source:find("QuestBridge", 1, true),
|
||||||
"generic Android activity must not require Quest classes")
|
"generic Android activity must not require Quest classes")
|
||||||
|
|
||||||
|
-- Required mod files use Android's Storage Access Framework, which works with
|
||||||
|
-- Android 13 scoped storage without broad media/storage permissions. Keep the
|
||||||
|
-- native destination distinct so it cannot be consumed as a game ROM.
|
||||||
|
check(source:find('PICKED_REQUIRED_IMPORT_FILENAME = "picked_required_import.bin"',
|
||||||
|
1, true), "required imports use their own Android picker destination")
|
||||||
|
check(source:find("showRequiredImportFilePicker", 1, true),
|
||||||
|
"Android exposes a required-import picker entry point")
|
||||||
|
check(source:find("Intent.ACTION_OPEN_DOCUMENT", 1, true)
|
||||||
|
and source:find("Intent.FLAG_GRANT_READ_URI_PERMISSION", 1, true),
|
||||||
|
"Android 13 uses SAF with an explicit read grant")
|
||||||
|
|
||||||
|
local systemPath = "mobile/android/love/src/jni/love/src/modules/system/System.cpp"
|
||||||
|
local systemFile = assert(io.open(systemPath, "rb"))
|
||||||
|
local system = systemFile:read("*a")
|
||||||
|
systemFile:close()
|
||||||
|
check(system:find('strcmp(kind, "required_import")', 1, true)
|
||||||
|
and system:find('dest = "picked_required_import.bin"', 1, true)
|
||||||
|
and system:find('return "rom,mod,sav,required_import"', 1, true),
|
||||||
|
"native Android bridge advertises and routes required imports")
|
||||||
|
|
||||||
print("android_host_extension_test: ok")
|
print("android_host_extension_test: ok")
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- iOS required imports travel through the same document-picker contract as
|
||||||
|
-- Android. Keep the Swift bridge and liblove patch aligned: a build that has
|
||||||
|
-- only one side would show the import button but fail on device.
|
||||||
|
local function read(path)
|
||||||
|
local file = assert(io.open(path, "rb"))
|
||||||
|
local data = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check(value, message)
|
||||||
|
if not value then error(message, 2) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local bridge = read("mobile/ios/native/GRPickerBridge.swift")
|
||||||
|
check(bridge:find('case "required_import":', 1, true)
|
||||||
|
and bridge:find('destName = "picked_required_import.bin"', 1, true),
|
||||||
|
"iOS routes required imports to their own staged filename")
|
||||||
|
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),
|
||||||
|
"iOS advertises required_import to Lua before opening the picker")
|
||||||
|
|
||||||
|
local patch = read("mobile/ios/patch_love_src.py")
|
||||||
|
check(patch:find("int w_pickFileKinds", 1, true)
|
||||||
|
and patch:find('{ "pickFileKinds", w_pickFileKinds }', 1, true),
|
||||||
|
"iOS liblove patch exposes the picker capability query")
|
||||||
|
check(patch:find('("GRPickerBridge.swift", ID_FILE_PICKER', 1, true),
|
||||||
|
"iOS build patch compiles the required-import picker bridge")
|
||||||
|
|
||||||
|
print("ios_required_import_picker_test: ok")
|
||||||
@@ -44,6 +44,46 @@ check(importer.installedPath == [[C:\LocalState\picked_mod.zip]],
|
|||||||
check(removedPath == [[C:\LocalState\picked_mod.zip]],
|
check(removedPath == [[C:\LocalState\picked_mod.zip]],
|
||||||
"removes the temporary copy after installation")
|
"removes the temporary copy after installation")
|
||||||
|
|
||||||
|
-- The UWP picker returns a temporary path rather than a mobile staged name.
|
||||||
|
-- Required imports must use that same picker and remain scoped to the selected
|
||||||
|
-- mod instead of relying on a desktop shell or Android/iOS inbox handling.
|
||||||
|
local pickedKind
|
||||||
|
love.system.pickFile = function(kind)
|
||||||
|
pickedKind = kind
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
love.system.getPickedFile = function()
|
||||||
|
love.system.getPickedFile = function() return nil end
|
||||||
|
return [[C:\LocalState\picked_required_import.bin]]
|
||||||
|
end
|
||||||
|
removedPath = nil
|
||||||
|
local required = RomImporter.new(function() end, { launcher = true })
|
||||||
|
required.mods = { {
|
||||||
|
id = "needs-source",
|
||||||
|
manifest = {
|
||||||
|
id = "needs-source", name = "Needs source",
|
||||||
|
required_imports = { {
|
||||||
|
id = "source", name = "Source", file = "source.bin",
|
||||||
|
md5 = { "00000000000000000000000000000000" },
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
} }
|
||||||
|
required._importRequiredSource = function(self, modId, importId, path)
|
||||||
|
self.requiredPath = { modId = modId, importId = importId, path = path }
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
required:chooseRequiredImport("needs-source", "source")
|
||||||
|
check(pickedKind == "required_import", "UWP requests the required-import picker kind")
|
||||||
|
required:update(0)
|
||||||
|
check(required.requiredPath and required.requiredPath.path
|
||||||
|
== [[C:\LocalState\picked_required_import.bin]],
|
||||||
|
"UWP routes the picked dependency to its declared import")
|
||||||
|
check(required.requiredPath.modId == "needs-source"
|
||||||
|
and required.requiredPath.importId == "source",
|
||||||
|
"UWP preserves the pending mod and import identity")
|
||||||
|
check(removedPath == [[C:\LocalState\picked_required_import.bin]],
|
||||||
|
"UWP removes its temporary required-import copy after validation")
|
||||||
|
|
||||||
love.system.getOS = saved.getOS
|
love.system.getOS = saved.getOS
|
||||||
love.system.pickFile = saved.pickFile
|
love.system.pickFile = saved.pickFile
|
||||||
love.system.getPickedFile = saved.getPickedFile
|
love.system.getPickedFile = saved.getPickedFile
|
||||||
|
|||||||
Reference in New Issue
Block a user