diff --git a/conf.lua b/conf.lua index 74c9f021..50b63e30 100644 --- a/conf.lua +++ b/conf.lua @@ -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. diff --git a/src/core/FaithfulRes.lua b/src/core/FaithfulRes.lua index 843ebfa5..f9f546ae 100644 --- a/src/core/FaithfulRes.lua +++ b/src/core/FaithfulRes.lua @@ -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 diff --git a/src/core/Performance.lua b/src/core/Performance.lua index 4d61b65f..b9be0c27 100644 --- a/src/core/Performance.lua +++ b/src/core/Performance.lua @@ -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" diff --git a/src/core/Platform.lua b/src/core/Platform.lua index 2d1adc4a..87adb951 100644 --- a/src/core/Platform.lua +++ b/src/core/Platform.lua @@ -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 diff --git a/src/core/VideoMode.lua b/src/core/VideoMode.lua index c4956b24..dc178169 100644 --- a/src/core/VideoMode.lua +++ b/src/core/VideoMode.lua @@ -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 diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 934f8abe..2925a3f3 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -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 diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 048c6f0e..21b4e410 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -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 diff --git a/tests/engine/cart_import_picker.lua b/tests/engine/cart_import_picker.lua index 3968c566..6182d1a4 100644 --- a/tests/engine/cart_import_picker.lua +++ b/tests/engine/cart_import_picker.lua @@ -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 diff --git a/tests/engine/platform_ps4_test.lua b/tests/engine/platform_ps4_test.lua new file mode 100644 index 00000000..0c8dd67f --- /dev/null +++ b/tests/engine/platform_ps4_test.lua @@ -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()