mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-09 10:01:50 -04:00
PS4 platform support (LÖVE for PS4, GoldHEN), as platform capabilities
PS4 is described in Platform by what it can do, and the launcher asks for
those capabilities instead of checking console names:
- Platform: ps4 flag, console, no child processes, save-directory ROM import,
no self-updater or remote fetch. Three new capabilities:
padNavigation ("pointer" on NX, "focus" on Xbox and PS4),
systemQuit (iOS, PS4: no in-app Quit button) and
inboxTransfer ("mtp" on NX, "ftp" on PS4).
- RomImporter: isNX means the Switch again. The save-directory inbox flow
("Scan again", transfer hints) asks _inboxImport(); fixtures that only set
isNX keep the NX flow. Pad focus navigation follows padNavigation, so PS4
gets Xbox's D-pad focus ring by default (Triangle still toggles the cursor).
Inbox hints gain FTP variants; the NX strings are unchanged. Custom
Carts (.g1rcart) import rescans imports/carts/ on PS4 as on NX.
- LauncherView: Quit button and header layout follow systemQuit; import label,
drop hint and open-folder follow the inbox capability. pcall around the
footer invert shader so a driver rejecting it cannot abort the boot.
- conf.lua: fixed 1920x1080 window (single EGL surface on the console);
VideoMode/FaithfulRes: fixed display; Performance: balanced tier.
- tests/engine/platform_ps4_test.lua: the capability matrix for PS4, NX,
Xbox, iOS and desktop.
This commit is contained in:
@@ -81,7 +81,17 @@ function love.conf(t)
|
||||
local osName = love._os
|
||||
local mobile = osName == "Android" or osName == "iOS"
|
||||
local nx = osName == "NX"
|
||||
if nx then
|
||||
local ps4 = osName == "PS4"
|
||||
if ps4 then
|
||||
-- PS4 (love-ps4): scan-out is fixed at 1920x1080 and the EGL surface allows
|
||||
-- a single live instance -- recreating it leaves video dead without an
|
||||
-- error -- so the window is requested once and never changed.
|
||||
t.window.width = 1920
|
||||
t.window.height = 1080
|
||||
t.window.fullscreen = false
|
||||
t.window.resizable = false
|
||||
t.window.highdpi = false
|
||||
elseif nx then
|
||||
-- Switch (love-nx): hint handheld 720p. SDL auto-switches portable↔dock
|
||||
-- (720p↔1080p) only when the window is resizable and not exclusive
|
||||
-- fullscreen; NxDisplay.sync also applies the size on boot and dock change.
|
||||
|
||||
@@ -117,6 +117,7 @@ function FaithfulRes.fixedDisplay()
|
||||
if not love or not love.system or not love.system.getOS then return false end
|
||||
local osName = love.system.getOS()
|
||||
return osName == "Android" or osName == "iOS" or osName == "NX"
|
||||
or osName == "PS4"
|
||||
end
|
||||
|
||||
FaithfulRes.isMobile = FaithfulRes.fixedDisplay
|
||||
|
||||
@@ -111,6 +111,12 @@ function Performance.detect()
|
||||
if isArm and os == "Linux" then
|
||||
return "low"
|
||||
end
|
||||
-- PS4: 1.6 GHz Jaguar cores. cpuArch() cannot tell, and detect() fell
|
||||
-- through to "high", the most demanding tier. balanced drops the 3D tilt,
|
||||
-- the heaviest extra.
|
||||
if os == "PS4" then
|
||||
return "balanced"
|
||||
end
|
||||
-- Phones and tablets: balanced drops the 3D tilt, the heaviest extra.
|
||||
if os == "Android" or os == "iOS" then
|
||||
return "balanced"
|
||||
|
||||
+31
-4
@@ -9,6 +9,7 @@ local function compute()
|
||||
or "Unknown"
|
||||
local nx = osName == "NX"
|
||||
local uwp = osName == "UWP"
|
||||
local ps4 = osName == "PS4"
|
||||
local mobile = osName == "Android" or osName == "iOS"
|
||||
local nativePicker = love and love.system
|
||||
and type(love.system.pickFile) == "function"
|
||||
@@ -18,14 +19,15 @@ local function compute()
|
||||
os = osName,
|
||||
nx = nx,
|
||||
uwp = uwp,
|
||||
ps4 = ps4,
|
||||
mobile = mobile,
|
||||
console = nx or uwp,
|
||||
console = nx or uwp or ps4,
|
||||
hasNativePicker = nativePicker,
|
||||
canSpawnProcess = osName == "OS X" or osName == "Windows" or osName == "Linux",
|
||||
romImportMode = nx and "save-directory"
|
||||
romImportMode = (nx or ps4) and "save-directory"
|
||||
or (nativePicker and "native-picker")
|
||||
or "desktop",
|
||||
networkValidated = not nx and not uwp,
|
||||
networkValidated = not nx and not uwp and not ps4,
|
||||
-- networkValidated is the self-updater's gate and stays a per-platform
|
||||
-- policy call: a console package cannot replace itself on disk, so that
|
||||
-- answer never depends on whether a transport exists. Fetching a mod
|
||||
@@ -36,7 +38,16 @@ local function compute()
|
||||
-- (#597). The UWP LOVE backend does not export that bridge yet, so this
|
||||
-- still resolves false on Xbox and the launcher still says so, but the
|
||||
-- day the backend grows one, nothing here or in RomImporter has to change.
|
||||
canFetchRemote = (not nx and not uwp) or nativeHttp,
|
||||
canFetchRemote = (not nx and not uwp and not ps4) or nativeHttp,
|
||||
-- How a gamepad drives the launcher when it is the only input: the Switch
|
||||
-- moves a virtual cursor, Xbox and PS4 move a focus ring (the highlighted
|
||||
-- control is the selected one; Y/Triangle still toggles the cursor). nil
|
||||
-- on desktop and handhelds, where the cursor stays latent until used.
|
||||
padNavigation = (nx and "pointer") or ((uwp or ps4) and "focus") or nil,
|
||||
-- The system owns leaving the app (iOS Home, PS button): no Quit button.
|
||||
systemQuit = osName == "iOS" or ps4,
|
||||
-- How files reach a save-directory inbox, for the hints under its path.
|
||||
inboxTransfer = (nx and "mtp") or (ps4 and "ftp") or nil,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -53,10 +64,26 @@ function Platform.isUWP()
|
||||
return Platform.detect().uwp
|
||||
end
|
||||
|
||||
function Platform.isPS4()
|
||||
return Platform.detect().ps4
|
||||
end
|
||||
|
||||
function Platform.romImportMode()
|
||||
return Platform.detect().romImportMode
|
||||
end
|
||||
|
||||
function Platform.padNavigation()
|
||||
return Platform.detect().padNavigation
|
||||
end
|
||||
|
||||
function Platform.systemQuit()
|
||||
return Platform.detect().systemQuit and true or false
|
||||
end
|
||||
|
||||
function Platform.inboxTransfer()
|
||||
return Platform.detect().inboxTransfer
|
||||
end
|
||||
|
||||
function Platform.canSpawnProcess()
|
||||
return Platform.detect().canSpawnProcess
|
||||
end
|
||||
|
||||
@@ -23,6 +23,7 @@ function VideoMode.fixedDisplay()
|
||||
if not love or not love.system or not love.system.getOS then return false end
|
||||
local osName = love.system.getOS()
|
||||
return osName == "Android" or osName == "iOS" or osName == "NX"
|
||||
or osName == "PS4"
|
||||
end
|
||||
|
||||
VideoMode.isMobile = VideoMode.fixedDisplay
|
||||
|
||||
+37
-13
@@ -42,6 +42,20 @@ local WebClip = require("src.core.WebClip")
|
||||
local PAL = Theme.PAL
|
||||
local LauncherView = {}
|
||||
|
||||
-- Platform capabilities as the view needs them. Fields on LauncherView, not
|
||||
-- locals: this chunk sits at Lua's 200-local limit. They read plain importer
|
||||
-- fields, not methods, so hand-built importers in tests keep working.
|
||||
-- Save-directory inbox import (Switch, PS4); same rule as
|
||||
-- RomImporter:_inboxImport().
|
||||
function LauncherView.inboxImport(imp)
|
||||
return (imp.inboxImport or imp.isNX) and true or false
|
||||
end
|
||||
|
||||
-- iOS and PS4 leave the app through the system (Home, PS button).
|
||||
function LauncherView.hasQuitButton(imp)
|
||||
return not imp.ios and not require("src.core.Platform").systemQuit()
|
||||
end
|
||||
|
||||
local COMMUNITY_URL = "https://bois.icu"
|
||||
|
||||
-- One dedup window covers a touch release plus the mouse click SDL
|
||||
@@ -1635,9 +1649,9 @@ local function buildHeader(imp, m)
|
||||
-- under the gear and the quit X -- "the settings is covering the logo".
|
||||
-- Reserving the space on both sides costs a little width and cannot
|
||||
-- overlap at any window size.
|
||||
-- iOS has no quit button (the OS owns app exit), so the cluster is the
|
||||
-- No quit button on iOS/PS4 (the OS owns app exit), so the cluster is the
|
||||
-- gear alone and the wordmark gets that width back
|
||||
local clusterN = imp.ios and 2 or 3
|
||||
local clusterN = LauncherView.hasQuitButton(imp) and 3 or 2
|
||||
local clusterW = clusterN * gear + (clusterN - 1) * math.floor(6 * m.s) + m.pad
|
||||
local mobile = not imp.isNX or not m.twoCol
|
||||
local boxX = mobile and (m.x + m.pad) or (m.x + clusterW)
|
||||
@@ -1682,7 +1696,7 @@ local function buildHeader(imp, m)
|
||||
-- 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 = not imp.ios and rx - gear or nil
|
||||
local quitX = LauncherView.hasQuitButton(imp) and rx - gear or nil
|
||||
if quitX then rx = quitX - math.floor(6 * m.s) end
|
||||
|
||||
-- Settings gear. It now also owns the CONTROL settings (touch overlay
|
||||
@@ -1823,14 +1837,18 @@ end
|
||||
-- enabled whether that button may be pressed
|
||||
-- progress 0-1 while an import for THIS version is running
|
||||
local function romModel(imp, version, info, ready, locked)
|
||||
local importLabel = imp.isNX and Strings("Scan again") or Strings("Import ROM")
|
||||
local importLabel = LauncherView.inboxImport(imp) and Strings("Scan again")
|
||||
or Strings("Import ROM")
|
||||
if locked then
|
||||
return { state = Strings("Not supported yet"),
|
||||
detail = Strings("Support for this game is on the way."),
|
||||
label = Strings("Import unavailable"), enabled = false }
|
||||
end
|
||||
local ext = GameVersion.generation(version) == 3 and ".gba" or ".gb/.gbc"
|
||||
local dropHint = imp.isNX and Strings("Copy the %s via MTP into imports/.", ext)
|
||||
local viaFtp = require("src.core.Platform").inboxTransfer() == "ftp"
|
||||
local dropHint = LauncherView.inboxImport(imp) and (viaFtp
|
||||
and Strings("Copy the %s via FTP into imports/.", ext)
|
||||
or Strings("Copy the %s via MTP into imports/.", ext))
|
||||
or (imp.baseRomDiscovery and Strings("Or copy the %s into baseroms/.", ext)
|
||||
or (imp.android and Strings("Copy the %s via USB.", ext)
|
||||
or Strings("Or drop the %s file here.", ext)))
|
||||
@@ -1986,7 +2004,7 @@ local function buildSlotCard(imp, x, y, w, availH, m, version, ready)
|
||||
for _, entry in ipairs(slots) do if entry.id == active then slot = entry break end end
|
||||
local pad, gap = math.floor(14 * m.s), math.floor(8 * m.s)
|
||||
local iw, bh = w - 2 * pad, m.btnH
|
||||
local importLabel = imp.isNX and Strings("Scan again") or Strings("Import")
|
||||
local importLabel = LauncherView.inboxImport(imp) and Strings("Scan again") or Strings("Import")
|
||||
local browseLabel = Strings("Other saves (%d)", #slots)
|
||||
local iconExtra = math.floor(bh * 0.42) + math.floor(7 * Kit.scale)
|
||||
local importW = chipWidth(importLabel, m) + iconExtra
|
||||
@@ -3710,12 +3728,18 @@ local function buildFooter(imp, m, y)
|
||||
Theme.fill(m.x, y, m.w, 1, PAL.line, Theme.A.hairline)
|
||||
local cy = y + math.floor(8 * m.s)
|
||||
-- The BCG mark is dark ink; invert it for the black field.
|
||||
imp.invertShader = imp.invertShader or love.graphics.newShader([[
|
||||
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
|
||||
vec4 p = Texel(tex, tc);
|
||||
return vec4((vec3(1.0) - p.rgb) * color.rgb, p.a * color.a);
|
||||
}
|
||||
]])
|
||||
-- pcall: this was the only unguarded newShader on the Gen 1 path, and a
|
||||
-- driver rejecting this trivial fragment shader took down the whole boot
|
||||
-- instead of just losing the inverted footer mark.
|
||||
if imp.invertShader == nil then
|
||||
local ok, sh = pcall(love.graphics.newShader, [[
|
||||
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
|
||||
vec4 p = Texel(tex, tc);
|
||||
return vec4((vec3(1.0) - p.rgb) * color.rgb, p.a * color.a);
|
||||
}
|
||||
]])
|
||||
imp.invertShader = ok and sh or false
|
||||
end
|
||||
local bw, bh = imp.bcg:getDimensions()
|
||||
local scale = math.min((130 * m.s) / bw, (22 * m.s) / bh)
|
||||
local dw, dh = bw * scale, bh * scale
|
||||
@@ -5513,7 +5537,7 @@ local function buildGameManageModal(imp, m)
|
||||
and love.filesystem.getSaveDirectory() or nil
|
||||
-- The folder link is desktop-only: Android and NX have no browsable path to
|
||||
-- open, and both already print their own transfer hint on the slot card.
|
||||
local canOpenFolder = saveDir and not imp.android and not imp.isNX
|
||||
local canOpenFolder = saveDir and not imp.android and not LauncherView.inboxImport(imp)
|
||||
local canWebClip = ready and webClipAvailable(imp)
|
||||
local webClipKey = tostring(version) .. ":" .. tostring(cartId or "")
|
||||
local webClipNotice = imp._webClipNotice
|
||||
|
||||
+61
-28
@@ -586,7 +586,9 @@ function RomImporter:_setNxInboxNotice(version)
|
||||
self.notice = {
|
||||
version = version,
|
||||
status = Strings("Copy your .gb/.gbc/.gba into:"),
|
||||
detail = Strings("%s/imports/\nDBI MTP → 1: SD Card/%simports/", saveDir, rel),
|
||||
detail = Platform.inboxTransfer() == "ftp"
|
||||
and Strings("%s/imports/\nover FTP (GoldHEN, port 2121)", saveDir)
|
||||
or Strings("%s/imports/\nDBI MTP → 1: SD Card/%simports/", saveDir, rel),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -594,7 +596,9 @@ 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(
|
||||
self._cartNotice = Platform.inboxTransfer() == "ftp"
|
||||
and Strings("Copy your .g1rcart into:\n%s/imports/carts/\nover FTP (GoldHEN, port 2121)", saveDir)
|
||||
or Strings(
|
||||
"Copy your .g1rcart into:\n%s/imports/carts/\nDBI MTP → 1: SD Card/%simports/carts/",
|
||||
saveDir, rel)
|
||||
end
|
||||
@@ -605,7 +609,9 @@ function RomImporter:_setNxModsInboxNotice()
|
||||
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
|
||||
self.modNotice = {
|
||||
ok = true,
|
||||
text = Strings("Copy your .zip into:\n%s/imports/mods/\nDBI MTP → 1: SD Card/%simports/mods/",
|
||||
text = Platform.inboxTransfer() == "ftp"
|
||||
and Strings("Copy your .zip into:\n%s/imports/mods/\nover FTP (GoldHEN, port 2121)", saveDir)
|
||||
or Strings("Copy your .zip into:\n%s/imports/mods/\nDBI MTP → 1: SD Card/%simports/mods/",
|
||||
saveDir, rel),
|
||||
}
|
||||
end
|
||||
@@ -626,7 +632,10 @@ function RomImporter:_setNxSavesInboxNotice(version)
|
||||
self.saveNotice = self.saveNotice or {}
|
||||
self.saveNotice[version] = {
|
||||
ok = true,
|
||||
text = Strings("Copy your %s .sav into:\n%s/%s/\nDBI MTP → 1: SD Card/%s%s/",
|
||||
text = Platform.inboxTransfer() == "ftp"
|
||||
and Strings("Copy your %s .sav into:\n%s/%s/\nover FTP (GoldHEN, port 2121)",
|
||||
game, saveDir, inbox)
|
||||
or Strings("Copy your %s .sav into:\n%s/%s/\nDBI MTP → 1: SD Card/%s%s/",
|
||||
game, saveDir, inbox, rel, inbox),
|
||||
}
|
||||
end
|
||||
@@ -1012,7 +1021,7 @@ function RomImporter:rescanAction(version)
|
||||
end
|
||||
|
||||
function RomImporter:_romAction(version)
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
if self.ready[version] then self:reimport(version)
|
||||
else self:rescanAction(version) end
|
||||
elseif self.ready[version] then self:reimport(version)
|
||||
@@ -1518,6 +1527,8 @@ function RomImporter.new(onComplete, opts)
|
||||
onEditTouchControls = opts.onEditTouchControls,
|
||||
onOpenSkinStudio = opts.onOpenSkinStudio,
|
||||
isNX = isNX,
|
||||
-- Save-directory inbox import (Switch, PS4): see _inboxImport().
|
||||
inboxImport = romImportMode == "save-directory",
|
||||
romImportMode = romImportMode,
|
||||
mobileFileBridge = mobileFileBridge,
|
||||
android = android,
|
||||
@@ -1691,7 +1702,7 @@ function RomImporter.new(onComplete, opts)
|
||||
-- is up, so a rejected pick can outlive the focus handler (#442).
|
||||
consumePickedRomError(self)
|
||||
end
|
||||
elseif self.isNX and self.launcher then
|
||||
elseif self:_inboxImport() and self.launcher then
|
||||
self:ensureImportsDir()
|
||||
self:_setNxInboxNotice()
|
||||
end
|
||||
@@ -2173,7 +2184,7 @@ function RomImporter:_completeImport(version, prefix, displayName)
|
||||
RomImporter.syncAndroidShortcuts(version)
|
||||
-- NX launcher stays put: keep the imports/ cleanup hint instead of
|
||||
-- overwriting it with a "Starting…" line that never boots from here.
|
||||
if self.launcher and self.isNX and type(displayName) == "string" then
|
||||
if self.launcher and self:_inboxImport() and type(displayName) == "string" then
|
||||
self.detail = Strings("%s imported. You may delete the copy from "
|
||||
.. "imports/ when finished.", displayName)
|
||||
else
|
||||
@@ -2573,7 +2584,7 @@ end
|
||||
function RomImporter:chooseMod()
|
||||
if self.workState == "working" then return end
|
||||
if self._hostPick or self._modInboxInstall then return end
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
self:ensureModsInboxDir()
|
||||
self:rescanModsAction()
|
||||
return
|
||||
@@ -2963,7 +2974,7 @@ function RomImporter:chooseRequiredImport(modId, importId)
|
||||
local spec = requiredSpec(manifest, importId)
|
||||
if not spec then return end
|
||||
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
local inbox = "imports/baseroms"
|
||||
local fs = pfs()
|
||||
fs.createDirectory(inbox)
|
||||
@@ -2990,7 +3001,9 @@ function RomImporter:chooseRequiredImport(modId, importId)
|
||||
end
|
||||
end
|
||||
requiredImportNotice(self, modId, importId, lastError
|
||||
or "No matching file in imports/baseroms/. Copy it there over MTP, then try again.")
|
||||
or (Platform.inboxTransfer() == "ftp"
|
||||
and "No matching file in imports/baseroms/. Copy it there over FTP, then try again."
|
||||
or "No matching file in imports/baseroms/. Copy it there over MTP, then try again."))
|
||||
self.modNotice = nil
|
||||
return
|
||||
end
|
||||
@@ -3104,7 +3117,7 @@ end
|
||||
function RomImporter:chooseSaveImport(version)
|
||||
if self.workState == "working" then return end
|
||||
version = self:_resolveSaveVersion(version)
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
self:ensureSavesInboxDir(version)
|
||||
self:rescanSavesAction(version)
|
||||
return
|
||||
@@ -3198,14 +3211,16 @@ function RomImporter:exportSave(version, format, scope, slotId)
|
||||
self.saveNotice[noticeScope] = { ok = false, text = tostring(res) }
|
||||
return
|
||||
end
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
local saveDir = love.filesystem.getSaveDirectory()
|
||||
local rel = RomImporter.mtpHintPath(saveDir)
|
||||
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
|
||||
local outDir = exportsDir(version)
|
||||
self.saveNotice[noticeScope] = {
|
||||
ok = true,
|
||||
text = Strings("Exported to %s\nDBI MTP → 1: SD Card/%s%s/", res, rel, outDir),
|
||||
text = Platform.inboxTransfer() == "ftp"
|
||||
and Strings("Exported to %s\nget it over FTP (GoldHEN, port 2121)", res)
|
||||
or Strings("Exported to %s\nDBI MTP → 1: SD Card/%s%s/", res, rel, outDir),
|
||||
}
|
||||
return
|
||||
end
|
||||
@@ -3276,7 +3291,7 @@ end
|
||||
function RomImporter:choose(version)
|
||||
if self.workState == "working" then return end
|
||||
self.chooseVersion = version or "red"
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
-- Same path as the Scan again button: rescan imports/ (or show MTP hint).
|
||||
self:rescanAction(self.chooseVersion)
|
||||
return
|
||||
@@ -3728,7 +3743,15 @@ local PAD_SPEED = 560 -- px/s at full stick deflection
|
||||
local PAD_DPAD_SPEED = 420
|
||||
|
||||
function RomImporter:_consolePointerHost()
|
||||
return (self.isNX or Platform.isUWP()) and true or false
|
||||
return (self.isNX or Platform.padNavigation() ~= nil) and true or false
|
||||
end
|
||||
|
||||
-- True when ROMs, mods and saves arrive through the save-directory inbox
|
||||
-- (imports/) instead of a file picker: "Scan again", transfer hints. Asked
|
||||
-- here rather than through isNX, which means the Switch itself. isNX still
|
||||
-- counts so hand-built importers (tests) that only set it keep the NX flow.
|
||||
function RomImporter:_inboxImport()
|
||||
return (self.inboxImport or self.isNX) and true or false
|
||||
end
|
||||
|
||||
function RomImporter:_activatePadCursor()
|
||||
@@ -3932,7 +3955,7 @@ function RomImporter:_updatePadCursor(dt)
|
||||
end
|
||||
|
||||
if not self._padCursorActive and not self.isNX
|
||||
and (Platform.isUWP() or self._padNavChosen) then
|
||||
and (Platform.padNavigation() == "focus" or self._padNavChosen) then
|
||||
self:_navigateWithStick(dt, okKit and Kit or nil)
|
||||
local scrollY = self._padAxis.righty or 0
|
||||
if math.abs(scrollY) > PAD_DEAD and self._flex then
|
||||
@@ -5164,16 +5187,18 @@ function RomImporter:_syncResolve(key, choice)
|
||||
end
|
||||
|
||||
function RomImporter:_skinsImportButtonLabel()
|
||||
if self.isNX then return Strings("Scan again") end
|
||||
if self:_inboxImport() then return Strings("Scan again") end
|
||||
return Strings("Import skin .zip")
|
||||
end
|
||||
|
||||
function RomImporter:chooseSkin()
|
||||
if self.workState == "working" then return end
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
local found = #self:_ensureSkins(true)
|
||||
self._skinNotice = { ok = true, text = Strings(
|
||||
"%d skins found. Copy a skin .zip into %s/ over MTP, then scan again.",
|
||||
Platform.inboxTransfer() == "ftp"
|
||||
and "%d skins found. Copy a skin .zip into %s/ over FTP, then scan again."
|
||||
or "%d skins found. Copy a skin .zip into %s/ over MTP, then scan again.",
|
||||
found, require("src.core.TouchSkin").USER_ROOT) }
|
||||
return
|
||||
end
|
||||
@@ -5461,19 +5486,19 @@ function RomImporter:rescanCartsAction(version)
|
||||
end
|
||||
|
||||
function RomImporter:_cartImportButtonLabel()
|
||||
if self.isNX then return Strings("Scan again") end
|
||||
if self:_inboxImport() 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
|
||||
-- mod, and save import: NX and PS4 rescan 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
|
||||
if self:_inboxImport() then
|
||||
self:ensureCartsInboxDir()
|
||||
return self:rescanCartsAction(version)
|
||||
end
|
||||
@@ -6324,11 +6349,12 @@ function RomImporter:exportCart(id)
|
||||
local base = fs.getSaveDirectory and fs.getSaveDirectory() or ""
|
||||
if base ~= "" then abs = base .. "/" .. rel end
|
||||
end
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
local hint = RomImporter.mtpHintPath(love.filesystem.getSaveDirectory())
|
||||
if hint ~= "" and hint:sub(-1) ~= "/" then hint = hint .. "/" end
|
||||
self._cartNotice =
|
||||
Strings("Exported to %s\nDBI MTP → 1: SD Card/%sexports/carts/", abs, hint)
|
||||
Platform.inboxTransfer() == "ftp" and Strings("Exported to %s\nget it over FTP (GoldHEN, port 2121)", abs)
|
||||
or Strings("Exported to %s\nDBI MTP → 1: SD Card/%sexports/carts/", abs, hint)
|
||||
return
|
||||
end
|
||||
if self.android then
|
||||
@@ -7946,15 +7972,18 @@ end
|
||||
|
||||
-- NX / desktop / Android labels and inbox hints for the FlexLove view.
|
||||
function RomImporter:_modsImportButtonLabel()
|
||||
if self.isNX then return Strings("Scan again") end
|
||||
if self:_inboxImport() then return Strings("Scan again") end
|
||||
return Strings("Import mod .zip")
|
||||
end
|
||||
|
||||
function RomImporter:_modsDefaultHint()
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
local saveDir = love.filesystem.getSaveDirectory()
|
||||
local rel = RomImporter.mtpHintPath(saveDir)
|
||||
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
|
||||
if Platform.inboxTransfer() == "ftp" then
|
||||
return Strings("Copy a .zip via FTP (GoldHEN, port 2121) into %s/imports/mods/", saveDir)
|
||||
end
|
||||
return Strings("Copy a .zip via MTP into %s/imports/mods/\n"
|
||||
.. "DBI MTP → 1: SD Card/%simports/mods/", saveDir, rel)
|
||||
end
|
||||
@@ -7963,13 +7992,17 @@ function RomImporter:_modsDefaultHint()
|
||||
end
|
||||
|
||||
function RomImporter:_savesDefaultHint(version)
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
version = self:_resolveSaveVersion(version)
|
||||
local inbox = savesInboxDir(version)
|
||||
local saveDir = love.filesystem.getSaveDirectory()
|
||||
local rel = RomImporter.mtpHintPath(saveDir)
|
||||
if rel ~= "" and rel:sub(-1) ~= "/" then rel = rel .. "/" end
|
||||
local game = GameVersion.info(version).displayName
|
||||
if Platform.inboxTransfer() == "ftp" then
|
||||
return Strings("Copy a %s .sav via FTP (GoldHEN, port 2121) into %s/%s/",
|
||||
game, saveDir, inbox)
|
||||
end
|
||||
return Strings("Copy a %s .sav via MTP into %s/%s/\n"
|
||||
.. "DBI MTP → 1: SD Card/%s%s/", game, saveDir, inbox, rel, inbox)
|
||||
end
|
||||
@@ -7980,7 +8013,7 @@ function RomImporter:_savesDefaultHint(version)
|
||||
end
|
||||
|
||||
function RomImporter:_modsEmptyHint()
|
||||
if self.isNX then
|
||||
if self:_inboxImport() then
|
||||
return Strings("No mods installed - copy a .zip into imports/mods/ "
|
||||
.. "and tap Scan again.")
|
||||
end
|
||||
|
||||
@@ -80,6 +80,22 @@ 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")
|
||||
|
||||
-- PS4 shares the NX inbox (no file picker), with FTP in the hints.
|
||||
love.system.getOS = function() return "PS4" end
|
||||
Platform._resetForTests()
|
||||
local ps4 = RomImporter.new(function() end, { launcher = true })
|
||||
ps4._installCartFile = nx._installCartFile
|
||||
eq(ps4:_cartImportButtonLabel(), "Scan again", "PS4 cart button rescans the inbox")
|
||||
love.filesystem.write("imports/carts/inbox.g1rcart", "cart-bytes")
|
||||
eq(ps4:importCartFile("red"), true, "PS4 Import scans imports/carts/")
|
||||
eq(ps4.installed, "imports/carts/inbox.g1rcart", "and installs the inbox file")
|
||||
love.filesystem.remove("imports/carts/inbox.g1rcart")
|
||||
ps4.installed = nil
|
||||
eq(ps4:importCartFile("red"), false, "an empty PS4 inbox does not invent a cart")
|
||||
check(ps4._cartNotice:find("over FTP", 1, true) ~= nil
|
||||
and ps4._cartNotice:find("DBI MTP", 1, true) == nil,
|
||||
"and the notice says FTP, not the Switch's MTP")
|
||||
|
||||
love.system.getOS = saved.getOS
|
||||
love.system.pickFile = saved.pickFile
|
||||
love.system.pickFileKinds = saved.pickFileKinds
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
-- PS4 capability detection, and the launcher capabilities it shares with or
|
||||
-- keeps apart from NX / Xbox / iOS / desktop.
|
||||
-- Self-contained: luajit tests/engine/platform_ps4_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local eq = T.eq
|
||||
|
||||
local savedLove = _G.love
|
||||
|
||||
local function withOS(osName, pickFile, fn)
|
||||
_G.love = {
|
||||
system = {
|
||||
getOS = function() return osName end,
|
||||
pickFile = pickFile,
|
||||
},
|
||||
}
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
local Platform = require("src.core.Platform")
|
||||
Platform._resetForTests()
|
||||
local ok, err = pcall(fn, Platform)
|
||||
_G.love = savedLove
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
if not ok then error(err) end
|
||||
end
|
||||
|
||||
-- PS4: the Switch's inbox import, Xbox's focus navigation, iOS's system quit.
|
||||
withOS("PS4", nil, function(Platform)
|
||||
local caps = Platform.detect()
|
||||
eq(caps.ps4, true, "PS4 flag")
|
||||
eq(caps.nx, false, "PS4 is not NX")
|
||||
eq(caps.console, true, "PS4 console")
|
||||
eq(caps.romImportMode, "save-directory", "PS4 imports through the inbox")
|
||||
eq(caps.canSpawnProcess, false, "PS4 cannot spawn processes")
|
||||
eq(caps.networkValidated, false, "PS4 has no self-updater")
|
||||
eq(Platform.isPS4(), true, "isPS4 convenience")
|
||||
eq(Platform.isNX(), false, "isNX false on PS4")
|
||||
eq(Platform.padNavigation(), "focus", "PS4 pad drives the focus ring")
|
||||
eq(Platform.systemQuit(), true, "PS4: the PS button owns quitting")
|
||||
eq(Platform.inboxTransfer(), "ftp", "PS4 inbox hints say FTP")
|
||||
end)
|
||||
|
||||
withOS("NX", nil, function(Platform)
|
||||
eq(Platform.padNavigation(), "pointer", "NX pad drives the virtual cursor")
|
||||
eq(Platform.systemQuit(), false, "NX keeps the Quit button")
|
||||
eq(Platform.inboxTransfer(), "mtp", "NX inbox hints say MTP")
|
||||
end)
|
||||
|
||||
withOS("UWP", function() end, function(Platform)
|
||||
eq(Platform.padNavigation(), "focus", "Xbox pad drives the focus ring")
|
||||
eq(Platform.systemQuit(), false, "Xbox keeps the Quit button")
|
||||
eq(Platform.inboxTransfer(), nil, "Xbox has no inbox")
|
||||
end)
|
||||
|
||||
withOS("iOS", function() end, function(Platform)
|
||||
eq(Platform.systemQuit(), true, "iOS: Home owns quitting")
|
||||
eq(Platform.padNavigation(), nil, "iOS pad cursor stays latent")
|
||||
end)
|
||||
|
||||
withOS("OS X", nil, function(Platform)
|
||||
eq(Platform.padNavigation(), nil, "desktop pad cursor stays latent")
|
||||
eq(Platform.systemQuit(), false, "desktop keeps the Quit button")
|
||||
eq(Platform.inboxTransfer(), nil, "desktop has no inbox")
|
||||
end)
|
||||
|
||||
T.finish()
|
||||
Reference in New Issue
Block a user