Merge pull request #906 from caorthann-celt/dev

Fix Xbox mod downloads and internal ROM imports
This commit is contained in:
bryanthaboi
2026-08-06 10:20:00 -04:00
committed by GitHub
14 changed files with 299 additions and 6 deletions
+1 -1
View File
@@ -199,7 +199,7 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|LauncherView|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_baseroms|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
+34
View File
@@ -255,6 +255,40 @@ build and install from source on a Mac instead, see
<a href="https://github.com/bryanthaboi/gen1recomp/releases/latest"><img src="./.github/resources/github-badge.png" alt="Download from GitHub" height="60"></a>
</div>
## Xbox Dev Mode
Every release ships `gen1recomp-*-xbox-uwp.zip` for Xbox One and Xbox Series
consoles in Developer Mode. It cannot be installed in retail mode.
Extract the archive, then use Xbox Device Portal to install the `.msix` and
the x64 package under `Dependencies`.
### External setup
1. Put your legally obtained Red, Blue, or Yellow ROMs on an external drive.
Mod ZIPs can go on the same drive.
2. Connect the drive to the Xbox and open Gen1Recomp.
3. Select **Import ROM** or **Import Mod**, then choose the file with the Xbox
file picker.
4. Repeat the ROM import for each version you want to use.
### Internal setup
1. Create a folder named `baseroms` on your PC and place your legally obtained
Red, Blue, or Yellow ROMs inside it.
2. ZIP the folder, keeping `baseroms` at the top level of the archive.
3. Launch Gen1Recomp once, then close it.
4. Open Xbox Device Portal and upload the ZIP to
`Gen1Recomp/LocalState/pokemon-love2d/`.
5. Choose **Yes** when Device Portal asks whether to extract the archive.
6. Open Gen1Recomp. The launcher checks baseroms once at startup. When it finds a compatible ROM, that games tab shows ROM FOUND and an Import detected ROM button.
ROMs, generated game data, saves, and mods remain in LocalState and are not
included in the app.
Source builds and package details are covered in
[the Xbox UWP build notes](ports/uwp/BUILD.md).
## Handhelds
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

+5
View File
@@ -113,6 +113,11 @@ picker copies user-selected files into LocalState and the launcher imports them
from there. Saves, ROM cache and installed mods remain under the LÖVE save
directory in LocalState.
The launcher also checks `LocalState\pokemon-love2d\baseroms` once at startup
for clean Red, Blue, and Yellow ROMs. Compatible files are offered on their
launcher tabs and remain in `baseroms` after import. The file picker remains
available when no compatible ROM is found.
LuaJIT requires the `codeGeneration` capability. `removableStorage` exposes
external media to the Xbox picker. The network capabilities support relay play
and direct hosting. The package does not request full trust or broad filesystem
+1 -1
View File
@@ -1,6 +1,6 @@
# LÖVE UWP binaries
These x64 UWP Release binaries were built from [`caorthann-celt/love-xbox-uwp`](https://github.com/caorthann-celt/love-xbox-uwp) at commit `62275ed10190afbec01f65754b3bcc04f244dd35`.
These x64 UWP Release binaries were built from [`caorthann-celt/love-xbox-uwp`](https://github.com/caorthann-celt/love-xbox-uwp) at commit `cdf85f28ed794c95d6f5f7e3a8f23ca9ee1fbdcb`.
The build uses LÖVE 11.5, LuaJIT, SDL2, and ANGLE. Keep the DLLs and import libraries together; they are one binary interface.
Binary file not shown.
+2 -2
View File
@@ -7,7 +7,7 @@
"love": {
"version": "11.5",
"repository": "https://github.com/caorthann-celt/love-xbox-uwp.git",
"commit": "62275ed10190afbec01f65754b3bcc04f244dd35"
"commit": "cdf85f28ed794c95d6f5f7e3a8f23ca9ee1fbdcb"
},
"luajit": {
"repository": "https://github.com/LuaJIT/LuaJIT.git",
@@ -99,7 +99,7 @@
},
{
"path": "love/bin/love.dll",
"sha256": "7B1BE4699AC950173474BB0C03B5DD52E1202ABA6F60C29E294957057F587692"
"sha256": "A3622828014F03DDB502D2A7807DC8F94432172595BE03508099BD66937FD45F"
},
{
"path": "love/bin/lua51.dll",
+16 -2
View File
@@ -541,11 +541,15 @@ local function romModel(imp, version, info, ready, locked)
label = Strings("Import unavailable"), enabled = false }
end
local dropHint = imp.isNX and Strings("Copy the .gb/.gbc via MTP into imports/.")
or (imp.android and Strings("Copy the .gb/.gbc via USB.")
or Strings("Or drop the .gb/.gbc file here."))
or (imp.baseRomDiscovery and Strings("Or copy the .gb/.gbc into baseroms/.")
or (imp.android and Strings("Copy the .gb/.gbc via USB.")
or Strings("Or drop the .gb/.gbc file here.")))
local importing = imp.importing == version
local erroring = imp.workState == "error" and imp.errorVersion == version
local notice = imp.notice and imp.notice.version == version and imp.notice
local baseRom = imp.baseRoms and imp.baseRoms[version]
local scanning = imp.baseRomDiscovery and imp.baseRomScan
and imp.baseRomScan.state ~= "done"
if importing and (imp.workState == "working" or imp.workState == "complete") then
return { state = imp.status or Strings("Importing"),
detail = imp.detail or "", progress = imp.progress or 0 }
@@ -564,6 +568,14 @@ local function romModel(imp, version, info, ready, locked)
detail = ((notice.status or "") .. " " .. (notice.detail or ""))
:gsub("^%s+", ""):gsub("%s+$", ""),
label = importLabel, enabled = true }
elseif baseRom then
return { state = Strings("Compatible ROM found"),
detail = Strings("Found in baseroms/: %s", baseRom.name),
label = Strings("Import detected ROM"), enabled = true }
elseif scanning then
return { state = Strings("Checking baseroms..."),
detail = Strings("Looking for compatible Red, Blue, and Yellow ROMs."),
label = Strings("Import ROM"), enabled = false }
elseif imp.returning[version] then
return { state = Strings("Update required"),
detail = Strings("This build needs a few more things from your ")
@@ -922,6 +934,8 @@ local function buildGamePanel(imp, x, y, w, availH, m, version)
Kit.text("title", Kit.ellipsize("title", gameName, w * 0.6), x, y, PAL.heading)
local tagText, tagCol
if ready then tagText, tagCol = Strings("GOOD TO GO"), PAL.green
elseif imp.baseRoms and imp.baseRoms[version] then
tagText, tagCol = Strings("ROM FOUND"), PAL.green
elseif locked then tagText, tagCol = Strings("COMING SOON"), PAL.steel
else tagText, tagCol = Strings("ROM REQUIRED"), PAL.yellow end
local tagW = Kit.textWidth("micro", tagText) + math.floor(18 * m.s)
+84
View File
@@ -341,6 +341,7 @@ local function commandOutput(command)
end
local IMPORTS_DIR = "imports"
local BASE_ROMS_DIR = "baseroms"
local MODS_INBOX_DIR = "imports/mods"
local SAVES_INBOX_DIR = "imports/saves"
local ROM_BYTES = 1024 * 1024
@@ -474,6 +475,65 @@ local function listRomPaths(dir)
return paths
end
local function baseRomScanSatisfied(self)
for _, version in ipairs(GameVersion.ORDER) do
if not self.ready[version] and not self.baseRoms[version] then
return false
end
end
return true
end
function RomImporter:_queueBaseRomScan()
if not self.baseRomDiscovery then return end
if baseRomScanSatisfied(self) then
self.baseRomScan = { state = "done" }
return
end
self.baseRomScan = { state = "queued", index = 1 }
end
function RomImporter:_stepBaseRomScan()
local scan = self.baseRomScan
if not scan or scan.state == "done" or self.workState == "working" then
return
end
if scan.state == "queued" then
local info = love.filesystem.getInfo(BASE_ROMS_DIR)
if not info and love.filesystem.createDirectory then
love.filesystem.createDirectory(BASE_ROMS_DIR)
end
scan.paths = listRomPaths(BASE_ROMS_DIR)
table.sort(scan.paths)
scan.state = "running"
end
local path = scan.paths[scan.index]
if not path then
scan.state = "done"
return
end
scan.index = scan.index + 1
local info = love.filesystem.getInfo(path, "file")
if info and info.size == ROM_BYTES then
local data = love.filesystem.read(path)
if type(data) == "string" and #data == ROM_BYTES then
local version = GameVersion.forSha1(sha1(data))
if version and not self.ready[version] and not self.baseRoms[version] then
self.baseRoms[version] = {
path = path,
name = path:match("[^/\\]+$") or path,
}
end
end
end
if baseRomScanSatisfied(self) or not scan.paths[scan.index] then
scan.state = "done"
end
end
local function listZipPaths(dir)
local paths = {}
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
@@ -1050,6 +1110,9 @@ function RomImporter.new(onComplete, opts)
android = android,
ios = mobileOS == "iOS",
nativePicker = romImportMode == "native-picker",
baseRomDiscovery = opts.launcher and Platform.isUWP(),
baseRoms = {},
baseRomScan = nil,
-- One startup poll pass on both mobiles. iOS: files dropped through the
-- Files app are swept into the save dir before Lua boots (GRBootstrap) with
-- no love.focus event necessarily following. Android: the SAF picker is a
@@ -1149,6 +1212,7 @@ function RomImporter.new(onComplete, opts)
.. (info.id == "yellow" and ".gbc" or ".gb")
end
self:_applyLastVersionTab()
self:_queueBaseRomScan()
-- Android: import a save-dir .gb/.gbc that is not yet ready (USB drop or a
-- leftover SAF pick), routed by SHA-1. Already-imported carts are skipped
@@ -1755,6 +1819,21 @@ function RomImporter:choose(version)
self:rescanAction(self.chooseVersion)
return
end
local baseRom = self.baseRomDiscovery and self.baseRoms[self.chooseVersion]
if baseRom then
self.baseRoms[self.chooseVersion] = nil
local data = love.filesystem.read(baseRom.path)
if not data then
self.notice = {
version = self.chooseVersion,
status = "The detected ROM is no longer available.",
detail = "Choose Import ROM to select it another way.",
}
return
end
self:startData(data, baseRom.name)
return
end
if self.nativePicker and love.system.getPickedFile then
self.pickerPendingKind = "rom"
if not pickFile("rom") then
@@ -1877,6 +1956,7 @@ end
function RomImporter:update(dt)
self.pulse = self.pulse + dt
self:_updatePadCursor(dt)
self:_stepBaseRomScan()
-- Pump the FlexLove view (input polling + the queued click actions). The
-- flag is only set once draw() has built a tree, so headless runs and the
-- test tier never touch the toolkit.
@@ -2293,6 +2373,10 @@ function RomImporter:reimport(version)
self.ready[version] = false
self.returning[version] = false
self.chooseVersion = version
if self.baseRomDiscovery then
self.baseRoms[version] = nil
self:_queueBaseRomScan()
end
end
local function clamp(v, lo, hi)
+156
View File
@@ -0,0 +1,156 @@
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("UWP baseroms discovery")
local check, eq = S.check, S.eq
local GameVersion = require("src.core.GameVersion")
local Platform = require("src.core.Platform")
local RomImporter = require("src.import.RomImporter")
love.data = love.data or {}
love.system = love.system or {}
love.filesystem = love.filesystem or {}
local MiB = 1024 * 1024
local roms = {
["baseroms/z-red.gb"] = string.rep("R", MiB),
["baseroms/a-blue.gb"] = string.rep("B", MiB),
["baseroms/b-yellow.gbc"] = string.rep("Y", MiB),
["baseroms/small.gb"] = "small",
["baseroms/unknown.gb"] = string.rep("?", MiB),
}
local saved = {
hash = love.data.hash,
encode = love.data.encode,
getOS = love.system.getOS,
pickFile = love.system.pickFile,
getPickedFile = love.system.getPickedFile,
read = love.filesystem.read,
getInfo = love.filesystem.getInfo,
getDirectoryItems = love.filesystem.getDirectoryItems,
createDirectory = love.filesystem.createDirectory,
isReady = RomImporter.isReady,
}
love.data.hash = function(_, data) return data:sub(1, 1) end
love.data.encode = function(_, _, digest)
if digest == "R" then return GameVersion.info("red").sha1 end
if digest == "B" then return GameVersion.info("blue").sha1 end
if digest == "Y" then return GameVersion.info("yellow").sha1 end
return "0000000000000000000000000000000000000000"
end
local reads, listings, picks = 0, 0, 0
love.filesystem.read = function(path)
reads = reads + 1
return roms[path]
end
love.filesystem.getInfo = function(path, filter)
if path == "baseroms" then
return filter and nil or { type = "directory" }
end
local data = roms[path]
if data and (not filter or filter == "file") then
return { type = "file", size = #data }
end
return nil
end
love.filesystem.getDirectoryItems = function(path)
if path ~= "baseroms" then return {} end
listings = listings + 1
return { "z-red.gb", "small.gb", "unknown.gb", "b-yellow.gbc", "a-blue.gb" }
end
love.filesystem.createDirectory = function() return true end
love.system.pickFile = function()
picks = picks + 1
return true
end
love.system.getPickedFile = function() return nil end
local function importer(ready)
return setmetatable({
baseRomDiscovery = true,
baseRoms = {},
ready = ready or { red = false, blue = false, yellow = false },
returning = {},
workState = nil,
nativePicker = true,
isNX = false,
}, RomImporter)
end
local allReady = importer({ red = true, blue = true, yellow = true })
allReady:_queueBaseRomScan()
eq(allReady.baseRomScan.state, "done", "ready launcher skips discovery")
eq(listings, 0, "ready launcher does not enumerate baseroms")
local imp = importer()
imp:_queueBaseRomScan()
for _ = 1, 5 do
local before = reads
imp:_stepBaseRomScan()
check(reads - before <= 1, "discovery reads at most one ROM per step")
end
eq(listings, 1, "discovery enumerates baseroms once")
eq(imp.baseRoms.blue.name, "a-blue.gb", "Blue ROM is detected by SHA-1")
eq(imp.baseRoms.yellow.name, "b-yellow.gbc", "Yellow ROM is detected by SHA-1")
eq(imp.baseRoms.red.name, "z-red.gb", "Red ROM is detected by SHA-1")
eq(reads, 4, "wrong-sized files are skipped before reading")
eq(imp.baseRomScan.state, "done", "discovery stops when every missing ROM is found")
local settledReads, settledListings = reads, listings
for _ = 1, 10 do imp:_stepBaseRomScan() end
eq(reads, settledReads, "completed discovery does not read again")
eq(listings, settledListings, "completed discovery does not enumerate again")
local detected = importer()
detected.baseRoms.red = { path = "baseroms/z-red.gb", name = "z-red.gb" }
detected.startData = function(self, data, name)
self.started = { data = data, name = name }
end
detected:choose("red")
eq(detected.started.name, "z-red.gb", "detected ROM uses the normal import path")
eq(picks, 0, "detected ROM does not open the picker")
check(roms["baseroms/z-red.gb"] ~= nil, "detected ROM remains in baseroms")
local missing = importer()
missing.baseRoms.red = { path = "baseroms/gone.gb", name = "gone.gb" }
missing:choose("red")
check(missing.notice ~= nil, "missing detected ROM reports a notice")
eq(picks, 0, "missing detected ROM does not open the picker unexpectedly")
missing:choose("red")
eq(picks, 1, "the next import attempt falls back to the native picker")
local rescanned = importer({ red = true, blue = true, yellow = true })
rescanned.baseRoms.red = { path = "baseroms/z-red.gb", name = "z-red.gb" }
rescanned:reimport("red")
check(rescanned.baseRoms.red == nil, "re-import clears the detected ROM")
eq(rescanned.baseRomScan.state, "queued", "re-import queues one fresh scan")
RomImporter.isReady = function() return false end
love.system.getOS = function() return "UWP" end
Platform._resetForTests()
local launcher = RomImporter.new(function() end, { launcher = true })
check(launcher.baseRomDiscovery, "UWP launcher enables baseroms discovery")
local importOnly = RomImporter.new(function() end)
check(not importOnly.baseRomDiscovery, "non-launcher UWP import stays unchanged")
love.system.getOS = function() return "Windows" end
Platform._resetForTests()
local desktop = RomImporter.new(function() end, { launcher = true })
check(not desktop.baseRomDiscovery, "desktop launcher does not scan baseroms")
love.data.hash = saved.hash
love.data.encode = saved.encode
love.system.getOS = saved.getOS
love.system.pickFile = saved.pickFile
love.system.getPickedFile = saved.getPickedFile
love.filesystem.read = saved.read
love.filesystem.getInfo = saved.getInfo
love.filesystem.getDirectoryItems = saved.getDirectoryItems
love.filesystem.createDirectory = saved.createDirectory
RomImporter.isReady = saved.isReady
Platform._resetForTests()
S.finish()