mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 17:51:37 -04:00
Merge pull request #2504 from tomasmorello/ps4-port
This commit is contained in:
@@ -111,8 +111,9 @@ SHA-1 before creating any game data. FireRed, LeafGreen, Ruby, Sapphire, and Eme
|
||||
|
||||
**Platform notes:** [Linux](docs/platforms/linux.md),
|
||||
[iOS](docs/platforms/ios.md), [Xbox Dev Mode](docs/platforms/xbox.md),
|
||||
[handhelds](docs/platforms/handhelds.md), and
|
||||
[Nintendo Switch](docs/platforms/switch.md) each have their own install steps.
|
||||
[handhelds](docs/platforms/handhelds.md),
|
||||
[Nintendo Switch](docs/platforms/switch.md), and
|
||||
[PlayStation 4](docs/platforms/ps4.md) each have their own install steps.
|
||||
|
||||
**Windows Defender:** it sometimes flags the Windows build with a generic
|
||||
detection such as `Trojan:Win32/Wacatac!ml` (#621). This is a known false
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,6 +20,7 @@ editor, and developer setup live on the
|
||||
- [Xbox Dev Mode](platforms/xbox.md)
|
||||
- [Handhelds](platforms/handhelds.md)
|
||||
- [Nintendo Switch](platforms/switch.md)
|
||||
- [PlayStation 4](platforms/ps4.md)
|
||||
|
||||
## Modding
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# PlayStation 4
|
||||
|
||||
Homebrew (GoldHEN) build: `gen1recomp-*-ps4.pkg`, the game fused with
|
||||
[LÖVE for PS4](https://github.com/tomasmorello/love-ps4) (LÖVE 11.5), pinned
|
||||
like love-nx is for the Switch. PS4 port by
|
||||
[Tomas Morello](https://github.com/tomasmorello). Tested on hardware with
|
||||
Red, Blue and Yellow; Gen 2 and Gen 3 are not tested yet.
|
||||
|
||||
- Players: [ps4-install.md](../ps4-install.md). Install the `.pkg` from
|
||||
USB, copy your own legal ROM over FTP into the inbox, Scan again.
|
||||
- Builders: `scripts/build_ps4.sh --fetch --fused` (see the script header).
|
||||
@@ -0,0 +1,73 @@
|
||||
# Install Gen1Recomp on PlayStation 4
|
||||
|
||||
Releases that include PS4 support ship `gen1recomp-*-ps4.pkg`: the game and its
|
||||
runtime in one installable package. Install it, then import your own legal
|
||||
`.gb` / `.gbc` ROM.
|
||||
|
||||
> You need a PS4 that runs homebrew (GoldHEN). This project does not help you
|
||||
> set that up.
|
||||
|
||||
PS4 port by [Tomas Morello](https://github.com/tomasmorello), on
|
||||
[LÖVE for PS4](https://github.com/tomasmorello/love-ps4) (LÖVE 11.5).
|
||||
|
||||
**Tested on real hardware:** Pokemon Red, Blue and Yellow (ROM import, play,
|
||||
save and load, mods, returning to the launcher). **Not tested yet:** Gold,
|
||||
Silver, Crystal, FireRed and LeafGreen. They may work; reports are welcome.
|
||||
|
||||
## 1. Install the package
|
||||
|
||||
1. Open [Releases](https://github.com/bryanthaboi/gen1recomp/releases) and
|
||||
download `gen1recomp-*-ps4.pkg` (optionally check it against its `.sha256`).
|
||||
2. Copy it to a USB drive formatted **exFAT** and plug it into the console.
|
||||
3. **Settings → Debug Settings → Game → Package Installer**, pick the file.
|
||||
4. Launch **Gen1Recomp** from the home screen.
|
||||
|
||||
Updating: install the newer `.pkg` the same way; it installs over the old one.
|
||||
Saves, imported ROMs and mods live in `/data/love/pokemon-love2d/`, outside the
|
||||
package. **Do not delete that folder** when updating.
|
||||
|
||||
## 2. Import your ROM
|
||||
|
||||
The PS4 has no file picker, so the launcher reads an inbox folder, like the
|
||||
Switch build:
|
||||
|
||||
1. Enable GoldHEN's FTP server (GoldHEN menu), port **2121**.
|
||||
2. With any FTP client, copy your `.gb` / `.gbc` into
|
||||
`/data/love/pokemon-love2d/imports/`.
|
||||
3. In the launcher, press **Scan again**.
|
||||
|
||||
The other inboxes work the same way (copy over FTP, then **Scan again** in
|
||||
that screen), all under `/data/love/pokemon-love2d/`:
|
||||
|
||||
| What | Folder |
|
||||
|---|---|
|
||||
| Mods (`.zip`) | `imports/mods/` |
|
||||
| Saves (`.sav`) | `imports/saves/<game>/`, e.g. `imports/saves/red/` |
|
||||
| Custom Carts (`.g1rcart`) | `imports/carts/` |
|
||||
|
||||
## 3. Controls
|
||||
|
||||
The launcher uses **D-pad focus navigation**: the highlighted option is the
|
||||
selected one; Cross confirms, Circle goes back. **Triangle** switches to the
|
||||
virtual cursor and back. In game, the pad maps like on every other platform.
|
||||
|
||||
To quit, use the **PS button** and close the application, like any PS4 app.
|
||||
|
||||
⚠ **START + SHARE** belongs to GoldHEN (it opens its menu), so the "hold
|
||||
START + SELECT" force-quit shortcut does not reach the game on PS4.
|
||||
|
||||
## 4. Already have LÖVE for PS4?
|
||||
|
||||
`scripts/build_ps4.sh --loose` produces a plain `game.love`: copy it to
|
||||
`/data/love/game.love` and start **LÖVE for PS4** instead of installing the
|
||||
standalone package.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
scripts/build_ps4.sh --fetch --fused --version X.Y.Z
|
||||
```
|
||||
|
||||
Needs the OpenOrbis PS4 Toolchain v0.5.4 packaging tools (`OO_PS4_TOOLCHAIN`)
|
||||
and bash 4+. The runtime is downloaded from a pinned LÖVE for PS4 release and
|
||||
verified against `scripts/ps4/love-ps4-runtime.sha256`.
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
# PlayStation 4 packaging entry point (homebrew, GoldHEN).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/build_ps4.sh --fetch
|
||||
# scripts/build_ps4.sh --loose
|
||||
# scripts/build_ps4.sh --fused [--version X.Y.Z]
|
||||
# scripts/build_ps4.sh --fetch --fused [--version X.Y.Z]
|
||||
#
|
||||
# Modes:
|
||||
# --fetch Download the pinned LÖVE for PS4 runtime zip into
|
||||
# .bazinga/love-ps4/<tag>/ and verify SHA-256 against
|
||||
# scripts/ps4/love-ps4-runtime.sha256.
|
||||
#
|
||||
# --loose Pack game.love into dist/ps4/loose/ for players who already have
|
||||
# the generic "LÖVE for PS4" package installed: they copy it to
|
||||
# /data/love/game.love over FTP. Does not need the runtime.
|
||||
#
|
||||
# --fused Build gen1recomp-<ver>-ps4.pkg: runtime + game.love in one
|
||||
# installable package (title id GENR00001; its own tile and saves).
|
||||
# Requires the pin (run --fetch or combine) and the OpenOrbis
|
||||
# packaging tools: OO_PS4_TOOLCHAIN pointing at an unpacked
|
||||
# OpenOrbis PS4 Toolchain v0.5.4 (bin/linux or bin/macos with
|
||||
# PkgTool.Core and create-gp4), plus bash 4+.
|
||||
#
|
||||
# The runtime is LÖVE 11.5 for PS4 by Tomas Morello
|
||||
# (https://github.com/tomasmorello/love-ps4), pinned the way love-nx is pinned
|
||||
# for the Switch.
|
||||
#
|
||||
# Non-goals: installing on a console, FTP uploads, ROM handling.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="$ROOT/.bazinga/work"
|
||||
DIST="$ROOT/dist/ps4"
|
||||
MANIFEST="$ROOT/scripts/ps4/love-ps4-runtime.sha256"
|
||||
TITLE_ID="${GEN1_PS4_TITLE_ID:-GENR00001}"
|
||||
FETCH=0; LOOSE=0; FUSED=0
|
||||
VERSION="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo dev)"
|
||||
|
||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*" >&2; }
|
||||
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--fetch) FETCH=1; shift ;;
|
||||
--loose) LOOSE=1; shift ;;
|
||||
--fused) FUSED=1; shift ;;
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ $((FETCH + LOOSE + FUSED)) -gt 0 ] || fail "specify --fetch, --loose and/or --fused (see --help)"
|
||||
[ $((LOOSE + FUSED)) -le 1 ] || fail "--loose and --fused cannot be combined"
|
||||
|
||||
manifest_field() { # manifest_field <key>
|
||||
local v
|
||||
v="$(awk -v k="$1" '$1 == k { print $2; exit }' "$MANIFEST")"
|
||||
[ -n "$v" ] || fail "$MANIFEST has no '$1' entry"
|
||||
printf '%s' "$v"
|
||||
}
|
||||
TAG="$(manifest_field tag)"
|
||||
ZIP_NAME="love-ps4-${TAG#v}-runtime.zip"
|
||||
RUNTIME_DIR="$ROOT/.bazinga/love-ps4/$TAG"
|
||||
ZIP="$RUNTIME_DIR/$ZIP_NAME"
|
||||
BASE_URL="${GEN1_LOVE_PS4_BASE_URL:-https://github.com/tomasmorello/love-ps4/releases/download/$TAG}"
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
|
||||
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
||||
}
|
||||
|
||||
fetch_runtime() {
|
||||
local want have
|
||||
want="$(manifest_field "$ZIP_NAME")"
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
if [ -f "$ZIP" ] && [ "$(sha256_of "$ZIP")" = "$want" ]; then
|
||||
say "runtime $TAG already fetched"
|
||||
else
|
||||
say "downloading $BASE_URL/$ZIP_NAME"
|
||||
curl -fL --retry 3 --retry-delay 1 -o "$ZIP.part" "$BASE_URL/$ZIP_NAME" \
|
||||
|| { rm -f "$ZIP.part"; fail "download failed: $BASE_URL/$ZIP_NAME"; }
|
||||
have="$(sha256_of "$ZIP.part")"
|
||||
[ "$have" = "$want" ] || { rm -f "$ZIP.part"; fail "SHA-256 mismatch for $ZIP_NAME: got $have, pinned $want"; }
|
||||
mv "$ZIP.part" "$ZIP"
|
||||
fi
|
||||
rm -rf "$RUNTIME_DIR/unpacked"
|
||||
mkdir -p "$RUNTIME_DIR/unpacked"
|
||||
(cd "$RUNTIME_DIR/unpacked" && unzip -q "$ZIP")
|
||||
}
|
||||
|
||||
pack_game_love() {
|
||||
mkdir -p "$WORK"
|
||||
local love_out="$WORK/game.love"
|
||||
# pack_love.sh only accepts X.Y.Z; dev builds (a commit hash) stay unstamped.
|
||||
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
"$ROOT/scripts/pack_love.sh" --output "$love_out" --listing "$WORK/love-listing.txt" \
|
||||
--version "$VERSION" >/dev/null
|
||||
else
|
||||
"$ROOT/scripts/pack_love.sh" --output "$love_out" --listing "$WORK/love-listing.txt" >/dev/null
|
||||
fi
|
||||
printf '%s' "$love_out"
|
||||
}
|
||||
|
||||
# X.Y.Z -> PS4 APP_VER "NN.NN" as (10*X+Y).Z: 0.3.1 -> 03.01, 1.0.0 -> 10.00.
|
||||
# Monotonic while Y < 10 and Z < 100, so a newer release installs over an older one.
|
||||
app_ver() {
|
||||
if printf '%s' "$1" | grep -Eq '^[0-9]+\.[0-9]\.[0-9]{1,2}$'; then
|
||||
local x y z
|
||||
IFS=. read -r x y z <<EOF
|
||||
$1
|
||||
EOF
|
||||
printf '%02d.%02d' "$((10 * x + y))" "$z"
|
||||
else
|
||||
printf '00.00' # dev builds: always older than any release
|
||||
fi
|
||||
}
|
||||
|
||||
[ "$FETCH" -eq 1 ] && fetch_runtime
|
||||
|
||||
if [ "$LOOSE" -eq 1 ]; then
|
||||
mkdir -p "$DIST/loose"
|
||||
cp "$(pack_game_love)" "$DIST/loose/game.love"
|
||||
say "dist/ps4/loose/game.love -> copy to /data/love/game.love on the console"
|
||||
fi
|
||||
|
||||
if [ "$FUSED" -eq 1 ]; then
|
||||
[ -f "$ZIP" ] || fail "runtime not fetched: run with --fetch"
|
||||
[ -d "$RUNTIME_DIR/unpacked" ] || fetch_runtime
|
||||
[ -n "${OO_PS4_TOOLCHAIN:-}" ] || fail "OO_PS4_TOOLCHAIN is not set (OpenOrbis PS4 Toolchain v0.5.4)"
|
||||
fuse="$(ls "$RUNTIME_DIR"/unpacked/*/fuse-pkg.sh)"
|
||||
love="$(pack_game_love)"
|
||||
icon="$ROOT/scripts/ps4/icon0.png"
|
||||
[ -f "$icon" ] || icon=""
|
||||
mkdir -p "$DIST"
|
||||
"$fuse" --love "$love" --title-id "$TITLE_ID" --title "Gen1Recomp" \
|
||||
--version "$(app_ver "$VERSION")" --content-label GEN1RECOMP --out-dir "$WORK/ps4-pkg" \
|
||||
${icon:+--icon "$icon"}
|
||||
cp "$WORK"/ps4-pkg/IV0000-"$TITLE_ID"_00-*.pkg "$DIST/gen1recomp-$VERSION-ps4.pkg"
|
||||
(cd "$DIST" && sha256_of "gen1recomp-$VERSION-ps4.pkg" > "gen1recomp-$VERSION-ps4.pkg.sha256")
|
||||
say "dist/ps4/gen1recomp-$VERSION-ps4.pkg"
|
||||
fi
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
@@ -0,0 +1,8 @@
|
||||
# LÖVE for PS4 — pinned runtime (Tomas Morello)
|
||||
# https://github.com/tomasmorello/love-ps4/releases
|
||||
#
|
||||
# scripts/build_ps4.sh --fetch downloads <zip> from the release <tag> and refuses
|
||||
# it unless its SHA-256 matches. Bump both lines together.
|
||||
|
||||
tag v0.9.3
|
||||
love-ps4-0.9.3-runtime.zip 8cc66a447738a6772fc79775d7306791fb63542582e420cf08db8cb0420eded7
|
||||
@@ -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
@@ -43,6 +43,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
|
||||
@@ -1721,9 +1735,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)
|
||||
@@ -1768,7 +1782,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
|
||||
@@ -1906,14 +1920,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)))
|
||||
@@ -2071,7 +2089,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
|
||||
@@ -3804,12 +3822,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
|
||||
@@ -5625,7 +5649,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
|
||||
|
||||
+68
-29
@@ -613,7 +613,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
|
||||
|
||||
@@ -621,7 +623,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
|
||||
@@ -632,7 +636,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
|
||||
@@ -654,7 +660,10 @@ function RomImporter:_setNxSavesInboxNotice(version)
|
||||
self.saveNotice[version] = {
|
||||
ok = true,
|
||||
persistent = 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
|
||||
@@ -1041,7 +1050,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)
|
||||
@@ -1567,6 +1576,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,
|
||||
@@ -1740,7 +1751,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
|
||||
@@ -2292,7 +2303,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
|
||||
@@ -2721,7 +2732,7 @@ function RomImporter:chooseBoxImport()
|
||||
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||
if self.isNX or self.ios or isHandheld then
|
||||
if self:_inboxImport() or self.ios or isHandheld then
|
||||
require("src.ui.kit.Kit").FileBrowser.open({ title = "Select Pokémon Box save (.gci / .sav)",
|
||||
mode = "box", onSelect = function(path) self:_importBoxFile(path) end })
|
||||
return
|
||||
@@ -2793,7 +2804,7 @@ function RomImporter:chooseMod()
|
||||
if self:_importerPickPending() then return end
|
||||
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
|
||||
@@ -3188,7 +3199,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)
|
||||
@@ -3215,7 +3226,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
|
||||
@@ -3330,7 +3343,7 @@ function RomImporter:chooseSaveImport(version)
|
||||
if self:_importerPickPending() then return end
|
||||
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
|
||||
@@ -3426,7 +3439,7 @@ 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
|
||||
@@ -3434,7 +3447,9 @@ function RomImporter:exportSave(version, format, scope, slotId)
|
||||
self.saveNotice[noticeScope] = {
|
||||
ok = true,
|
||||
persistent = 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
|
||||
@@ -3508,7 +3523,7 @@ function RomImporter:choose(version)
|
||||
if self:_importerPickPending() then return end
|
||||
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
|
||||
@@ -3987,7 +4002,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()
|
||||
@@ -4201,7 +4224,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
|
||||
@@ -4436,6 +4459,9 @@ function RomImporter:gamepadpressed(_, button)
|
||||
elseif self._profileRenamePrompt then self._profileRenamePrompt = nil; self:_disarmTextInput(); return
|
||||
elseif self._profileSavePrompt then self._profileSavePrompt = nil; self:_disarmTextInput(); return
|
||||
elseif self._settingsText then self._settingsText = nil; self:_disarmTextInput(); return
|
||||
-- B leaves Settings the way Escape does (keypressed): a pad-only
|
||||
-- console had no way out but the pointer.
|
||||
elseif self._settings then self:_closeSettings(); return
|
||||
end
|
||||
elseif button == "dpup" or action == "dpup" then
|
||||
if okKit then Kit.navigate("up") end
|
||||
@@ -4469,6 +4495,9 @@ function RomImporter:gamepadpressed(_, button)
|
||||
elseif self._profileRenamePrompt then self._profileRenamePrompt = nil; self:_disarmTextInput(); return
|
||||
elseif self._profileSavePrompt then self._profileSavePrompt = nil; self:_disarmTextInput(); return
|
||||
elseif self._settingsText then self._settingsText = nil; self:_disarmTextInput(); return
|
||||
-- B leaves Settings the way Escape does (keypressed): a pad-only
|
||||
-- console had no way out but the pointer.
|
||||
elseif self._settings then self:_closeSettings(); return
|
||||
end
|
||||
elseif button == "dpup" or button == "dpdown"
|
||||
or button == "dpleft" or button == "dpright" then
|
||||
@@ -5530,17 +5559,19 @@ 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:_importerPickPending() then return end
|
||||
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
|
||||
@@ -5832,12 +5863,12 @@ 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).
|
||||
@@ -5845,7 +5876,7 @@ function RomImporter:importCartFile(version)
|
||||
if self:_importerPickPending() then return end
|
||||
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
|
||||
@@ -6718,11 +6749,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
|
||||
@@ -8474,15 +8506,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
|
||||
@@ -8491,13 +8526,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
|
||||
@@ -8508,7 +8547,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
|
||||
|
||||
+31
-2
@@ -353,8 +353,12 @@ local function getNavLayer(slot)
|
||||
local id = tostring(slot.id or "")
|
||||
local y = slot.y or 0
|
||||
|
||||
-- Layer 1: Top Bar (Settings / Gear, Close / Quit)
|
||||
if id == "gear" or id == "settings" or id == "close" or id == "quit" or (y < 45 * Kit.scale and not id:match("^tab%-")) then
|
||||
-- Layer 1: Top Bar (Settings / Gear, Close / Quit, Save Sync). tab-sync
|
||||
-- rides the gear's cluster; by its tab- prefix alone it fell into layer 2,
|
||||
-- so Right from it found nothing and Up jumped sideways to the gear.
|
||||
if id == "gear" or id == "settings" or id == "close" or id == "quit"
|
||||
or id == "tab-sync"
|
||||
or (y < 45 * Kit.scale and not id:match("^tab%-")) then
|
||||
return 1
|
||||
end
|
||||
|
||||
@@ -520,6 +524,31 @@ function Kit._resolveNav()
|
||||
end
|
||||
return
|
||||
elseif dir == "up" or dir == "down" then
|
||||
-- Within the layer first: the nearest control above/below in the same
|
||||
-- layer wins, so Down from the cart reaches the Scan / Import button under
|
||||
-- it instead of jumping over it to the footer. The horizontal gap weighs
|
||||
-- double so a control straight below beats one off to the side.
|
||||
do
|
||||
local best, bestScore
|
||||
for i = 1, n do
|
||||
local c = Kit._nav[i]
|
||||
if c.id ~= cur.id and getNavLayer(c) == curLayer then
|
||||
local dy = (c.y + c.h / 2) - cy
|
||||
local forward = dir == "down" and dy or -dy
|
||||
if forward > 1 then
|
||||
local gap = math.max(0, c.x - (cur.x + cur.w), cur.x - (c.x + c.w))
|
||||
local score = forward + gap * 2
|
||||
if not bestScore or score < bestScore then
|
||||
best, bestScore = c, score
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if best then
|
||||
Kit.focusId = best.id
|
||||
return
|
||||
end
|
||||
end
|
||||
-- VERTICAL LAYER NAVIGATION (Up/Down steps between layers: 1 <-> 2 <-> 3 <-> 4)
|
||||
local targetLayer = dir == "up" and (curLayer - 1) or (curLayer + 1)
|
||||
targetLayer = math.max(1, math.min(4, targetLayer))
|
||||
|
||||
@@ -146,6 +146,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,49 @@
|
||||
-- Focus-ring navigation between and within layers (found driving the
|
||||
-- launcher with a pad only, on PS4; the same ring runs on Xbox and on
|
||||
-- desktop arrow keys).
|
||||
-- Self-contained: luajit tests/engine/launcher_pad_nav_layers_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness")
|
||||
local eq = T.eq
|
||||
|
||||
local Kit = require("src.ui.kit.Kit")
|
||||
|
||||
-- One frame of focusables: the header cluster, a cart with its action
|
||||
-- button straight below, and a footer card.
|
||||
local function frame()
|
||||
Kit.beginFrame(0, 0, false, 0)
|
||||
Kit.focusable("tab-sync", 520, 10, 30, 30)
|
||||
Kit.focusable("gear", 560, 10, 30, 30)
|
||||
Kit.focusable("tab-red", 20, 60, 40, 24)
|
||||
Kit.focusable("play-red", 20, 120, 120, 150)
|
||||
Kit.focusable("rom-red", 20, 290, 120, 30)
|
||||
Kit.focusable("footer-promo", 20, 400, 200, 40)
|
||||
Kit.endFrame()
|
||||
end
|
||||
|
||||
local function step(from, dir)
|
||||
Kit.scale = 1
|
||||
Kit.blockClicks = false
|
||||
Kit.focusId = from
|
||||
frame()
|
||||
Kit.focusId = from
|
||||
Kit.navigate(dir)
|
||||
frame()
|
||||
return Kit.focusId
|
||||
end
|
||||
|
||||
-- Save Sync sits in the top bar beside the gear, not in the tab row.
|
||||
eq(step("tab-sync", "right"), "gear", "Right from Save Sync reaches the gear")
|
||||
eq(step("gear", "left"), "tab-sync", "Left from the gear reaches Save Sync")
|
||||
|
||||
-- Down inside the content layer before leaving it.
|
||||
eq(step("play-red", "down"), "rom-red",
|
||||
"Down from the cart reaches the button under it, not the footer")
|
||||
eq(step("rom-red", "up"), "play-red", "Up from that button returns to the cart")
|
||||
eq(step("rom-red", "down"), "footer-promo",
|
||||
"with nothing further down in the layer, Down still reaches the footer")
|
||||
|
||||
T.finish()
|
||||
@@ -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