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:
Tomas Morello
2026-09-23 01:13:50 -03:00
parent bf45bb51d6
commit c073009099
9 changed files with 231 additions and 46 deletions
+11 -1
View File
@@ -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.
+1
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+16
View File
@@ -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
+67
View File
@@ -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()