mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
Merge origin/dev (v0.1.74) into feat/switch-ota-unified-launcher.
Keep Switch version chip alongside upstream's new quit button in the launcher header.
This commit is contained in:
@@ -294,6 +294,9 @@ function CacheFs.read(rel)
|
||||
f:close()
|
||||
return data
|
||||
end
|
||||
-- headless (plain luajit, e.g. the modkit validate/pack driver): there is
|
||||
-- no save directory to read from, so a cache miss is nil, not a crash
|
||||
if not (love and love.filesystem) then return nil end
|
||||
return love.filesystem.read(rel)
|
||||
end
|
||||
|
||||
|
||||
@@ -242,6 +242,18 @@ local function coreRows(opts)
|
||||
opts.touchControls = tc
|
||||
return true
|
||||
end)
|
||||
-- VIBRATION sits with it (#806): same gate, same subsystem. Stepping
|
||||
-- the row buzzes once at the level being selected.
|
||||
local okTC, TC = pcall(require, "src.core.TouchControls")
|
||||
if okTC then
|
||||
add(Strings("VIBRATION"),
|
||||
function() return Strings(TC.hapticLabel(opts.haptics)) end,
|
||||
function(dir)
|
||||
opts.haptics = TC.cycleHaptics(opts.haptics, dir)
|
||||
TC.buzz(opts.haptics)
|
||||
return true
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -312,8 +312,20 @@ local function setPage(imp, key, v)
|
||||
imp._pages[key] = v
|
||||
end
|
||||
|
||||
-- A hand-drawn X, for the same reason drawCheck exists below: the UI font has
|
||||
-- no guaranteed glyph, and the launcher ships no icon asset for it.
|
||||
local function drawCross(x, y, size, color)
|
||||
love.graphics.push("all")
|
||||
love.graphics.setColor(color)
|
||||
love.graphics.setLineWidth(math.max(2, size * 0.16))
|
||||
love.graphics.setLineJoin("bevel")
|
||||
love.graphics.line(x, y, x + size, y + size)
|
||||
love.graphics.line(x + size, y, x, y + size)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- header
|
||||
-- Rail, logo row (settings on the right), tab bar.
|
||||
-- Rail, logo row (settings and quit on the right), tab bar.
|
||||
-- Returns the y at which content may start. Its vertical arithmetic is
|
||||
-- mirrored by headerHeight() at the bottom of this file (the short-window
|
||||
-- scroll decision needs the height before anything draws) -- keep in sync.
|
||||
@@ -356,7 +368,14 @@ local function buildHeader(imp, m)
|
||||
by + math.floor((gear - th) / 2), PAL.yellow)
|
||||
end
|
||||
|
||||
-- Settings gear, top-right corner.
|
||||
-- The right cluster is laid out right to left -- Quit outermost, the gear
|
||||
-- inboard of it -- but the two are REGISTERED gear first, because the first
|
||||
-- focusable of the first frame adopts the keyboard ring and that must not be
|
||||
-- the button that exits the app.
|
||||
local quitX = rx - gear
|
||||
rx = quitX - math.floor(6 * m.s)
|
||||
|
||||
-- Settings gear.
|
||||
imp._gearIcon = imp._gearIcon
|
||||
or love.graphics.newImage("assets/launcher/gear.png")
|
||||
rx = rx - gear
|
||||
@@ -381,6 +400,23 @@ local function buildHeader(imp, m)
|
||||
end
|
||||
end
|
||||
|
||||
-- Quit, top-right corner.
|
||||
do
|
||||
local x = quitX
|
||||
Kit._audit("control", x, by, gear, gear, "quit")
|
||||
local focused = Kit.focusable("quit", x, by, gear, gear)
|
||||
local hot = focused or Kit.hover(x, by, gear, gear)
|
||||
Theme.fill(x, by, gear, gear, hot and PAL.ink or PAL.bg, 1)
|
||||
Theme.stroke(x, by, gear, gear, PAL.line,
|
||||
hot and Theme.A.focus or Theme.A.hairline, 1)
|
||||
local pad = math.floor(gear * 0.32)
|
||||
drawCross(x + pad, by + pad, gear - 2 * pad,
|
||||
hot and { 0, 0, 0, 1 } or { 1, 1, 1, 0.85 })
|
||||
if Kit.press(x, by, gear, gear) or Kit._activateId == "quit" then
|
||||
queueAction(imp, "quit", function() imp:_quitApp() end)
|
||||
end
|
||||
end
|
||||
|
||||
-- The self-update control lives in the FOOTER next to the BCG mark (small,
|
||||
-- out of the wordmark's way -- it used to overlap the logo on a phone). It
|
||||
-- still GLOWS through Kit.button when there is something to act on.
|
||||
@@ -2180,7 +2216,15 @@ end
|
||||
-- pinned Play block used to walk up over the cards on a short window, which
|
||||
-- is unusable, and the footer simply lives below the fold until scrolled to.
|
||||
local function minPanelHeight(m)
|
||||
return math.floor(460 * m.s)
|
||||
-- One column stacks the actions card, the slot card and the pinned Play
|
||||
-- block in a single pile, so it needs more room than the side-by-side
|
||||
-- layout: 460 was tuned for two columns, and on a squat one-column window
|
||||
-- (a 4:3 device, a phone held upright) it left the slot card clipped
|
||||
-- inert against the pinned buttons -- Kit's clip bounds hit-testing, so
|
||||
-- no slot could be picked at all (#852). 660 fits the actions card, one
|
||||
-- slot row with its pager and New button, and the pinned block; whatever
|
||||
-- the window cannot show, the page scroll above reaches.
|
||||
return math.floor((m.twoCol and 460 or 660) * m.s)
|
||||
end
|
||||
|
||||
function LauncherView.draw(imp)
|
||||
|
||||
+73
-24
@@ -315,27 +315,16 @@ end
|
||||
-- keyboard-navigates (keyboard focus is a separate grab) but ignores the
|
||||
-- mouse entirely -- issue #254 on Linux. Whether it bites is a race with how
|
||||
-- long the click was held, which is why the same build picks one ROM fine and
|
||||
-- then hangs the mouse on the next. So pump until no button is held, letting
|
||||
-- SDL see the release and let go first; bounded, so a stuck button costs a
|
||||
-- moment and never the launcher. pump() only drains OS events into LOVE's
|
||||
-- queue -- it dispatches nothing -- so there is no reentry into mousepressed
|
||||
-- and the release is still delivered normally on the next frame.
|
||||
local function releasePointerGrab()
|
||||
if not (love.mouse and love.mouse.isDown and love.event and love.event.pump
|
||||
and love.timer) then
|
||||
return
|
||||
end
|
||||
local deadline = love.timer.getTime() + 1
|
||||
while love.mouse.isDown(1, 2, 3) do
|
||||
love.event.pump()
|
||||
if love.timer.getTime() > deadline then break end
|
||||
love.timer.sleep(0.005)
|
||||
end
|
||||
end
|
||||
-- then hangs the mouse on the next.
|
||||
--
|
||||
-- The release itself now lives in HostShell.releasePointerGrab, called from
|
||||
-- HostShell.popen, so every host spawn inherits it and not just the three
|
||||
-- pickers here. It stays a single release point on purpose: this file used
|
||||
-- to run its own copy first, and each copy carries its own one-second bound,
|
||||
-- so keeping both made a stuck button cost two seconds instead of one.
|
||||
|
||||
local function commandOutput(command)
|
||||
if not Platform.canSpawnProcess() then return nil end
|
||||
releasePointerGrab()
|
||||
local pipe = HostShell.popen(command)
|
||||
if not pipe then return nil end
|
||||
local result = pipe:read("*a")
|
||||
@@ -997,6 +986,25 @@ local function updaterAllowed()
|
||||
return true
|
||||
end
|
||||
|
||||
-- #835: which column the launcher opens on. `tab` starts at the --game
|
||||
-- shortcut's version (LaunchOptions.pendingTab) or Red; this then prefers the
|
||||
-- game play() last handed off, so relaunching lands on the game that was last
|
||||
-- played instead of always Red. An explicit --game still wins, and a
|
||||
-- remembered version whose cache is gone or stale is ignored, since opening a
|
||||
-- column with no Play button would read as the launcher losing the import.
|
||||
-- Called from new() once self.ready is filled, which is what that check needs.
|
||||
function RomImporter:_applyLastVersionTab()
|
||||
local okLO, LO = pcall(require, "src.core.LaunchOptions")
|
||||
if okLO and LO.pendingTab then return end
|
||||
local okOpt, opts = pcall(function()
|
||||
return require("src.core.SaveData").loadOptions()
|
||||
end)
|
||||
local last = okOpt and opts and opts.lastVersion
|
||||
if last and GameVersion.VERSIONS[last] and self.ready[last] then
|
||||
self.tab = last
|
||||
end
|
||||
end
|
||||
|
||||
-- The launcher runs each GameVersion as an independent tab. Each dropped or
|
||||
-- chosen ROM is routed to its version by SHA-1, extracted into that version's
|
||||
-- own cache (Red at the root, Blue under blue/, Yellow under yellow/), so all
|
||||
@@ -1128,6 +1136,7 @@ function RomImporter.new(onComplete, opts)
|
||||
self.romName[version] = "pokemon_" .. info.id
|
||||
.. (info.id == "yellow" and ".gbc" or ".gb")
|
||||
end
|
||||
self:_applyLastVersionTab()
|
||||
|
||||
-- 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
|
||||
@@ -2240,6 +2249,17 @@ function RomImporter:play(version)
|
||||
if self.workState == "working" then return end
|
||||
if not self.ready[version] then return end
|
||||
self._handedOff = true
|
||||
-- #835: remember the game being launched so the next launcher start opens on
|
||||
-- its column (_applyLastVersionTab). It rides options.lua rather than a file
|
||||
-- of its own, so portable installs and POKEPORT_IDENTITY sandboxes keep it
|
||||
-- with the rest of the launcher's persisted state. A failed write only
|
||||
-- costs the memory of the choice, so it must never block the boot.
|
||||
pcall(function()
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local opts = SaveData.loadOptions()
|
||||
opts.lastVersion = version
|
||||
SaveData.saveOptions(opts)
|
||||
end)
|
||||
resetPointerCursor(self)
|
||||
-- The game draws with raw love.graphics from here on; drop the view's
|
||||
-- element tree and canvases before the handoff.
|
||||
@@ -2378,10 +2398,18 @@ function RomImporter:runActions(queue)
|
||||
end
|
||||
|
||||
-- Clicks are polled inside FlexLove (mouse + love.touch); host-forwarded
|
||||
-- mousepressed stays inert so Android's synthesized mouse path cannot
|
||||
-- double-fire a tap (#553). Touch move/press/release must still reach
|
||||
-- mousepressed mints no click, so Android's synthesized mouse path cannot
|
||||
-- double-fire a tap (#553). It DOES hand the pointer back from the pad
|
||||
-- cursor (#781): a Linux boot with a joystick present arms it (see the
|
||||
-- getJoystickCount block in new()), and while it is active
|
||||
-- LauncherView.update refuses to mint mouse clicks, so a real press must
|
||||
-- win the pointer back even when the polled motion yield misses (X11
|
||||
-- multi-monitor coords). Same contract as PadCursor.yieldToPointer for
|
||||
-- the overlay hosts. Touch move/press/release must still reach
|
||||
-- FlexLove.touch* or scroll containers never drag on phones.
|
||||
function RomImporter:mousepressed() end
|
||||
function RomImporter:mousepressed()
|
||||
self._padCursorActive = false
|
||||
end
|
||||
|
||||
function RomImporter:touchpressed(id, x, y, dx, dy, pressure)
|
||||
if not self._flex then return end
|
||||
@@ -2428,6 +2456,13 @@ function RomImporter:_openSettings()
|
||||
if ok and model then self._settings = model end
|
||||
end
|
||||
|
||||
-- Quit from the launcher's own X. It goes through love.event.quit so main.lua's
|
||||
-- love.quit hook still runs: that is where the worker threads are shut down
|
||||
-- (#339) and where a launcher close is told apart from a running game's (#785).
|
||||
function RomImporter:_quitApp()
|
||||
if love.event and love.event.quit then love.event.quit() end
|
||||
end
|
||||
|
||||
function RomImporter:_closeSettings()
|
||||
if self._settings then self._settings.save() end
|
||||
self._settings = nil
|
||||
@@ -2871,9 +2906,14 @@ end
|
||||
-- Update button: when a newer release is known, confirm then install; when
|
||||
-- already current, force-refresh the 6h cache and report / offer update.
|
||||
function RomImporter:_modGithubAction(id, action)
|
||||
if not Platform.networkValidated() then
|
||||
-- canFetchRemote, not networkValidated: the self-updater's gate used to
|
||||
-- stand in for this one, which cost Xbox the whole mod catalog rather than
|
||||
-- just the self-update it actually cannot do (#876). Say what still works
|
||||
-- while we are here, since the native picker is live on every platform that
|
||||
-- lands in this branch.
|
||||
if not Platform.canFetchRemote() then
|
||||
self.modNotice = { ok = false,
|
||||
text = "Remote mod download is unavailable on this platform." }
|
||||
text = "Remote mod download is unavailable on this platform. Install a mod .zip from storage instead." }
|
||||
return
|
||||
end
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
@@ -3177,9 +3217,18 @@ end
|
||||
-- never ran. The fetch now starts here and completes across later frames in
|
||||
-- _pumpFindFetch; the loader overlay is up for the whole flight.
|
||||
function RomImporter:_refreshFind(force)
|
||||
if not Platform.networkValidated() then
|
||||
-- The notice is the fix, not the gate (#876). This branch used to return an
|
||||
-- empty listing silently, and because the player had by then added a source,
|
||||
-- the panel skipped its "No mod index added" card and rendered the merged
|
||||
-- listing empty state instead: a valid feed reported as "This index lists no
|
||||
-- mods yet." Every other failure on this panel surfaces through findNotice,
|
||||
-- and this one has to as well, or adding an index looks like it worked and
|
||||
-- the index looks empty.
|
||||
if not Platform.canFetchRemote() then
|
||||
self.findLoaded = true
|
||||
self.findIndex = { mods = {}, categories = {} }
|
||||
self.findNotice = { ok = false,
|
||||
text = "Mod indexes cannot be fetched on this platform. Install a mod .zip from storage instead." }
|
||||
return
|
||||
end
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
|
||||
Reference in New Issue
Block a user