mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 05:40:07 -05:00
Compare commits
59
Commits
dev
..
1.7alpha11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7671106a32 | ||
|
|
b1a0dce78d | ||
|
|
01ea403b88 | ||
|
|
06576fabe3 | ||
|
|
01f5cf7182 | ||
|
|
640d8bdd59 | ||
|
|
bc34a3bb26 | ||
|
|
3638cdaae5 | ||
|
|
3190100de8 | ||
|
|
757f4514cc | ||
|
|
b3368dca15 | ||
|
|
a594117aaf | ||
|
|
47a8196a8c | ||
|
|
9345a844b9 | ||
|
|
24a421b350 | ||
|
|
599d5d04f3 | ||
|
|
f5c7870583 | ||
|
|
718f02017b | ||
|
|
3f49f60cec | ||
|
|
5ff85005bf | ||
|
|
b00536960d | ||
|
|
a4a35af5f0 | ||
|
|
3bff288a86 | ||
|
|
9485f22ddc | ||
|
|
75b310ee25 | ||
|
|
496ed48e33 | ||
|
|
4f48237d93 | ||
|
|
8d888c5a25 | ||
|
|
3c484e68ab | ||
|
|
7da387fc12 | ||
|
|
a393737423 | ||
|
|
6f64551b42 | ||
|
|
6e58123275 | ||
|
|
3aa62a3728 | ||
|
|
38bb6790db | ||
|
|
be92afa71d | ||
|
|
fa8423d427 | ||
|
|
3d8d248336 | ||
|
|
22bacfe168 | ||
|
|
4085fcd8ca | ||
|
|
eb3c611ebf | ||
|
|
9f0242044d | ||
|
|
c4e9839fe5 | ||
|
|
73b26ba3fb | ||
|
|
5691f25ece | ||
|
|
9cda9717ea | ||
|
|
e24a444147 | ||
|
|
03ad6d2e45 | ||
|
|
6d59555b66 | ||
|
|
7300dc1830 | ||
|
|
ad5afc5689 | ||
|
|
6d8622c829 | ||
|
|
9f85a3b421 | ||
|
|
0fb241cba5 | ||
|
|
09589b7ae2 | ||
|
|
8566c0294c | ||
|
|
56dd338abd | ||
|
|
090723879c | ||
|
|
16873e01f2 |
No files matched your search
@@ -10,3 +10,6 @@ mkexfat_macos.sh
|
||||
mkufs2.sh
|
||||
New-OsfExfatImage.ps1
|
||||
README.md
|
||||
docs/socket-api.md
|
||||
docs/openapi.yaml
|
||||
tools/api_test.py
|
||||
@@ -47,6 +47,7 @@ jobs:
|
||||
automake \
|
||||
build-essential \
|
||||
clang-18 \
|
||||
cmake \
|
||||
curl \
|
||||
git \
|
||||
libarchive-tools \
|
||||
@@ -56,6 +57,8 @@ jobs:
|
||||
meson \
|
||||
pacman-package-manager \
|
||||
pkg-config \
|
||||
python3 \
|
||||
python3-yaml \
|
||||
xxd \
|
||||
zip
|
||||
|
||||
@@ -65,12 +68,29 @@ jobs:
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 openlibm package
|
||||
working-directory: pacbrew-repo/openlibm
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 json-c package
|
||||
working-directory: pacbrew-repo/json-c
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 sqlite package
|
||||
working-directory: pacbrew-repo/sqlite
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Check API tools and manifest
|
||||
run: |
|
||||
python3 -m py_compile tools/api_test.py
|
||||
python3 -c 'import pathlib, yaml; doc = yaml.safe_load(pathlib.Path("docs/openapi.yaml").read_text()); assert doc["openapi"] == "3.1.2" and len(doc["paths"]) == 5'
|
||||
|
||||
- name: Build payload
|
||||
run: |
|
||||
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
*.o
|
||||
*.elf
|
||||
*.so
|
||||
*.pyc
|
||||
__pycache__/
|
||||
daemon.lock
|
||||
debug.log
|
||||
notify.txt
|
||||
|
||||
@@ -2,28 +2,37 @@ PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
|
||||
VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo unknown)
|
||||
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Standard Flags (No extra libraries)
|
||||
# Compiler and dependency flags
|
||||
JSON_C_ROOT := $(PS5_PAYLOAD_SDK)/target/user/homebrew
|
||||
JSON_CFLAGS := -I$(JSON_C_ROOT)/include
|
||||
CFLAGS := -O3 -flto=thin -DNDEBUG -ffunction-sections -fdata-sections -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes -Werror=strict-prototypes -Werror=missing-prototypes -D_BSD_SOURCE -std=gnu11 -Iinclude -Isrc
|
||||
CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
|
||||
|
||||
# Linker
|
||||
LDFLAGS := -flto=thin -Wl,--gc-sections
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
# Libraries
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lSceNet -lSceSsl -lSceHttp2 -lsqlite3 $(JSON_C_ROOT)/lib/libjson-c.a -lm
|
||||
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
|
||||
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
|
||||
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
ASM_SRCS := src/sm_shellcore_bridge.S
|
||||
OBJS := $(SRCS:.c=.o) $(ASM_SRCS:.S=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
L10N_CATALOGS := $(wildcard include/lang/*.inc)
|
||||
|
||||
# Targets
|
||||
all: shadowmountplus.elf
|
||||
|
||||
usb-info.elf: tools/usb_info.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< -lSceNotification
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
|
||||
@@ -40,8 +49,21 @@ src/notify_icon_asset.c: smp_icon.png
|
||||
src/config_ini_example_asset.c: config.ini.example
|
||||
xxd -i $< > $@
|
||||
|
||||
src/sm_api_service.o: CFLAGS += $(JSON_CFLAGS)
|
||||
src/main.o src/sm_image_index.o: CFLAGS += -DSHADOWMOUNT_BUILD_TIME=\"$(BUILD_TIME)\"
|
||||
src/main.o src/sm_image_index.o: FORCE
|
||||
|
||||
src/sm_l10n.o: $(L10N_CATALOGS)
|
||||
src/sm_ampr_updater.o: src/sm_ampr_ca.inc
|
||||
|
||||
.PHONY: FORCE
|
||||
FORCE:
|
||||
|
||||
src/%.o: src/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
src/%.o: src/%.S
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
rm -f shadowmountplus.elf usb-info.elf api-test.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
+5
-21
@@ -46,9 +46,9 @@
|
||||
-----
|
||||
- This script does NOT auto-elevate. Start PowerShell as Administrator.
|
||||
- Filesystem is always exFAT.
|
||||
- Cluster size is auto-selected:
|
||||
- large-file sets: 65536
|
||||
- small/mixed-file sets: 32768
|
||||
- Cluster size is fixed at 65536 bytes for the LVD type-5/BFS sdimg path.
|
||||
- Keep ShadowMount's LVD exFAT logical sector at 512 bytes; 65536 is the
|
||||
filesystem allocation unit and LVD secondary unit, not sector_size.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
@@ -197,22 +197,6 @@ function Get-LogicalDriveFileSystem([string]$driveLetter) {
|
||||
return ""
|
||||
}
|
||||
|
||||
function Get-OptimalExfatClusterSize([string]$dir) {
|
||||
[Int64]$largeFileThreshold = 1MB
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
|
||||
if ($files.Count -eq 0) { return 32768 }
|
||||
|
||||
[Int64]$rawFileBytes = 0
|
||||
foreach ($f in $files) {
|
||||
$rawFileBytes += [Int64]$f.Length
|
||||
}
|
||||
|
||||
[Int64]$avgFileBytes = [Int64]($rawFileBytes / [Int64]$files.Count)
|
||||
if ($avgFileBytes -ge $largeFileThreshold) { return 65536 }
|
||||
return 32768
|
||||
}
|
||||
|
||||
function Format-AllocationUnitArg([int]$clusterSize) {
|
||||
if ($clusterSize -ge 1MB -and ($clusterSize % 1MB) -eq 0) {
|
||||
return "{0}M" -f ($clusterSize / 1MB)
|
||||
@@ -308,7 +292,7 @@ if (Test-Path $ImagePath) {
|
||||
|
||||
[Int64]$expectedBytes = 0
|
||||
[string]$osfSizeArg = $null
|
||||
[int]$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
|
||||
[int]$ExfatClusterSize = 65536
|
||||
[bool]$sizeProvided = -not [string]::IsNullOrWhiteSpace($Size)
|
||||
[int]$TargetClusterSize = $ExfatClusterSize
|
||||
|
||||
@@ -354,7 +338,7 @@ try {
|
||||
if ($CreateEmptyAndMount) {
|
||||
Write-Host "[2/2] Done. Empty image is mounted at $dest."
|
||||
Write-Host "Manual steps:"
|
||||
Write-Host " 1) Format $dest as exFAT (recommended cluster: 64KB for large-file sets, 32KB for small/mixed sets)."
|
||||
Write-Host " 1) Format $dest as exFAT with a 64KB allocation unit (required for the LVD/BFS fast-path profile)."
|
||||
Write-Host " 2) Copy contents of '$SourceDir' to $dest."
|
||||
Write-Host " 3) Dismount: `"$osf`" -d -m $MountPoint"
|
||||
return
|
||||
|
||||
@@ -54,27 +54,42 @@ This file is optional. If it does not exist, ShadowMountPlus creates it from the
|
||||
Supported keys (all optional):
|
||||
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
|
||||
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
|
||||
- `language=auto|<locale>` (notification language; `auto` uses the console system language, e.g. `en-US`, `ru-RU`; default: `auto`)
|
||||
- `api_bind_address=<IPv4>` (HTTP/JSON API bind address; default: `127.0.0.1`; use `0.0.0.0` for network access)
|
||||
- `api_port=<1..65535>` (HTTP/JSON API port; default: `10101`)
|
||||
- `mount_read_only=1|0` (default: `1`)
|
||||
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through `sceAppInstUtilAppInstallAll`; default: `0` on FW below `12.00`, forced `1` on FW `12.00+`)
|
||||
- `persistent_image_mounts=1|0` (`1` keeps discovered images mounted between game launches; default: `0`)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through the stock batch `sceAppInstUtilAppInstallAll`; default: `0` on every firmware. On FW `12.00+`, the default per-title path is provided by the SceShellCore TitleDir bridge.)
|
||||
- `auto_remove_missing_games=1|0` (`1` automatically removes games from the system library when their source is no longer available; default: `0`)
|
||||
- `auto_remove_games_with_dlc=1|0` (`1` allows automatic removal of games that have installed DLC; default: `0`)
|
||||
- `auto_remove_missing_delay_seconds=<1..86400>` (how long a source must remain unavailable before removal; default: `300`)
|
||||
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
|
||||
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
|
||||
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
|
||||
- `image_sector=<image_filename>:<sector_size>` (repeatable; force the logical device sector size, not the filesystem cluster size)
|
||||
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
|
||||
- `watch_subdirs=1|0` (`1` keeps vnode watchers on scan root subdirectories; default: `0`)
|
||||
- `watch_image_files=1|0` (`1` keeps vnode watchers on discovered image files; default: `1`)
|
||||
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
|
||||
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
|
||||
- `exfat_backend=lvd|md` (default: `lvd`)
|
||||
- `ufs_backend=lvd|md` (default: `lvd`)
|
||||
- `legacy_mount_ufs=1|0` (`.ffpkg`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `legacy_mount_exfat=1|0` (`.exfat`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `legacy_mount_pfs=1|0` (`.ffpfs` and nested `pfs_image.dat`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `legacy_mount_pfsc=1|0` (outer `.ffpfsc`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `nested_pfs_index_cache=1|0` (cache the containing PFS compressed-file index before attaching a nested image in legacy mode; default: `0`; optimized mode always requests it; the old `legacy_gddr5_cache` name remains accepted)
|
||||
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
|
||||
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; default: `1`)
|
||||
- `update_emulators=1|0` (`1` updates all emulators with matching files in a game's own fakelib; `fakelib2` is excluded; default: `1`)
|
||||
- `emulators_path=<absolute_path>` (folder containing emulator update files; default: `/data/shadowmount/emus`)
|
||||
- `auto_update_ampr=1|0` (check for a new `libSceAmpr.sprx` 30 seconds after startup and every four hours; default: `0`)
|
||||
- `ampr_update_url=<http_or_https_url>` (AMPR emulator download URL; default: `https://github.com/drakmor/ampr_emu/releases/latest/download/libSceAmpr.sprx`)
|
||||
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; `fakelib2` remains exclusive; default: `1`)
|
||||
- `global_fakelib_path=<absolute_path>` (global fakelib folder; default: `/data/shadowmount/fakelib`)
|
||||
- `global_fakelib_priority=game|global` (overlay priority when both global and game fakelib exist; default: `game`)
|
||||
- `global_fakelib_exclude=<TITLE_ID>` (repeatable; disables the global fakelib overlay for matching titles)
|
||||
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; default: `1`)
|
||||
- `kstuff_crash_detection=1|0` (`1` enables crash monitoring and pause-delay autotune updates; default: `1`)
|
||||
- `fan_target_temperature=system|<50..91>` (automatic fan-controller target in degrees Celsius, applied when a game starts; `system` leaves fan control to the console and is the default)
|
||||
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `25`)
|
||||
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `15`)
|
||||
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
|
||||
@@ -86,9 +101,48 @@ Supported keys (all optional):
|
||||
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
|
||||
- `lvd_exfat_sector_size=<value>` (default: `512`)
|
||||
- `lvd_ufs_sector_size=<value>` (default: `4096`)
|
||||
- `lvd_pfs_sector_size=<value>` (default: `32768`)
|
||||
- `lvd_pfs_sector_size=<value>` (default: `4096`; the optimized profile uses a `65536`-byte LVD mapping unit)
|
||||
- `md_exfat_sector_size=<value>` (default: `512`)
|
||||
- `md_ufs_sector_size=<value>` (default: `512`)
|
||||
- `md_ufs_sector_size=<value>` (default: `512` with the legacy UFS profile, `4096` with the optimized profile)
|
||||
|
||||
Supported notification languages:
|
||||
|
||||
| Language | Locale |
|
||||
| --- | --- |
|
||||
| Arabic (Saudi Arabia) | `ar-SA` |
|
||||
| Chinese (Simplified) | `zh-CN` |
|
||||
| Chinese (Traditional) | `zh-TW` |
|
||||
| Czech | `cs-CZ` |
|
||||
| Danish | `da-DK` |
|
||||
| Dutch | `nl-NL` |
|
||||
| English (United Kingdom) | `en-GB` |
|
||||
| English (United States) | `en-US` |
|
||||
| Finnish | `fi-FI` |
|
||||
| French (Canada) | `fr-CA` |
|
||||
| French (France) | `fr-FR` |
|
||||
| German | `de-DE` |
|
||||
| Greek | `el-GR` |
|
||||
| Hungarian | `hu-HU` |
|
||||
| Indonesian | `id-ID` |
|
||||
| Italian | `it-IT` |
|
||||
| Japanese | `ja-JP` |
|
||||
| Korean | `ko-KR` |
|
||||
| Norwegian | `no-NO` |
|
||||
| Polish | `pl-PL` |
|
||||
| Portuguese (Brazil) | `pt-BR` |
|
||||
| Portuguese (Portugal) | `pt-PT` |
|
||||
| Romanian | `ro-RO` |
|
||||
| Russian | `ru-RU` |
|
||||
| Spanish (Mexico) | `es-MX` |
|
||||
| Spanish (Spain) | `es-ES` |
|
||||
| Swedish | `sv-SE` |
|
||||
| Thai | `th-TH` |
|
||||
| Turkish | `tr-TR` |
|
||||
| Ukrainian | `uk-UA` |
|
||||
| Vietnamese | `vi-VN` |
|
||||
|
||||
The public API routes and JSON schemas are documented in [docs/socket-api.md](docs/socket-api.md)
|
||||
and available as an [OpenAPI manifest](docs/openapi.yaml).
|
||||
|
||||
Per-image mode override behavior:
|
||||
- Match is done by image file name (without path).
|
||||
@@ -101,14 +155,21 @@ mount_read_only=1
|
||||
image_rw=PPSA1234-my-image.ffpfs
|
||||
image_rw=MYGame 123.exfat
|
||||
image_ro=legacy_dump.ffpkg
|
||||
image_sector=MYGame 123.exfat:65536
|
||||
image_sector=legacy-512-sector.exfat:512
|
||||
```
|
||||
|
||||
Persistent image mount behavior:
|
||||
- With `persistent_image_mounts=0` (default), backing images are mounted when a game starts and released after it exits.
|
||||
- With `persistent_image_mounts=1`, images discovered during scans remain mounted between launches. Per-title runtime and backport layers are still mounted only when needed.
|
||||
- USB-backed images are always released during suspend and mounted again by the resume scan.
|
||||
- Persistent mode consumes one backing device per mounted image layer and is intended for libraries that fit the available LVD/MD device capacity.
|
||||
|
||||
Per-image sector override behavior:
|
||||
- Match is done by image file name (without path).
|
||||
- `image_sector` in `/data/shadowmount/autotune.ini` has the highest priority for matching image files.
|
||||
- If no per-image rule matches, the backend-specific global sector size defaults are used.
|
||||
- When image validation fails because the mounted file-system cluster size is smaller than the selected device sector size, ShadowMountPlus writes `image_sector=<image_filename>:<cluster_size>` into `/data/shadowmount/autotune.ini` and asks you to try mounting again.
|
||||
- For the exFAT LVD/BFS fast path, leave the logical sector at its default `512`. The required `65536` values are the exFAT allocation unit and the internal LVD `secondary_unit`; do not set `image_sector=...:65536` to request this mode.
|
||||
|
||||
Scan path behavior:
|
||||
- If at least one `scanpath=...` is present, only those custom paths are used.
|
||||
@@ -118,6 +179,7 @@ Scan path behavior:
|
||||
- If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`.
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `15`).
|
||||
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
|
||||
- With `auto_remove_missing_games=1`, a game is removed from the system library only after its source remains unavailable for `auto_remove_missing_delay_seconds`. If the source becomes available again before removal, the pending removal is cancelled.
|
||||
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
|
||||
|
||||
Backport overlay behavior:
|
||||
@@ -126,12 +188,13 @@ Backport overlay behavior:
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
|
||||
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
|
||||
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
|
||||
- `global_fakelib_priority=global` reverses that priority.
|
||||
- ShadowMount+ checks the selected backport for `fakelib2` and then `fakelib`; if neither exists, it uses only `fakelib` from the original game source. The selected directory is mounted into the running game's sandbox `common/lib`. A backport `fakelib2` is always mounted directly and exclusively: neither emulator updates nor the global fakelib can replace or supplement it.
|
||||
- If `update_emulators=1`, matching files from `emulators_path` replace files in the selected game fakelib, except when `fakelib2` is selected. The cache is refreshed when its sources change and expires after seven days without a game launch.
|
||||
- With `auto_update_ampr=1`, ShadowMount+ checks for AMPR updates 30 seconds after startup and every four hours. It downloads a missing or newer emulator and displays a notification after a successful update.
|
||||
- The backport notification adds `Emulators updated` when emulator files are updated for the launched game.
|
||||
- If both global and per-game fakelib exist, they are combined in the game cache according to `global_fakelib_priority`, unless the selected backport contains `fakelib2`. Without a per-game fakelib, the global folder is mounted directly.
|
||||
- Use repeatable `global_fakelib_exclude=<TITLE_ID>` entries to skip the global fakelib for specific games without disabling per-game fakelib.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib and emulator updates.
|
||||
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
|
||||
|
||||
Kstuff game lifecycle behavior:
|
||||
@@ -149,7 +212,7 @@ Kstuff game lifecycle behavior:
|
||||
- `direct=<seconds>`
|
||||
- `image=<seconds>`
|
||||
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
|
||||
- When crash monitoring detects an app crash before kstuff was paused, ShadowMountPlus only notifies that the app crashed and kstuff is not to blame.
|
||||
- When crash monitoring detects an app crash before kstuff was paused, ShadowMountPlus only notifies that the app crashed.
|
||||
- When crash monitoring detects an app crash within 2 minutes after kstuff auto-pause, ShadowMountPlus doubles the applied pause delay for that title and upserts it into `/data/shadowmount/autotune.ini` (up to `3600` seconds), then prompts you to launch the game again.
|
||||
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
|
||||
|
||||
@@ -177,7 +240,7 @@ PFSC container layout requirement (`.ffpfsc`):
|
||||
- Do not place game files directly in the container root.
|
||||
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
|
||||
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
|
||||
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
|
||||
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification remains disabled for unsigned images; nested inner images request the PFSC compressed-offset cache before LVD attach.
|
||||
|
||||
## Compressed PFS containers (`.ffpfsc`)
|
||||
|
||||
@@ -209,7 +272,7 @@ mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --ver
|
||||
'./pfs_image.dat'
|
||||
```
|
||||
|
||||
Then pack the nested image into a compressed PFS container:
|
||||
Then pack the nested image (**pfs_image.dat**) into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
@@ -333,6 +396,42 @@ Recommended folder structure:
|
||||
|
||||
Recommended only for titles that need external-drive-style compatibility. For general use, prefer `.ffpkg`.
|
||||
|
||||
### LVD type-5 / BFS fast-path requirements
|
||||
|
||||
With `legacy_mount_exfat=0`, ShadowMount attaches `.exfat` through LVD as
|
||||
`img_type=5 (Sv)`, with a 512-byte
|
||||
logical sector and a 64 KiB `secondary_unit`. This lets the LVD worker collect
|
||||
up to 31 queued, 64-KiB-aligned requests of exactly 64 KiB and
|
||||
submit them through `bfs_iosession_rw_sdimg`/`BfsSdimg`. Non-matching requests
|
||||
continue through the normal vnode path.
|
||||
|
||||
All of the following are required for the kernel to consider that fast path:
|
||||
|
||||
- Use `exfat_backend=lvd` (the default). `/dev/mdctl` has no LVD image type or
|
||||
BFS sdimg batching.
|
||||
- Set `legacy_mount_exfat=0`; the default version 1.6 profile uses
|
||||
`img_type=0` and does not request this fast path.
|
||||
- Store the final `.exfat` file directly on the internal BFS, normally below
|
||||
`/data`. An image on USB/UFS/exFAT/PFS, or an exFAT image nested in compressed
|
||||
PFS, uses the normal path.
|
||||
- Format exFAT with a fixed 64 KiB allocation unit. The supplied Linux, macOS,
|
||||
and Windows builders now always use this value, including small-file images.
|
||||
- Keep `lvd_exfat_sector_size=512` and do not add an
|
||||
`image_sector=<name>.exfat:65536` override. A logical sector and an exFAT
|
||||
allocation unit are different geometries.
|
||||
- Make the image size a multiple of 64 KiB; the supplied builders round it to
|
||||
1 MiB and therefore satisfy this automatically.
|
||||
- Copy the completed image to BFS in one sequential operation when practical.
|
||||
This does not control eligibility, but avoids needless fragmentation of the
|
||||
backing file. The default read-only mount is preferred when writes are not
|
||||
required.
|
||||
|
||||
The 64 KiB cluster improves eligibility but cannot force every access through
|
||||
the batch path: exFAT metadata and small or unaligned application I/O still use
|
||||
the safe fallback. In the ShadowMount debug log, an eligible profile request is
|
||||
shown by `img=5 sec=512 sec2=65536`; the kernel still makes the final BFS and
|
||||
platform checks internally.
|
||||
|
||||
Linux (Ubuntu/Debian):
|
||||
- Required components installation:
|
||||
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
|
||||
@@ -344,10 +443,15 @@ Linux (Ubuntu/Debian):
|
||||
- Notes:
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
|
||||
- For manual exFAT builds, keep the cluster size at `64 KB` or you may lose performance.
|
||||
- Automatically selects exFAT cluster profile:
|
||||
- Large-file profile: `64K`
|
||||
- Small/mixed-file profile: `32K`
|
||||
- Always formats with `mkfs.exfat -c 64K` for the LVD/BFS profile.
|
||||
- For a manual build, use an image size divisible by 64 KiB and run `mkfs.exfat -c 64K <image>`.
|
||||
|
||||
macOS:
|
||||
|
||||
- Script: `mkexfat_macos.sh` (uses the built-in exFAT, disk-image and `rsync` tools).
|
||||
- Example: `chmod +x mkexfat_macos.sh && ./mkexfat_macos.sh ./APPXXXX ./PPSA12345.exfat`.
|
||||
- The script attaches the raw image as a device and formats it with
|
||||
`newfs_exfat -b 65536` before copying the game-root contents.
|
||||
|
||||
Windows:
|
||||
- Recommended: use `make_image.bat` (wrapper for `New-OsfExfatImage.ps1` + OSFMount).
|
||||
@@ -355,13 +459,14 @@ Windows:
|
||||
- Install OSFMount: https://www.osforensics.com/tools/mount-disk-images.html.
|
||||
- Keep `make_image.bat` and `New-OsfExfatImage.ps1` in the same folder.
|
||||
- Run `cmd.exe` as Administrator.
|
||||
- If you build an exFAT image manually instead of using the script, keep the cluster size at `64 KB` or you may lose performance.
|
||||
- If you build an exFAT image manually, format it with a `64K` allocation unit, for example `format X: /FS:exFAT /A:64K /Q`.
|
||||
- Usage:
|
||||
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
|
||||
- Behavior:
|
||||
- Auto-sizes the image to fit source content.
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Formats and copies source folder contents into image root.
|
||||
- Uses a fixed 64 KiB allocation unit for both large-file and small/mixed-file images.
|
||||
- Optional (fixed size): run PowerShell script directly:
|
||||
- `powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 -ImagePath "C:\images\game.exfat" -SourceDir "C:\payload\APPXXXX" -Size 8G -ForceOverwrite`
|
||||
|
||||
|
||||
+106
-21
@@ -21,6 +21,19 @@
|
||||
# Default: 0
|
||||
# quiet_mode=0
|
||||
|
||||
# Notification language:
|
||||
# auto -> use the console system language
|
||||
# Or set a supported locale, for example: en-US, ru-RU, de-DE, fr-FR, es-ES
|
||||
# Default: auto
|
||||
# language=auto
|
||||
|
||||
# Public HTTP/JSON API listener:
|
||||
# The loopback default accepts connections only from software running on the PS5.
|
||||
# Use 0.0.0.0 explicitly if remote devices on the network must connect.
|
||||
# Defaults: 127.0.0.1:10101
|
||||
# api_bind_address=127.0.0.1
|
||||
# api_port=10101
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
@@ -33,30 +46,43 @@
|
||||
# Default: 0
|
||||
# force_mount=0
|
||||
|
||||
# Keep discovered images mounted between game launches:
|
||||
# 1/true/yes/on -> mount during scans and keep images mounted
|
||||
# 0/false/no/off -> mount images on demand for each game (default)
|
||||
# USB-backed images are still unmounted during suspend.
|
||||
# Default: 0
|
||||
# persistent_image_mounts=0
|
||||
|
||||
# Use batch registration for newly staged titles:
|
||||
# 1/true/yes/on -> submit queued installs through sceAppInstUtilAppInstallAll
|
||||
# 0/false/no/off -> register each title directly
|
||||
# Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer
|
||||
# app_install_all=0
|
||||
|
||||
# Automatically remove games from the system library when their source
|
||||
# is no longer available:
|
||||
# 1/true/yes/on -> remove after the configured missing delay
|
||||
# 0/false/no/off -> keep the registration (default)
|
||||
# Default: 0
|
||||
# auto_remove_missing_games=0
|
||||
|
||||
# Allow automatic removal of games that have installed DLC:
|
||||
# 1/true/yes/on -> allow removal
|
||||
# 0/false/no/off -> keep games with installed DLC (default)
|
||||
# Default: 0
|
||||
# auto_remove_games_with_dlc=0
|
||||
|
||||
# How long a source must remain unavailable before removal (seconds),
|
||||
# range: 1..86400
|
||||
# Default: 300
|
||||
# auto_remove_missing_delay_seconds=300
|
||||
|
||||
# Scan depth relative to each scan root:
|
||||
# 1 -> scan only first-level subdirectories
|
||||
# 2 -> also scan one additional nested level
|
||||
# Default: 1
|
||||
# scan_depth=1
|
||||
|
||||
# Directory watcher granularity:
|
||||
# 1 -> keep vnode watchers on scan root subdirectories
|
||||
# 0 -> watch only scan roots/parents and rely on targeted/full scans for children
|
||||
# Default: 0
|
||||
# watch_subdirs=0
|
||||
|
||||
# Image file watcher granularity:
|
||||
# 1 -> keep vnode watchers on discovered image files
|
||||
# 0 -> detect image file changes through targeted/full scans
|
||||
# Default: 1
|
||||
# watch_image_files=1
|
||||
|
||||
# Legacy compatibility:
|
||||
# recursive_scan=1 forces scan_depth=2
|
||||
# Default: disabled
|
||||
@@ -75,17 +101,66 @@
|
||||
# lvd -> /dev/lvdctl -> /dev/lvdN
|
||||
# md -> /dev/mdctl -> /dev/mdN
|
||||
# Defaults: exfat=lvd, ufs=lvd
|
||||
# LVD is required for the exFAT img_type=5/BFS sdimg fast path. The image file
|
||||
# must also reside on internal BFS and be formatted with a 64 KiB cluster.
|
||||
# exfat_backend=lvd
|
||||
# ufs_backend=lvd
|
||||
|
||||
# Mount profile per image type:
|
||||
# 1/true/yes/on -> use the version 1.6 attach/nmount profile (default)
|
||||
# 0/false/no/off -> use the optimized version 1.7 profile
|
||||
# Changes apply the next time an image is mounted; active mounts are not reset.
|
||||
# ufs = .ffpkg
|
||||
# exfat = .exfat
|
||||
# pfs = .ffpfs and nested pfs_image.dat
|
||||
# pfsc = outer .ffpfsc container
|
||||
# legacy_mount_ufs=1
|
||||
# legacy_mount_exfat=1
|
||||
# legacy_mount_pfs=1
|
||||
# legacy_mount_pfsc=1
|
||||
|
||||
# PFS compressed-file index cache for nested images in legacy mode:
|
||||
# 1/true/yes/on -> cache the containing PFS compressed-file index before
|
||||
# attaching a nested image
|
||||
# 0/false/no/off -> keep the version 1.6 behavior (default)
|
||||
# The optimized profile always requests this cache when supported.
|
||||
# The old legacy_gddr5_cache key remains accepted for compatibility.
|
||||
# nested_pfs_index_cache=0
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
|
||||
# 1/true/yes/on -> mount app0/fakelib2 when present, otherwise app0/fakelib, into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
# Updates for multiple emulators in the game fakelib:
|
||||
# 1/true/yes/on -> cache the game's fakelib before launch and replace matching
|
||||
# cached files from emulators_path; fakelib2 is never changed
|
||||
# 0/false/no/off -> disable emulator updates
|
||||
# Default: 1
|
||||
# update_emulators=1
|
||||
#
|
||||
# Folder containing emulator update files (absolute path):
|
||||
# emulators_path=/data/shadowmount/emus
|
||||
# Cache: /data/shadowmount/cache/<TITLE_ID>/fakelib
|
||||
# Unused caches expire seven days after the most recent game launch.
|
||||
|
||||
# Automatic libSceAmpr emulator download:
|
||||
# 1/true/yes/on -> check 30 seconds after startup and then every four hours
|
||||
# 0/false/no/off -> do not access the network (default)
|
||||
# A missing local emulator is downloaded directly.
|
||||
# Otherwise, the SPRX is downloaded only when the server file is newer, then
|
||||
# replaces emulators_path/libSceAmpr.sprx only when its contents differ.
|
||||
# Downloads are deferred while a game mount is active.
|
||||
# Default: 0
|
||||
# auto_update_ampr=0
|
||||
#
|
||||
# HTTP or HTTPS download URL:
|
||||
# ampr_update_url=https://github.com/drakmor/ampr_emu/releases/latest/download/libSceAmpr.sprx
|
||||
|
||||
# Global fakelib overlay:
|
||||
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
|
||||
# 1/true/yes/on -> use global fakelib for every tracked game when present;
|
||||
# fakelib2 always remains exclusive when present
|
||||
# 0/false/no/off -> do not use the global fakelib folder
|
||||
# Default: 1
|
||||
# global_fakelib=1
|
||||
@@ -93,9 +168,9 @@
|
||||
# Default global fakelib folder:
|
||||
# global_fakelib_path=/data/shadowmount/fakelib
|
||||
#
|
||||
# Priority when both global and per-game fakelib exist:
|
||||
# game -> mount global first, then game-specific fakelib (game wins)
|
||||
# global -> mount game-specific fakelib first, then global fakelib (global wins)
|
||||
# Priority when both global and per-game fakelib exist in the combined cache:
|
||||
# game -> game-specific files win
|
||||
# global -> global files win
|
||||
# Default: game
|
||||
# global_fakelib_priority=game
|
||||
#
|
||||
@@ -114,6 +189,13 @@
|
||||
# Default: 1
|
||||
# kstuff_crash_detection=1
|
||||
|
||||
# Automatic fan-controller target for games:
|
||||
# system -> leave the system-managed target unchanged (default)
|
||||
# 50..91 -> apply this target when a game starts
|
||||
# The stock automatic fan controller remains enabled.
|
||||
# Default: system
|
||||
# fan_target_temperature=system
|
||||
|
||||
# Delay before pausing kstuff for image-backed titles.
|
||||
# Default: 25
|
||||
# kstuff_pause_delay_image_seconds=25
|
||||
@@ -123,12 +205,15 @@
|
||||
# kstuff_pause_delay_direct_seconds=15
|
||||
|
||||
# Backend-specific sector size overrides:
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=4096, md_exfat=512
|
||||
# md_ufs defaults to 512 in legacy mode and 4096 in optimized mode.
|
||||
# Keep lvd_exfat at 512 for the BFS fast path. 65536 is the exFAT cluster and
|
||||
# LVD secondary unit in the optimized profile, not the logical sector size.
|
||||
# lvd_exfat_sector_size=512
|
||||
# lvd_ufs_sector_size=4096
|
||||
# lvd_pfs_sector_size=32768
|
||||
# lvd_pfs_sector_size=4096
|
||||
# md_exfat_sector_size=512
|
||||
# md_ufs_sector_size=512
|
||||
# md_ufs_sector_size=4096
|
||||
|
||||
# Optional custom scan roots (can be repeated):
|
||||
# If at least one scanpath=... is present, only these paths are used.
|
||||
@@ -144,7 +229,7 @@
|
||||
## Examples:
|
||||
## image_ro=PPSA12345.ffpfs
|
||||
## image_rw=My Game Dump.exfat
|
||||
## image_sector=My Game Dump.exfat:65536
|
||||
## image_sector=Legacy 512 Sector.exfat:512
|
||||
|
||||
## Keep kstuff enabled for specific title IDs (repeatable):
|
||||
## kstuff_no_pause=PPSA12345
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
openapi: 3.1.2
|
||||
info:
|
||||
title: ShadowMount HTTP API
|
||||
version: "1"
|
||||
description: |
|
||||
HTTP/JSON API exposed by the ShadowMountPlus payload. The service has no
|
||||
authentication and should only be exposed on a trusted network.
|
||||
servers:
|
||||
- url: http://127.0.0.1:10101
|
||||
description: Default loopback endpoint on the PS5
|
||||
- url: http://{address}:{port}
|
||||
description: Configured network endpoint
|
||||
variables:
|
||||
address:
|
||||
default: 192.168.1.50
|
||||
description: PS5 IPv4 address or hostname
|
||||
port:
|
||||
default: "10101"
|
||||
description: Value of api_port
|
||||
security: []
|
||||
tags:
|
||||
- name: Service
|
||||
- name: Images
|
||||
- name: Games
|
||||
|
||||
paths:
|
||||
/api/v1/version:
|
||||
options:
|
||||
operationId: preflightVersion
|
||||
summary: CORS preflight
|
||||
tags: [Service]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: getVersion
|
||||
summary: Get API version and capabilities
|
||||
tags: [Service]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/EmptyRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/VersionSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/images:
|
||||
options:
|
||||
operationId: preflightImages
|
||||
summary: CORS preflight
|
||||
tags: [Images]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: listImages
|
||||
summary: Get the complete image snapshot
|
||||
tags: [Images]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/EmptyRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ImagesSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/games:
|
||||
options:
|
||||
operationId: preflightGames
|
||||
summary: CORS preflight
|
||||
tags: [Games]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: listGames
|
||||
summary: Get the complete game snapshot
|
||||
tags: [Games]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/EmptyRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/GamesSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/games/mount:
|
||||
options:
|
||||
operationId: preflightMountGame
|
||||
summary: CORS preflight
|
||||
tags: [Games]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: mountGame
|
||||
summary: Mount a managed game
|
||||
tags: [Games]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TitleRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MountSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
$ref: "#/components/responses/Busy"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/games/unmount:
|
||||
options:
|
||||
operationId: preflightUnmountGame
|
||||
summary: CORS preflight
|
||||
tags: [Games]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: unmountGame
|
||||
summary: Unmount a managed game
|
||||
tags: [Games]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TitleRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/UnmountSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
$ref: "#/components/responses/Busy"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
components:
|
||||
requestBodies:
|
||||
EmptyRequest:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example: {}
|
||||
TitleRequest:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TitleRequest"
|
||||
|
||||
headers:
|
||||
AllowOrigin:
|
||||
description: Browser origins allowed to access the API
|
||||
schema:
|
||||
type: string
|
||||
const: "*"
|
||||
AllowMethods:
|
||||
description: Methods allowed by CORS preflight
|
||||
schema:
|
||||
type: string
|
||||
const: POST, OPTIONS
|
||||
AllowHeaders:
|
||||
description: Request headers allowed by CORS preflight
|
||||
schema:
|
||||
type: string
|
||||
const: Content-Type
|
||||
AllowPrivateNetwork:
|
||||
description: Private Network Access permission
|
||||
schema:
|
||||
type: string
|
||||
const: "true"
|
||||
MaxAge:
|
||||
description: Number of seconds for which the preflight may be cached
|
||||
schema:
|
||||
type: integer
|
||||
const: 86400
|
||||
|
||||
responses:
|
||||
Preflight:
|
||||
description: CORS preflight accepted
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
Access-Control-Allow-Methods:
|
||||
$ref: "#/components/headers/AllowMethods"
|
||||
Access-Control-Allow-Headers:
|
||||
$ref: "#/components/headers/AllowHeaders"
|
||||
Access-Control-Allow-Private-Network:
|
||||
$ref: "#/components/headers/AllowPrivateNetwork"
|
||||
Access-Control-Max-Age:
|
||||
$ref: "#/components/headers/MaxAge"
|
||||
|
||||
VersionSuccess:
|
||||
description: API version and capabilities
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VersionResponse"
|
||||
|
||||
ImagesSuccess:
|
||||
description: Complete image snapshot
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ImagesResponse"
|
||||
|
||||
GamesSuccess:
|
||||
description: Complete game snapshot
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GamesResponse"
|
||||
|
||||
MountSuccess:
|
||||
description: Game mounted
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MountResponse"
|
||||
|
||||
UnmountSuccess:
|
||||
description: Game unmounted
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UnmountResponse"
|
||||
|
||||
BadRequest:
|
||||
description: Invalid HTTP request, JSON body or title ID
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
NotFound:
|
||||
description: Managed title tracker was not found
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
Busy:
|
||||
description: Runtime state does not currently permit the operation
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
status: 16
|
||||
error: Device busy
|
||||
LengthRequired:
|
||||
description: Content-Length is required
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
PayloadTooLarge:
|
||||
description: JSON request body exceeds 4096 bytes
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
UnsupportedMediaType:
|
||||
description: Content-Type is not application/json
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
HeadersTooLarge:
|
||||
description: HTTP headers exceed 8192 bytes
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
InternalError:
|
||||
description: Internal failure
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
NotImplemented:
|
||||
description: Unsupported transfer encoding or runtime operation
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
schemas:
|
||||
TitleId:
|
||||
type: string
|
||||
pattern: "^(CUSA|PPSA)[0-9]{5}$"
|
||||
examples: [PPSA12345]
|
||||
|
||||
TitleRequest:
|
||||
type: object
|
||||
required: [title_id]
|
||||
properties:
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
|
||||
VersionResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, api_version, shadowmount_version, capabilities]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
api_version:
|
||||
type: integer
|
||||
const: 1
|
||||
shadowmount_version:
|
||||
type: string
|
||||
capabilities:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [list_images, list_games, mount_game, unmount_game]
|
||||
example: [list_images, list_games, mount_game, unmount_game]
|
||||
|
||||
Image:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- path
|
||||
- mount_point
|
||||
- size
|
||||
- mtime_sec
|
||||
- mtime_nsec
|
||||
- unit_id
|
||||
- backend
|
||||
- complete
|
||||
- source_available
|
||||
- mapped
|
||||
- mounted
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
mount_point:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
mtime_sec:
|
||||
type: integer
|
||||
format: int64
|
||||
mtime_nsec:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 999999999
|
||||
unit_id:
|
||||
type: integer
|
||||
minimum: -1
|
||||
backend:
|
||||
type: string
|
||||
enum: [LVD, MD, UNKNOWN]
|
||||
complete:
|
||||
type: boolean
|
||||
source_available:
|
||||
type: boolean
|
||||
mapped:
|
||||
type: boolean
|
||||
mounted:
|
||||
type: boolean
|
||||
|
||||
ImagesResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, count, images]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
images:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Image"
|
||||
|
||||
Game:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- path
|
||||
- image_path
|
||||
- title_id
|
||||
- title_name
|
||||
- installed
|
||||
- managed
|
||||
- mounted
|
||||
- image_backed
|
||||
- source_available
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
image_path:
|
||||
type: string
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
title_name:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
managed:
|
||||
type: boolean
|
||||
mounted:
|
||||
type: boolean
|
||||
image_backed:
|
||||
type: boolean
|
||||
source_available:
|
||||
type: boolean
|
||||
|
||||
GamesResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, count, games]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
games:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Game"
|
||||
|
||||
MountResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, title_id, mounted]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
mounted:
|
||||
type: boolean
|
||||
const: true
|
||||
|
||||
UnmountResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, title_id, mounted]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
mounted:
|
||||
type: boolean
|
||||
const: false
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, error]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Positive errno value
|
||||
error:
|
||||
type: string
|
||||
@@ -0,0 +1,97 @@
|
||||
# ShadowMount HTTP/JSON API v1
|
||||
|
||||
The machine-readable OpenAPI 3.1 description is available in
|
||||
[`openapi.yaml`](openapi.yaml).
|
||||
|
||||
ShadowMount exposes its public API over HTTP/TCP. The default endpoint is
|
||||
`127.0.0.1:10101`, so only software running on the PS5 can connect. Set
|
||||
`api_bind_address=0.0.0.0` to allow clients on the network, or select a
|
||||
specific PS5 IPv4 address. `api_port` accepts values from 1 through 65535.
|
||||
Both settings are applied on runtime config reload.
|
||||
|
||||
The API has no authentication. Bind it to a network-facing address only on a
|
||||
trusted network.
|
||||
|
||||
The internal `/system_tmp/shadowmount.sock` Unix socket remains private to the
|
||||
SceShellCore bridge and must not be used by third-party applications.
|
||||
|
||||
The HTTP service owns and monitors its listener. If initial `socket/bind/listen`
|
||||
or a later `accept` fails, it retries with an interruptible 250 ms to 5 s
|
||||
backoff. Only the transition into recovery and the successful recovery are
|
||||
logged. Shutdown and config reload interrupt the wait immediately.
|
||||
|
||||
Every API operation must use `POST`, include `Content-Type: application/json`
|
||||
and contain one JSON object. Every response is a JSON object. Connections are
|
||||
closed after one response; chunked request bodies are not supported. The
|
||||
maximum request body is 4096 bytes.
|
||||
|
||||
All responses include `Access-Control-Allow-Origin: *`. Browser preflight
|
||||
requests using `OPTIONS` are supported and allow `POST`, the `Content-Type`
|
||||
header and Private Network Access. This permits calls from any browser origin;
|
||||
the listener still needs `api_bind_address=0.0.0.0` for access from another
|
||||
device.
|
||||
|
||||
## Routes
|
||||
|
||||
| Route | Request | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `/api/v1/version` | `{}` | API and ShadowMount versions plus capabilities |
|
||||
| `/api/v1/images` | `{}` | Complete image snapshot |
|
||||
| `/api/v1/games` | `{}` | Complete game snapshot |
|
||||
| `/api/v1/games/mount` | `{"title_id":"PPSA12345"}` | Mount a managed game |
|
||||
| `/api/v1/games/unmount` | `{"title_id":"PPSA12345"}` | Unmount a managed game |
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -sS http://127.0.0.1:10101/api/v1/version \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
|
||||
curl -sS http://127.0.0.1:10101/api/v1/games \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Responses
|
||||
|
||||
Every response contains numeric `status`. It is zero on success, or a positive
|
||||
errno value on failure. Error responses also contain `error` and use an
|
||||
appropriate HTTP status such as 400, 404, 409, 413 or 415.
|
||||
|
||||
A successful version response has this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 0,
|
||||
"api_version": 1,
|
||||
"shadowmount_version": "1.7",
|
||||
"capabilities": ["list_images", "list_games", "mount_game", "unmount_game"]
|
||||
}
|
||||
```
|
||||
|
||||
List responses contain `count` and the complete `images` or `games` array.
|
||||
Image items expose `path`, `mount_point`, `size`, modification time, `unit_id`,
|
||||
`backend`, `complete`, `source_available`, `mapped` and `mounted`. Game items
|
||||
expose `path`, `image_path`, `title_id`, `title_name`, `installed`, `managed`,
|
||||
`mounted`, `image_backed` and `source_available`.
|
||||
|
||||
Mount mutations remain conservative. They return HTTP 409 with `status` set to
|
||||
`EBUSY` while a game is active, while ShellCore owns another prepared title,
|
||||
during suspend, or while a conflicting mount/release is in progress. Clients
|
||||
must not retry this response in a tight loop. HTTP 404 with `ENOENT` means the
|
||||
title has no ShadowMount `mount.lnk` tracker.
|
||||
With `persistent_image_mounts=1`, unmounting a game releases its per-title
|
||||
runtime layers but keeps the backing image mounted.
|
||||
|
||||
## Test client
|
||||
|
||||
Run the host-side Python client while ShadowMount is active:
|
||||
|
||||
```sh
|
||||
python3 tools/api_test.py 192.168.1.50 10101
|
||||
```
|
||||
|
||||
It validates CORS preflight, the version response and complete image/game
|
||||
snapshots, then selects the first safe managed game for a mount/unmount cycle.
|
||||
The mutation test is skipped if there is no candidate or the runtime is busy.
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_ar_sa[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "الانتقال إلى إعدادات التصحيح",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor والمجتمع",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "شادوماونت+: لم يعد الفحص التكراري مدعوماً، وسيتم استخدام عمق الفحص ٢.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "شادوماونت+: تم تفعيل عمق الفحص %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "فشل تركيب طبقة التوافق مع الإصدارات الأقدم: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "طُبّق دعم الإصدار الأقدم على اللعبة: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "طُبّق دعم الإصدار الأقدم على اللعبة: %s\nتم تحديث المحاكيات",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "تم رفض تحميل الصورة:\n%s\nحجم الكتلة صغير جداً (%llu < %u).\nتم حفظ قيمة حجم القطاع الجديدة: %u.\nحاول التحميل مرة أخرى.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "تم رفض تحميل الصورة:\n%s\nحجم الكتلة صغير جداً (%llu < %u).\nاستخدم حجماً أكبر للكتلة في الصورة أو حجماً أصغر للقطاع في الإعدادات.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "فشل التسجيل: %s (%s)\nمجلد تثبيت التطبيقات غير متاح",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "تعذر التسجيل: %s (%s)\nجسر TitleDir غير متاح",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "فشل التسجيل: %s (%s)\nالرمز=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "جارٍ التثبيت: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "أصبح KStuff نشطًا من جديد",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "أُوقف KStuff مؤقتًا أثناء تشغيل اللعبة",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "تم التثبيت: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "غير معروف",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "فشل تحميل الصورة: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "حجم كتلة نظام الملفات (%llu) أصغر من حجم قطاع الجهاز (%u) لـ %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "الصورة المحمّلة غير قابلة للقراءة أو تالفة",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "ذاكرة الصور المؤقتة ممتلئة (%u إدخالًا)، ولا يمكن تتبع التركيب %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "ذاكرة الصور المؤقتة ممتلئة (%u إدخالًا)، جارٍ التراجع عن التركيب",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "فشل التثبيت: %s (%s)\nانتهت المهلة بعد 5 دقائق",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "تمت إضافة التثبيت الجماعي إلى قائمة الانتظار (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... و %d أخرى",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "فشل التثبيت الجماعي لـ %d تطبيق.\nالرمز=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "تعطّل التطبيق %s قبل إيقاف KStuff مؤقتًا. لم يكن KStuff هو السبب.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "تعطل %s قبل إيقاف KStuff مؤقتًا:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "تعطّل التطبيق %s بعد إيقاف KStuff مؤقتًا. تم تخطي الضبط التلقائي.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "تعطل %s بعد إيقاف KStuff مؤقتًا:\n%s\nتم تخطي الضبط التلقائي.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "تعذر تحميل الوحدة بعد إيقاف كايستاف مؤقتاً",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "تعذر تحميل الوحدة %.*s بعد إيقاف KStuff مؤقتًا",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. تمت زيادة التأخير إلى %u ثانية.\nشغّل اللعبة مرة أخرى.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "تم اكتشاف تعطل بعد إيقاف كايستاف مؤقتاً: تمت زيادة مدة الإيقاف الخاصة بـ %s إلى %u ثانية. شغّل اللعبة مرة أخرى.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "تعطل %s بعد إيقاف KStuff مؤقتًا:\n%s\nتمت زيادة التأخير إلى %u ث.\nشغّل اللعبة مجددًا.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "تم اكتشاف تعطل بعد إيقاف كايستاف مؤقتاً: %s. تعذر تحديث مدة التحسين التلقائي.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "تعطل %s بعد إيقاف KStuff مؤقتًا:\n%s\nتعذر تحديث تأخير الضبط التلقائي.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "ملف param.json مفقود أو غير صالح:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "عُثر على %d ألعاب جديدة. جارٍ معالجتها...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "تمت مزامنة المكتبة.\nتم العثور على %d ألعاب.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: أُعيد تحميل الإعدادات.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "تم تجاهل %s المكرر:\n%s\nالموجود مسبقًا: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "تم تحديث محاكي AMPR",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: تعذر تثبيت خطافات ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "درجة الحرارة المستهدفة %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "تم توصيل محرك الأقراص الخارجي %s.\nالمتاح %llu.%llu من %llu.%llu غيغابايت (حجم الكتلة %llu كيلوبايت)\nالألعاب على القرص: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "تم فصل محرك الأقراص الخارجي %s",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_cs_cz[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Přejít do nastavení ladění",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor a komunita",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 je zastaralé, používá se scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: povolena hloubka prohledávání %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay selhal: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport hry použit: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport hry použit: %s\nEmulátory aktualizovány",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Připojení obrazu odmítnuto:\n%s\nVelikost clusteru je příliš malá (%llu < %u).\nUloženo přepsání image_sector: %u.\nZkuste připojit znovu.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Připojení obrazu odmítnuto:\n%s\nVelikost clusteru je příliš malá (%llu < %u).\nPoužijte v obrazu větší cluster nebo v konfiguraci menší velikost sektoru.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrace selhala: %s (%s)\nAppInstallTitleDir není dostupné",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registrace selhala: %s (%s)\nMost TitleDir není dostupný",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrace selhala: %s (%s)\nkód=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Instalace: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff je znovu aktivní",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pozastaven, zatímco hra běží",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Nainstalováno: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Neznámé",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Připojení obrazu selhalo: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Velikost clusteru souborového systému (%llu) je menší než velikost sektoru zařízení (%u) pro %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Připojený obraz je nečitelný nebo poškozený",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Mezipaměť obrazů je plná (%u položek), připojení %s nelze sledovat",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Mezipaměť obrazů je plná (%u položek), připojení se vrací zpět",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Instalace selhala: %s (%s)\nČasový limit po 5 minutách",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Dávková instalace ve frontě (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... a dalších %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Dávková instalace selhala pro %d aplikací.\nkód=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Aplikace %s selhala před pozastavením KStuff. Příčinou není KStuff.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Aplikace %s selhala před pozastavením KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Aplikace %s selhala po pozastavení KStuff. Automatické ladění bylo přeskočeno.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Aplikace %s selhala po pozastavení KStuff:\n%s\nAutomatické ladění bylo přeskočeno.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Nelze načíst modul po pauze KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "po pozastavení KStuff nelze načíst modul %.*s",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Zpoždění zvýšeno na %u s.\nSpusťte hru znovu.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Po pozastavení KStuff byla zjištěna chyba: prodleva pro %s byla zvýšena na %u s. Spusťte hru znovu.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "Aplikace %s selhala po pozastavení KStuff:\n%s\nProdleva byla zvýšena na %u s.\nSpusťte hru znovu.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Po pozastavení KStuff byla zjištěna chyba: %s. Prodlevu automatického ladění se nepodařilo aktualizovat.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "Aplikace %s selhala po pozastavení KStuff:\n%s\nProdlevu automatického ladění se nepodařilo aktualizovat.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Chybí nebo je neplatný param.json:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Nalezeno %d nových her. Zpracovávám...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Knihovna synchronizována.\nNalezeno %d her.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: konfigurace byla znovu načtena.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicitní %s ignorováno:\n%s\nexistující: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulátor AMPR byl aktualizován",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: instalace hooků ShellCore se nezdařila",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Cílová teplota %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Externí disk %s připojen.\nVolné místo: %llu.%llu z %llu.%llu GB (blok %llu KB)\nHer na disku: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Externí disk %s odpojen",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_da_dk[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Gå til fejlfindingsindstillinger",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor og fællesskabet",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 er forældet, bruger scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: scanningsdybde %u aktiveret.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-overlay mislykkedes: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Spil backportet: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Spil backportet: %s\nEmulatorer opdateret",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montering af image afvist:\n%s\nKlyngestørrelsen er for lille (%llu < %u).\nimage_sector-tilsidesættelse gemt: %u.\nPrøv at montere igen.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montering af image afvist:\n%s\nKlyngestørrelsen er for lille (%llu < %u).\nBrug en større klyngestørrelse i imaget eller en mindre sektorstørrelse i konfigurationen.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrering mislykkedes: %s (%s)\nAppInstallTitleDir ikke tilgængelig",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registrering mislykkedes: %s (%s)\nTitleDir-broen er ikke tilgængelig",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrering mislykkedes: %s (%s)\nkode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installerer: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff er aktiv igen",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pauset mens spillet kører",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installeret: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Ukendt",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Montering af image mislykkedes: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Filsystemets klyngestørrelse (%llu) er mindre end enhedens sektorstørrelse (%u) for %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Mountet image er ulæseligt eller beskadiget",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Imagecachen er fuld (%u poster), monteringen %s kan ikke spores",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Imagecachen er fuld (%u poster), monteringen rulles tilbage",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Installation mislykkedes: %s (%s)\nTimeout efter 5 minutter",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batchinstallation i kø (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... og %d mere",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Batchinstallationen mislykkedes for %d apps.\nkode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Appen %s gik ned, før KStuff blev sat på pause. KStuff er ikke årsagen.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Appen %s gik ned, før KStuff blev sat på pause:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Appen %s gik ned, efter KStuff blev sat på pause. Automatisk justering blev sprunget over.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Appen %s gik ned, efter KStuff blev sat på pause:\n%s\nAutomatisk justering blev sprunget over.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Kan ikke indlæse modul efter KStuff-pause",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "modulet %.*s kan ikke indlæses efter KStuff-pausen",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Forsinkelse øget til %u s.\nStart spillet igen.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Nedbrud registreret efter KStuff-pausen: pauseforsinkelsen for %s blev øget til %u s. Start spillet igen.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s gik ned efter KStuff blev sat på pause:\n%s\nForsinkelsen blev øget til %u s.\nStart spillet igen.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Nedbrud registreret efter KStuff-pausen: %s. Forsinkelsen for automatisk justering kunne ikke opdateres.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s gik ned efter KStuff blev sat på pause:\n%s\nForsinkelsen for automatisk justering kunne ikke opdateres.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json mangler eller er ugyldig:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Fandt %d nye spil. Behandler...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliotek synkroniseret.\nFandt %d spil.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: konfigurationen blev genindlæst.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Dublet %s ignoreret:\n%s\neksisterende: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR-emulator opdateret",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore-hooks kunne ikke installeres",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Måltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Eksternt drev %s tilsluttet.\nLedig plads: %llu.%llu af %llu.%llu GB (blok %llu KB)\nSpil på drevet: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Eksternt drev %s frakoblet",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_de_de[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Zu den Debug-Einstellungen",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor und die Community",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 ist veraltet, scan_depth=2 wird verwendet.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: Scan-Tiefe %u ist aktiviert.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-Overlay fehlgeschlagen: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Spiel backportiert: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Spiel backportiert: %s\nEmulatoren aktualisiert",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Einbindung des Images abgelehnt:\n%s\nDie Clustergröße ist zu klein (%llu < %u).\nimage_sector-Überschreibung gespeichert: %u.\nVersuche es erneut.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Einbindung des Images abgelehnt:\n%s\nDie Clustergröße ist zu klein (%llu < %u).\nVerwende im Image eine größere Clustergröße oder in der Konfiguration eine kleinere Sektorgröße.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrierung fehlgeschlagen: %s (%s)\nAppInstallTitleDir nicht verfügbar",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registrierung fehlgeschlagen: %s (%s)\nTitleDir-Bridge nicht verfügbar",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrierung fehlgeschlagen: %s (%s)\nCode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installiere: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff ist wieder aktiv",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pausiert, während das Spiel läuft",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installiert: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Unbekannt",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Einbindung des Images fehlgeschlagen: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Dateisystem-Clustergröße (%llu) ist kleiner als Gerätesektorgröße (%u) für %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Gemountetes Image ist nicht lesbar oder beschädigt",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Image-Cache voll (%u Einträge), Einbindung %s kann nicht verfolgt werden",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-Cache voll (%u Einträge), Einbindung wird rückgängig gemacht",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Installation fehlgeschlagen: %s (%s)\nZeitüberschreitung nach 5 Minuten",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batch-Installation eingereiht (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... und %d weitere",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Batch-Installation für %d Apps fehlgeschlagen.\nCode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s ist abgestürzt, bevor KStuff pausiert wurde. KStuff ist nicht die Ursache.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s ist vor der KStuff-Pause abgestürzt:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s ist abgestürzt, nachdem KStuff pausiert wurde. Die automatische Abstimmung wurde übersprungen.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s ist nach der KStuff-Pause abgestürzt:\n%s\nAutomatische Abstimmung übersprungen.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Modul kann nach KStuff-Pause nicht geladen werden",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "Modul %.*s kann nach der KStuff-Pause nicht geladen werden",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Verzögerung auf %u s erhöht.\nStarte das Spiel erneut.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Absturz nach der KStuff-Pause erkannt: Die Pausenverzögerung für %s wurde auf %u s erhöht. Starte das Spiel erneut.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s ist nach der KStuff-Pause abgestürzt:\n%s\nVerzögerung auf %u s erhöht.\nStarte das Spiel erneut.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Absturz nach der KStuff-Pause erkannt: %s. Die Verzögerung der automatischen Abstimmung konnte nicht aktualisiert werden.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s ist nach der KStuff-Pause abgestürzt:\n%s\nVerzögerung der automatischen Abstimmung konnte nicht aktualisiert werden.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json fehlt oder ist ungültig:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d neue Spiele gefunden. Verarbeitung läuft...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliothek synchronisiert.\n%d Spiele gefunden.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: Konfiguration neu geladen.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s ignoriert:\n%s\nvorhanden: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR-Emulator aktualisiert",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore-Hooks konnten nicht installiert werden",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Zieltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Externes Laufwerk %s verbunden.\nFrei: %llu.%llu von %llu.%llu GB (Blockgröße %llu KB)\nSpiele auf dem Laufwerk: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Externes Laufwerk %s getrennt",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_el_gr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Μετάβαση στις ρυθμίσεις αποσφαλμάτωσης",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor και η κοινότητα",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: το recursive_scan=1 είναι παρωχημένο, χρησιμοποιείται scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: ενεργό βάθος σάρωσης %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Αποτυχία backport overlay: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Εφαρμόστηκε backport στο παιχνίδι: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Εφαρμόστηκε backport στο παιχνίδι: %s\nΟι εξομοιωτές ενημερώθηκαν",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Η προσάρτηση του ειδώλου απορρίφθηκε:\n%s\nΤο μέγεθος συστοιχίας είναι πολύ μικρό (%llu < %u).\nΑποθηκεύτηκε η τιμή image_sector: %u.\nΔοκιμάστε ξανά την προσάρτηση.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Η προσάρτηση του ειδώλου απορρίφθηκε:\n%s\nΤο μέγεθος συστοιχίας είναι πολύ μικρό (%llu < %u).\nΧρησιμοποιήστε μεγαλύτερη συστοιχία στο είδωλο ή μικρότερο μέγεθος τομέα στις ρυθμίσεις.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Αποτυχία εγγραφής: %s (%s)\nΤο AppInstallTitleDir δεν είναι διαθέσιμο",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Η εγγραφή απέτυχε: %s (%s)\nΗ γέφυρα TitleDir δεν είναι διαθέσιμη",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Αποτυχία εγγραφής: %s (%s)\nκωδικός=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Εγκατάσταση: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "Το KStuff είναι ξανά ενεργό",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "Το KStuff παύθηκε όσο τρέχει το παιχνίδι",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Εγκαταστάθηκε: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Άγνωστο",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Η προσάρτηση του ειδώλου απέτυχε: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Το μέγεθος cluster του συστήματος αρχείων (%llu) είναι μικρότερο από το μέγεθος sector της συσκευής (%u) για %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Το mounted image δεν διαβάζεται ή είναι κατεστραμμένο",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Η προσωρινή μνήμη ειδώλων είναι πλήρης (%u εγγραφές), δεν είναι δυνατή η παρακολούθηση της προσάρτησης %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Η προσωρινή μνήμη ειδώλων είναι πλήρης (%u εγγραφές), γίνεται αναίρεση της προσάρτησης",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Αποτυχία εγκατάστασης: %s (%s)\nTimeout μετά από 5 λεπτά",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Μαζική εγκατάσταση στην ουρά (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... και %d ακόμα",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Η μαζική εγκατάσταση απέτυχε για %d app(s).\nκωδικός=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Η εφαρμογή %s κατέρρευσε πριν από την παύση του KStuff. Δεν ευθύνεται το KStuff.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Το %s κατέρρευσε πριν από την παύση του KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Η εφαρμογή %s κατέρρευσε μετά την παύση του KStuff. Η αυτόματη ρύθμιση παραλείφθηκε.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Το %s κατέρρευσε μετά την παύση του KStuff:\n%s\nΗ αυτόματη ρύθμιση παραλείφθηκε.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Δεν μπορεί να φορτωθεί module μετά την παύση KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "δεν είναι δυνατή η φόρτωση της μονάδας %.*s μετά την παύση του KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Η καθυστέρηση αυξήθηκε σε %u s.\nΕκκινήστε ξανά το παιχνίδι.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Εντοπίστηκε κατάρρευση μετά την παύση του KStuff: η καθυστέρηση για το %s αυξήθηκε σε %u δ. Εκκινήστε ξανά το παιχνίδι.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "Το %s κατέρρευσε μετά την παύση του KStuff:\n%s\nΗ καθυστέρηση αυξήθηκε σε %u δ.\nΕκκινήστε ξανά το παιχνίδι.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Εντοπίστηκε κατάρρευση μετά την παύση του KStuff: %s. Αποτυχία ενημέρωσης της καθυστέρησης αυτόματης ρύθμισης.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "Το %s κατέρρευσε μετά την παύση του KStuff:\n%s\nΑποτυχία ενημέρωσης της καθυστέρησης αυτόματης ρύθμισης.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Λείπει ή είναι άκυρο το param.json:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Βρέθηκαν %d νέα παιχνίδια. Γίνεται επεξεργασία...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Η βιβλιοθήκη συγχρονίστηκε.\nΒρέθηκαν %d παιχνίδια.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: οι ρυθμίσεις επαναφορτώθηκαν.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Το διπλότυπο %s αγνοήθηκε:\n%s\nυπάρχον: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Ο εξομοιωτής AMPR ενημερώθηκε",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: απέτυχε η εγκατάσταση των hooks του ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Θερμοκρασία-στόχος %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Ο εξωτερικός δίσκος %s συνδέθηκε.\nΕλεύθερα: %llu.%llu από %llu.%llu GB (μέγεθος μπλοκ %llu KB)\nΠαιχνίδια στον δίσκο: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Ο εξωτερικός δίσκος %s αποσυνδέθηκε",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_en_gb[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Go to Debug Settings",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor and Community",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: scan depth %u enabled.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay failed: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport applied to game: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport applied to game: %s\nEmulators updated",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Register failed: %s (%s)\nTitleDir bridge unavailable",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Register failed: %s (%s)\ncode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installing: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff active again",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff paused while the game is running",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installed: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Unknown",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Image mount failed: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Mounted image is unreadable or damaged",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Image cache full (%u entries), cannot track mount %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image cache full (%u entries), rolling back mount",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Install failed: %s (%s)\nTimed out after 5 minutes",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batch install queued (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... and %d more",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Batch install failed for %d app(s).\ncode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s crashed before KStuff was paused. KStuff is not the cause.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s crashed before KStuff pause:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s crashed after KStuff was paused. Automatic tuning skipped.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s crashed after KStuff pause:\n%s\nAutotune skipped.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Can't load module after KStuff pause",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "can't load module %.*s after KStuff pause",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Delay increased to %us.\nLaunch the game again.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Crash detected after KStuff pause: %s. Failed to update autotune delay.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s crashed after KStuff pause:\n%s\nFailed to update autotune delay.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Missing/invalid param.json:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Found %d new games. Processing...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Library synchronised.\nFound %d games.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config reloaded.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicate %s ignored:\n%s\nexisting: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR emulator updated",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: failed to install ShellCore hooks",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Target temperature %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "External drive %s disconnected",
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
static const char *const g_en_us[SM_L10N_COUNT] = {
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Go to Debug Settings",
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor and Community",
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] =
|
||||
"ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2.",
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: scan depth %u enabled.",
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] =
|
||||
"Backport overlay failed: %s\n%s\n0x%08X",
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport applied to game: %s",
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] =
|
||||
"Backport applied to game: %s\nEmulators updated",
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] =
|
||||
"Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\n"
|
||||
"Saved image_sector override: %u.\nTry mounting again.",
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] =
|
||||
"Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\n"
|
||||
"Use a larger cluster size in the image or lower sector size in config.",
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] =
|
||||
"Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] =
|
||||
"Register failed: %s (%s)\nTitleDir bridge unavailable",
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Register failed: %s (%s)\ncode=0x%08X",
|
||||
[SM_L10N_INSTALLING] = "Installing: %s (%s)...",
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff active again",
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff paused while the game is running",
|
||||
[SM_L10N_INSTALLED] = "Installed: %s",
|
||||
[SM_L10N_UNKNOWN] = "Unknown",
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Image mount failed: 0x%08X (%s)\n%s",
|
||||
[SM_L10N_FS_CLUSTER_SMALL] =
|
||||
"Filesystem cluster size (%llu) is smaller than device sector size "
|
||||
"(%u) for %s",
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] =
|
||||
"Mounted image is unreadable or damaged",
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] =
|
||||
"Image cache full (%u entries), cannot track mount %s",
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] =
|
||||
"Image cache full (%u entries), rolling back mount",
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] =
|
||||
"Install failed: %s (%s)\nTimed out after 5 minutes",
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batch install queued (%d):",
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... and %d more",
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] =
|
||||
"Batch install failed for %d app(s).\ncode=0x%08X",
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] =
|
||||
"%s crashed before KStuff was paused. KStuff is not the cause.",
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] =
|
||||
"%s crashed before KStuff pause:\n%s",
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] =
|
||||
"%s crashed after KStuff was paused. Automatic tuning skipped.",
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] =
|
||||
"%s crashed after KStuff pause:\n%s\nAutotune skipped.",
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] =
|
||||
"Can't load module after KStuff pause",
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] =
|
||||
"can't load module %.*s after KStuff pause",
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] =
|
||||
"%s: %s. Delay increased to %us.\nLaunch the game again.",
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] =
|
||||
"Crash detected after KStuff pause: pause delay for %s increased to "
|
||||
"%us. Launch the game again.",
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] =
|
||||
"%s crashed after KStuff pause:\n%s\nDelay increased to %us.\n"
|
||||
"Launch the game again.",
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] =
|
||||
"Crash detected after KStuff pause: %s. Failed to update autotune delay.",
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] =
|
||||
"%s crashed after KStuff pause:\n%s\nFailed to update autotune delay.",
|
||||
[SM_L10N_MISSING_PARAM] = "Missing/invalid param.json:\n%s",
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Found %d new games. Processing...",
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Library synchronized.\nFound %d games.",
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config reloaded.",
|
||||
[SM_L10N_DUPLICATE_IGNORED] =
|
||||
"Duplicate %s ignored:\n%s\nexisting: %s",
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR emulator updated",
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] =
|
||||
"ShadowMount+: failed to install ShellCore hooks",
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] =
|
||||
"Target temperature %u °C → %u °C",
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] =
|
||||
"External drive %s connected\n"
|
||||
"Free %llu.%llu GB of %llu.%llu (Block size %llu KB)\n"
|
||||
"Games on drive: %d",
|
||||
[SM_L10N_USB_DISCONNECTED] = "External drive %s disconnected",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_es_es[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir a ajustes de depuración",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor y la comunidad",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, se usará scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: profundidad de escaneo %u activada.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Error en overlay de backport: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Juego con backport aplicado: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Juego con backport aplicado: %s\nEmuladores actualizados",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montaje de imagen rechazado:\n%s\nEl tamaño de clúster es demasiado pequeño (%llu < %u).\nSe guardó el valor de image_sector: %u.\nIntente montar de nuevo.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montaje de imagen rechazado:\n%s\nEl tamaño del clúster es demasiado pequeño (%llu < %u).\nUse un clúster mayor en la imagen o un tamaño de sector menor en la configuración.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Error al registrar: %s (%s)\nAppInstallTitleDir no disponible",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Error de registro: %s (%s)\nEl puente TitleDir no está disponible",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Error al registrar: %s (%s)\ncódigo=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Instalando: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff activo de nuevo",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pausado mientras el juego está en ejecución",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Instalado: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Desconocido",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Error al montar imagen: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "El tamaño de clúster del sistema de archivos (%llu) es menor que el tamaño de sector del dispositivo (%u) para %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "La imagen montada es ilegible o está dañada",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "La caché de imágenes está llena (%u entradas); no se puede supervisar el montaje %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "La caché de imágenes está llena (%u entradas); se deshará el montaje",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Error de instalación: %s (%s)\nTiempo agotado tras 5 minutos",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalación por lotes en cola (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... y %d más",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "La instalación por lotes falló para %d aplicaciones.\ncódigo=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s se bloqueó antes de pausar KStuff. KStuff no es la causa.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s se bloqueó antes de pausar KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s se bloqueó después de pausar KStuff. Se omitió el ajuste automático.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s se bloqueó después de pausar KStuff:\n%s\nSe omitió el ajuste automático.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "No se puede cargar el módulo tras la pausa de KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "no se puede cargar el módulo %.*s después de pausar KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Retardo aumentado a %u s.\nInicie el juego de nuevo.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Se detectó un bloqueo después de pausar KStuff: el retardo de pausa de %s aumentó a %u s. Inicie el juego de nuevo.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s se bloqueó después de pausar KStuff:\n%s\nEl retraso aumentó a %u s.\nInicie el juego de nuevo.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Se detectó un bloqueo después de pausar KStuff: %s. No se pudo actualizar el retardo del ajuste automático.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s se bloqueó después de pausar KStuff:\n%s\nNo se pudo actualizar el retraso del ajuste automático.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json ausente o no válido:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Se encontraron %d juegos nuevos. Procesando...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Biblioteca sincronizada.\nSe encontraron %d juegos.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configuración recargada.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicado %s ignorado:\n%s\nexistente: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulador AMPR actualizado",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: no se pudieron instalar los hooks de ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura objetivo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidad externa %s conectada.\nLibres: %llu.%llu de %llu.%llu GB (bloque %llu KB)\nJuegos en la unidad: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidad externa %s desconectada",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_es_mx[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir a configuración de depuración",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor y la comunidad",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, se usará scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: profundidad de escaneo %u activada.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Falló el overlay de backport: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport aplicado al juego: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport aplicado al juego: %s\nEmuladores actualizados",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montaje de imagen rechazado:\n%s\nEl tamaño de clúster es demasiado pequeño (%llu < %u).\nSe guardó el valor de image_sector: %u.\nIntenta montar de nuevo.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montaje de imagen rechazado:\n%s\nEl tamaño del clúster es demasiado pequeño (%llu < %u).\nUsa un clúster más grande en la imagen o un tamaño de sector menor en la configuración.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Falló el registro: %s (%s)\nAppInstallTitleDir no disponible",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Error de registro: %s (%s)\nEl puente TitleDir no está disponible",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Falló el registro: %s (%s)\ncódigo=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Instalando: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff activo otra vez",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pausado mientras el juego está corriendo",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Instalado: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Desconocido",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Falló el montaje de imagen: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "El tamaño de clúster del sistema de archivos (%llu) es menor que el tamaño de sector del dispositivo (%u) para %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "La imagen montada no se puede leer o está dañada",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "La caché de imágenes está llena (%u entradas); no se puede supervisar el montaje %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "La caché de imágenes está llena (%u entradas); se deshará el montaje",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Falló la instalación: %s (%s)\nTiempo agotado después de 5 minutos",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalación por lotes en cola (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... y %d más",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "La instalación por lotes falló para %d aplicaciones.\ncódigo=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s falló antes de pausar KStuff. KStuff no es la causa.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s falló antes de pausar KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s falló después de pausar KStuff. Se omitió el ajuste automático.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s falló después de pausar KStuff:\n%s\nSe omitió el ajuste automático.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "No se puede cargar el módulo después de la pausa de KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "no se puede cargar el módulo %.*s después de pausar KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Retardo aumentado a %u s.\nInicia el juego de nuevo.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Se detectó una falla después de pausar KStuff: el retraso de pausa de %s aumentó a %u s. Inicia el juego de nuevo.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s falló después de pausar KStuff:\n%s\nEl retraso aumentó a %u s.\nInicia el juego de nuevo.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Se detectó una falla después de pausar KStuff: %s. No se pudo actualizar el retraso del ajuste automático.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s falló después de pausar KStuff:\n%s\nNo se pudo actualizar el retraso del ajuste automático.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json faltante o no válido:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Se encontraron %d juegos nuevos. Procesando...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Biblioteca sincronizada.\nSe encontraron %d juegos.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configuración recargada.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicado %s ignorado:\n%s\nexistente: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulador AMPR actualizado",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: no se pudieron instalar los hooks de ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura objetivo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidad externa %s conectada.\nLibres: %llu.%llu de %llu.%llu GB (bloque %llu KB)\nJuegos en la unidad: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidad externa %s desconectada",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_fi_fi[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Siirry virheenkorjausasetuksiin",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor ja yhteisö",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 on vanhentunut, käytetään scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: skannaussyvyys %u käytössä.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-overlay epäonnistui: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Peliin käytettiin backportia: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Peliin käytettiin backportia: %s\nEmulaattorit päivitetty",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Imagen liittäminen hylätty:\n%s\nKlusterikoko on liian pieni (%llu < %u).\nimage_sector-ohitus tallennettu: %u.\nYritä liittää uudelleen.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Levykuvan liittäminen hylättiin:\n%s\nKlusterikoko on liian pieni (%llu < %u).\nKäytä levykuvassa suurempaa klusteria tai määrityksissä pienempää sektorikokoa.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Rekisteröinti epäonnistui: %s (%s)\nAppInstallTitleDir ei ole käytettävissä",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Rekisteröinti epäonnistui: %s (%s)\nTitleDir-silta ei ole käytettävissä",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Rekisteröinti epäonnistui: %s (%s)\nkoodi=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Asennetaan: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff on taas aktiivinen",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff keskeytetty pelin ollessa käynnissä",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Asennettu: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Tuntematon",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Imagen liittäminen epäonnistui: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Tiedostojärjestelmän klusterikoko (%llu) on pienempi kuin laitteen sektorikoko (%u) kohteelle %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Liitetty levykuva ei ole luettavissa tai se on vioittunut",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Levykuvavälimuisti on täynnä (%u merkintää), liittämistä %s ei voi seurata",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Levykuvavälimuisti on täynnä (%u merkintää), liittäminen perutaan",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Asennus epäonnistui: %s (%s)\nAikakatkaisu 5 minuutin jälkeen",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Eräasennus jonossa (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... ja %d lisää",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Eräasennus epäonnistui %d sovellukselle.\nkoodi=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Sovellus %s kaatui ennen KStuffin keskeytystä. KStuff ei ole syyllinen.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s kaatui ennen KStuffin keskeytystä:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Sovellus %s kaatui KStuffin keskeytyksen jälkeen. Automaattinen säätö ohitettiin.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s kaatui KStuffin keskeytyksen jälkeen:\n%s\nAutomaattinen säätö ohitettiin.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Moduulia ei voi ladata KStuff-tauon jälkeen",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "moduulia %.*s ei voi ladata KStuffin keskeytyksen jälkeen",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Viive nostettu arvoon %u s.\nKäynnistä peli uudelleen.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "KStuffin keskeytyksen jälkeen havaittiin kaatuminen: kohteen %s viive nostettiin arvoon %u s. Käynnistä peli uudelleen.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s kaatui KStuffin keskeytyksen jälkeen:\n%s\nViive nostettiin arvoon %u s.\nKäynnistä peli uudelleen.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "KStuffin keskeytyksen jälkeen havaittiin kaatuminen: %s. Automaattisen säädön viivettä ei voitu päivittää.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s kaatui KStuffin keskeytyksen jälkeen:\n%s\nAutomaattisen säädön viivettä ei voitu päivittää.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json puuttuu tai on virheellinen:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Löytyi %d uutta peliä. Käsitellään...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Kirjasto synkronoitu.\nLöytyi %d peliä.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: määritykset ladattiin uudelleen.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Kaksoiskappale %s ohitettu:\n%s\nolemassa: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR-emulaattori päivitetty",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore-hookkien asennus epäonnistui",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Tavoitelämpötila %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Ulkoinen asema %s yhdistetty.\nVapaana: %llu.%llu / %llu.%llu Gt (lohko %llu kt)\nPelejä asemalla: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Ulkoinen asema %s irrotettu",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_fr_ca[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Accéder aux paramètres de débogage",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor et la communauté",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+ : recursive_scan=1 est obsolète, scan_depth=2 sera utilisé.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+ : profondeur d’analyse %u activée.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Échec du montage de la surcouche de backport : %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport appliqué au jeu : %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport appliqué au jeu : %s\nÉmulateurs mis à jour",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montage de l’image refusé :\n%s\nLa taille des grappes est trop petite (%llu < %u).\nValeur image_sector enregistrée : %u.\nRéessayez de monter l’image.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montage de l’image refusé :\n%s\nLa taille des grappes est trop petite (%llu < %u).\nUtilisez des grappes plus grandes dans l’image ou une taille de secteur plus petite dans la configuration.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Échec de l'enregistrement : %s (%s)\nAppInstallTitleDir indisponible",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Échec de l’enregistrement : %s (%s)\nLe pont TitleDir n’est pas disponible",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Échec de l'enregistrement : %s (%s)\ncode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installation : %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff est de nouveau actif",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff mis en pause pendant l'exécution du jeu",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installé : %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Inconnu",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Échec du montage de l'image : 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "La taille des grappes du système de fichiers (%llu) est inférieure à la taille des secteurs du périphérique (%u) pour %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "L'image montée est illisible ou endommagée",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Le cache d’images est plein (%u entrées), impossible de suivre le montage %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Le cache d’images est plein (%u entrées), annulation du montage",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Échec de l'installation : %s (%s)\nDélai expiré après 5 minutes",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Installation par lot mise en file (%d) :",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... et %d de plus",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Échec de l'installation par lot pour %d application(s).\ncode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s a planté avant la mise en pause de KStuff. KStuff n’est pas en cause.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s a planté avant la mise en pause de KStuff :\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s a planté après la mise en pause de KStuff. Réglage automatique ignoré.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s a planté après la mise en pause de KStuff :\n%s\nRéglage automatique ignoré.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Impossible de charger le module après la pause KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "impossible de charger le module %.*s après la mise en pause de KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s : %s. Délai augmenté à %u s.\nRelancez le jeu.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Plantage détecté après la mise en pause de KStuff : le délai de pause de %s a été porté à %u s. Relancez le jeu.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s a planté après la mise en pause de KStuff :\n%s\nDélai augmenté à %u s.\nRelancez le jeu.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Plantage détecté après la mise en pause de KStuff : %s. Impossible de mettre à jour le délai du réglage automatique.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s a planté après la mise en pause de KStuff :\n%s\nImpossible de mettre à jour le délai du réglage automatique.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json manquant ou invalide :\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d nouveaux jeux trouvés. Traitement en cours...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliothèque synchronisée.\n%d jeux trouvés.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+ : configuration rechargée.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Doublon %s ignoré :\n%s\nexistant : %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Émulateur AMPR mis à jour",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+ : échec de l’installation des hooks ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Température cible %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Lecteur externe %s connecté.\nLibre : %llu.%llu sur %llu.%llu Go (bloc %llu Ko)\nJeux sur le lecteur : %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Lecteur externe %s déconnecté",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_fr_fr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Accéder aux paramètres de débogage",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor et la communauté",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+ : recursive_scan=1 est obsolète, scan_depth=2 sera utilisé.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+ : profondeur d’analyse %u activée.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Échec du montage de la surcouche de backport : %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport appliqué au jeu : %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport appliqué au jeu : %s\nÉmulateurs mis à jour",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montage de l’image refusé :\n%s\nLa taille des clusters est trop petite (%llu < %u).\nValeur image_sector enregistrée : %u.\nRéessayez de monter l’image.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montage de l’image refusé :\n%s\nLa taille des clusters est trop petite (%llu < %u).\nUtilisez des clusters plus grands dans l’image ou une taille de secteur plus petite dans la configuration.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Échec de l'enregistrement : %s (%s)\nAppInstallTitleDir indisponible",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Échec de l’enregistrement : %s (%s)\nLe pont TitleDir n’est pas disponible",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Échec de l'enregistrement : %s (%s)\ncode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installation : %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff est de nouveau actif",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff mis en pause pendant l'exécution du jeu",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installé : %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Inconnu",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Échec du montage de l'image : 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "La taille des clusters du système de fichiers (%llu) est inférieure à la taille des secteurs du périphérique (%u) pour %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "L'image montée est illisible ou endommagée",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Le cache d’images est plein (%u entrées), impossible de suivre le montage %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Le cache d’images est plein (%u entrées), annulation du montage",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Échec de l'installation : %s (%s)\nDélai expiré après 5 minutes",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Installation par lot mise en file (%d) :",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... et %d de plus",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Échec de l'installation par lot pour %d application(s).\ncode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s a planté avant la mise en pause de KStuff. KStuff n’est pas en cause.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s a planté avant la mise en pause de KStuff :\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s a planté après la mise en pause de KStuff. Réglage automatique ignoré.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s a planté après la mise en pause de KStuff :\n%s\nRéglage automatique ignoré.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Impossible de charger le module après la pause KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "impossible de charger le module %.*s après la mise en pause de KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s : %s. Délai augmenté à %u s.\nRelancez le jeu.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Plantage détecté après la mise en pause de KStuff : le délai de pause de %s a été porté à %u s. Relancez le jeu.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s a planté après la mise en pause de KStuff :\n%s\nDélai augmenté à %u s.\nRelancez le jeu.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Plantage détecté après la mise en pause de KStuff : %s. Impossible de mettre à jour le délai du réglage automatique.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s a planté après la mise en pause de KStuff :\n%s\nImpossible de mettre à jour le délai du réglage automatique.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json manquant ou invalide :\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d nouveaux jeux trouvés. Traitement en cours...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliothèque synchronisée.\n%d jeux trouvés.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+ : configuration rechargée.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Doublon %s ignoré :\n%s\nexistant : %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Émulateur AMPR mis à jour",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+ : échec de l’installation des hooks ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Température cible %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Lecteur externe %s connecté.\nLibre : %llu.%llu sur %llu.%llu Go (bloc %llu Ko)\nJeux sur le lecteur : %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Lecteur externe %s déconnecté",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_hu_hu[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ugrás a hibakeresési beállításokhoz",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor és a közösség",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: a recursive_scan=1 elavult, scan_depth=2 lesz használva.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: %u keresési mélység engedélyezve.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay sikertelen: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Játék backportolva: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Játék backportolva: %s\nEmulátorok frissítve",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "A lemezkép csatolása elutasítva:\n%s\nA klaszter mérete túl kicsi (%llu < %u).\nAz image_sector felülírása mentve: %u.\nPróbáld újra csatolni.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "A lemezkép csatolása elutasítva:\n%s\nA klaszter mérete túl kicsi (%llu < %u).\nHasználj nagyobb klasztert a lemezképben, vagy kisebb szektorméretet a beállításokban.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Regisztráció sikertelen: %s (%s)\nAppInstallTitleDir nem elérhető",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "A regisztráció sikertelen: %s (%s)\nA TitleDir-híd nem érhető el",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Regisztráció sikertelen: %s (%s)\nkód=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Telepítés: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "A KStuff újra aktív",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "A KStuff szünetel, amíg a játék fut",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Telepítve: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Ismeretlen",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "A lemezkép csatolása sikertelen: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "A fájlrendszer cluster mérete (%llu) kisebb, mint az eszköz szektormérete (%u): %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "A mountolt image olvashatatlan vagy sérült",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "A lemezkép-gyorsítótár megtelt (%u bejegyzés), a(z) %s csatolás nem követhető",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "A lemezkép-gyorsítótár megtelt (%u bejegyzés), a csatolás visszavonása folyamatban",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Telepítés sikertelen: %s (%s)\nIdőtúllépés 5 perc után",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Kötegelt telepítés sorban (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... és még %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "A kötegelt telepítés %d alkalmazásnál sikertelen.\nkód=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "A(z) %s összeomlott a KStuff szüneteltetése előtt. Nem a KStuff a hibás.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "A(z) %s összeomlott a KStuff szüneteltetése előtt:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "A(z) %s összeomlott a KStuff szüneteltetése után. Az automatikus hangolás kimaradt.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "A(z) %s összeomlott a KStuff szüneteltetése után:\n%s\nAz automatikus hangolás kihagyva.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "A modul nem tölthető be a KStuff szünet után",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "a(z) %.*s modul nem tölthető be a KStuff szüneteltetése után",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Késleltetés %u s értékre növelve.\nIndítsd újra a játékot.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Összeomlás történt a KStuff szüneteltetése után: a(z) %s késleltetése %u másodpercre nőtt. Indítsd újra a játékot.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "A(z) %s összeomlott a KStuff szüneteltetése után:\n%s\nA késleltetés %u másodpercre nőtt.\nIndítsd újra a játékot.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Összeomlás történt a KStuff szüneteltetése után: %s. Az automatikus hangolás késleltetése nem frissíthető.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "A(z) %s összeomlott a KStuff szüneteltetése után:\n%s\nAz automatikus hangolás késleltetése nem frissíthető.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Hiányzó vagy érvénytelen param.json:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d új játék található. Feldolgozás...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Könyvtár szinkronizálva.\n%d játék található.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: a beállítások újratöltve.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplikált %s kihagyva:\n%s\nlétező: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Az AMPR-emulátor frissítve",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: a ShellCore hookok telepítése sikertelen",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Célhőmérséklet %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Külső meghajtó csatlakoztatva: %s.\nSzabad: %llu.%llu / %llu.%llu GB (blokkméret %llu KB)\nJátékok a meghajtón: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Külső meghajtó leválasztva: %s",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_id_id[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Buka pengaturan debug",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor dan Komunitas",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 sudah usang, memakai scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: kedalaman pemindaian %u aktif.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay gagal: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport game diterapkan: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport game diterapkan: %s\nSemua emulator diperbarui",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Pemasangan image ditolak:\n%s\nUkuran cluster terlalu kecil (%llu < %u).\nNilai image_sector disimpan: %u.\nCoba pasang lagi.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Pemasangan image ditolak:\n%s\nUkuran cluster terlalu kecil (%llu < %u).\nGunakan cluster yang lebih besar pada image atau ukuran sektor yang lebih kecil pada konfigurasi.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrasi gagal: %s (%s)\nAppInstallTitleDir tidak tersedia",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registrasi gagal: %s (%s)\nBridge TitleDir tidak tersedia",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrasi gagal: %s (%s)\nkode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Menginstal: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff aktif lagi",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff dijeda saat game berjalan",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Terinstal: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Tidak diketahui",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Pemasangan image gagal: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Ukuran cluster filesystem (%llu) lebih kecil dari ukuran sector perangkat (%u) untuk %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Image yang dimount tidak terbaca atau rusak",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Cache image penuh (%u entri), pemasangan %s tidak dapat dilacak",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache image penuh (%u entri), pemasangan dibatalkan",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Instalasi gagal: %s (%s)\nWaktu habis setelah 5 menit",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalasi batch masuk antrean (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... dan %d lagi",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Instalasi batch gagal untuk %d aplikasi.\nkode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Aplikasi %s berhenti mendadak sebelum KStuff dijeda. KStuff bukan penyebabnya.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s berhenti mendadak sebelum KStuff dijeda:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Aplikasi %s berhenti mendadak setelah KStuff dijeda. Penyetelan otomatis dilewati.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s berhenti mendadak setelah KStuff dijeda:\n%s\nPenyetelan otomatis dilewati.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Tidak dapat memuat module setelah KStuff dijeda",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "tidak dapat memuat modul %.*s setelah KStuff dijeda",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Penundaan ditingkatkan menjadi %u dtk.\nJalankan game lagi.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Aplikasi berhenti mendadak setelah KStuff dijeda: penundaan untuk %s ditingkatkan menjadi %u dtk. Jalankan game lagi.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s berhenti mendadak setelah KStuff dijeda:\n%s\nPenundaan ditingkatkan menjadi %u dtk.\nJalankan game lagi.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Aplikasi berhenti mendadak setelah KStuff dijeda: %s. Gagal memperbarui penundaan penyetelan otomatis.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s berhenti mendadak setelah KStuff dijeda:\n%s\nGagal memperbarui penundaan penyetelan otomatis.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json hilang atau tidak valid:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Menemukan %d game baru. Memproses...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Pustaka disinkronkan.\nMenemukan %d game.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: konfigurasi dimuat ulang.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s diabaikan:\n%s\nyang ada: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulator AMPR diperbarui",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: gagal memasang hook ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Suhu target %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Drive eksternal %s tersambung.\nKosong: %llu.%llu dari %llu.%llu GB (ukuran blok %llu KB)\nGame di drive: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Drive eksternal %s terputus",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_it_it[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Vai alle impostazioni di debug",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor e la community",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 è deprecato; verrà usato scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: profondità di scansione %u abilitata.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Overlay backport non riuscito: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport applicato al gioco: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport applicato al gioco: %s\nEmulatori aggiornati",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montaggio immagine rifiutato:\n%s\nLa dimensione cluster è troppo piccola (%llu < %u).\nOverride image_sector salvato: %u.\nProva a montare di nuovo.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montaggio dell’immagine rifiutato:\n%s\nLa dimensione del cluster è troppo piccola (%llu < %u).\nUsa un cluster più grande nell’immagine o una dimensione del settore più piccola nella configurazione.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrazione non riuscita: %s (%s)\nAppInstallTitleDir non disponibile",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registrazione non riuscita: %s (%s)\nBridge TitleDir non disponibile",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrazione non riuscita: %s (%s)\ncodice=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installazione: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff di nuovo attivo",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff in pausa mentre il gioco è in esecuzione",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installato: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Sconosciuto",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Montaggio immagine non riuscito: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "La dimensione cluster del filesystem (%llu) è minore della dimensione settore del dispositivo (%u) per %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "L'immagine montata è illeggibile o danneggiata",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "La cache delle immagini è piena (%u voci), impossibile monitorare il montaggio %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "La cache delle immagini è piena (%u voci), annullamento del montaggio",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Installazione non riuscita: %s (%s)\nTimeout dopo 5 minuti",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Installazione batch in coda (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... e altri %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Installazione batch non riuscita per %d app.\ncodice=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s si è arrestata prima della pausa di KStuff. KStuff non è responsabile.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s si è arrestato prima della pausa di KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s si è arrestata dopo la pausa di KStuff. Regolazione automatica ignorata.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s si è arrestato dopo la pausa di KStuff:\n%s\nRegolazione automatica ignorata.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Impossibile caricare il modulo dopo la pausa KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "impossibile caricare il modulo %.*s dopo la pausa di KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Ritardo aumentato a %u s.\nAvvia di nuovo il gioco.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Arresto anomalo rilevato dopo la pausa di KStuff: il ritardo di pausa per %s è stato aumentato a %u s. Avvia di nuovo il gioco.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s si è arrestato dopo la pausa di KStuff:\n%s\nRitardo aumentato a %u s.\nAvvia di nuovo il gioco.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Arresto anomalo rilevato dopo la pausa di KStuff: %s. Impossibile aggiornare il ritardo della regolazione automatica.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s si è arrestato dopo la pausa di KStuff:\n%s\nImpossibile aggiornare il ritardo della regolazione automatica.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json mancante o non valido:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Trovati %d nuovi giochi. Elaborazione in corso...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Libreria sincronizzata.\nTrovati %d giochi.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configurazione ricaricata.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicato %s ignorato:\n%s\nesistente: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulatore AMPR aggiornato",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: impossibile installare gli hook ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura obiettivo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unità esterna %s collegata.\nLiberi: %llu.%llu di %llu.%llu GB (blocco %llu KB)\nGiochi sull'unità: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unità esterna %s scollegata",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_ja_jp[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "デバッグ設定へ移動",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor とコミュニティ",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 は非推奨です。scan_depth=2 を使用します。",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: スキャン深度 %u が有効です。",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "バックポートオーバーレイのマウントに失敗しました: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "ゲームにバックポートを適用しました: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "ゲームにバックポートを適用しました: %s\n複数のエミュレーターを更新しました",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "イメージのマウントが拒否されました:\n%s\nクラスタサイズが小さすぎます (%llu < %u)。\nimage_sector override を保存しました: %u。\nもう一度マウントしてください。",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "イメージのマウントが拒否されました:\n%s\nクラスタサイズが小さすぎます (%llu < %u)。\nイメージのクラスタサイズを大きくするか、設定のセクタサイズを小さくしてください。",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "登録に失敗しました: %s (%s)\nAppInstallTitleDir が利用できません",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "登録に失敗しました: %s (%s)\nTitleDir ブリッジを利用できません",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "登録に失敗しました: %s (%s)\nコード=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "インストール中: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff が再び有効です",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "ゲーム実行中のため KStuff を一時停止しました",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "インストール済み: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "不明",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "イメージのマウントに失敗しました: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "ファイルシステムのクラスタサイズ (%llu) がデバイスセクタサイズ (%u) より小さいです: %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "マウント済みイメージを読み取れないか破損しています",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "イメージキャッシュが満杯です (%u 件)。マウント %s を追跡できません",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "イメージキャッシュが満杯です (%u 件)。マウントを元に戻します",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "インストールに失敗しました: %s (%s)\n5 分後にタイムアウトしました",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "一括インストールをキューに追加しました (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... 他 %d 件",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "%d 個のアプリの一括インストールに失敗しました。\nコード=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s は KStuff の一時停止前にクラッシュしました。KStuff が原因ではありません。",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s は KStuff の一時停止前にクラッシュしました:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s は KStuff の一時停止後にクラッシュしました。自動調整をスキップしました。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s は KStuff の一時停止後にクラッシュしました:\n%s\n自動調整をスキップしました。",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "KStuff 一時停止後にモジュールを読み込めません",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "KStuff の一時停止後にモジュール %.*s を読み込めません",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s。遅延を %u 秒に増やしました。\nゲームをもう一度起動してください。",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "KStuff の一時停止後にクラッシュを検出しました: %s の遅延を %u 秒に増やしました。ゲームをもう一度起動してください。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s は KStuff の一時停止後にクラッシュしました:\n%s\n遅延を %u 秒に増やしました。\nゲームをもう一度起動してください。",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "KStuff の一時停止後にクラッシュを検出しました: %s。自動調整の遅延を更新できませんでした。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s は KStuff の一時停止後にクラッシュしました:\n%s\n自動調整の遅延を更新できませんでした。",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json が見つからないか無効です:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d 本の新しいゲームを検出しました。処理中...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "ライブラリを同期しました。\n%d 本のゲームを検出しました。",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: 設定を再読み込みしました。",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "重複 %s を無視しました:\n%s\n既存: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPRエミュレーターを更新しました",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCoreフックのインストールに失敗しました",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "目標温度 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "外付けドライブ %s が接続されました。\n空き容量: %llu.%llu / %llu.%llu GB(ブロックサイズ: %llu KB)\nドライブ上のゲーム: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "外付けドライブ %s が切断されました",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_ko_kr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "디버그 설정으로 이동",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor 및 커뮤니티",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1은 더 이상 권장되지 않으며 scan_depth=2를 사용합니다.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: 스캔 깊이 %u 활성화됨.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "백포트 오버레이 마운트 실패: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "게임에 백포트 적용됨: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "게임에 백포트 적용됨: %s\n에뮬레이터들이 업데이트됨",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "이미지 마운트 거부됨:\n%s\n클러스터 크기가 너무 작습니다 (%llu < %u).\nimage_sector override 저장됨: %u.\n다시 마운트해 보세요.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "이미지 마운트가 거부되었습니다:\n%s\n클러스터 크기가 너무 작습니다(%llu < %u).\n이미지의 클러스터 크기를 늘리거나 설정의 섹터 크기를 줄이세요.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "등록 실패: %s (%s)\nAppInstallTitleDir 사용 불가",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "등록 실패: %s (%s)\nTitleDir 브리지를 사용할 수 없음",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "등록 실패: %s (%s)\n코드=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "설치 중: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff 다시 활성화됨",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "게임 실행 중 KStuff 일시 중지됨",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "설치됨: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "알 수 없음",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "이미지 마운트 실패: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "파일 시스템 클러스터 크기(%llu)가 장치 섹터 크기(%u)보다 작습니다: %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "마운트된 이미지를 읽을 수 없거나 손상되었습니다",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "이미지 캐시가 가득 찼습니다(%u개). 마운트 %s을(를) 추적할 수 없습니다",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "이미지 캐시가 가득 찼습니다(%u개). 마운트를 되돌립니다",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "설치 실패: %s (%s)\n5분 후 시간 초과",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "일괄 설치 대기열 추가됨 (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... 외 %d개",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "%d개 앱 일괄 설치 실패.\n코드=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s이(가) KStuff 일시 중지 전에 충돌했습니다. KStuff가 원인이 아닙니다.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s이(가) KStuff 일시 중지 전에 충돌했습니다:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s이(가) KStuff 일시 중지 후에 충돌했습니다. 자동 조정을 건너뛰었습니다.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s이(가) KStuff 일시 중지 후에 충돌했습니다:\n%s\n자동 조정을 건너뛰었습니다.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "KStuff 일시 중지 후 모듈을 로드할 수 없습니다",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "KStuff 일시 중지 후 모듈 %.*s을(를) 로드할 수 없음",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. 지연이 %u초로 증가했습니다.\n게임을 다시 실행하세요.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "KStuff 일시 중지 후 충돌을 감지했습니다: %s의 지연을 %u초로 늘렸습니다. 게임을 다시 실행하세요.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s이(가) KStuff 일시 중지 후에 충돌했습니다:\n%s\n지연을 %u초로 늘렸습니다.\n게임을 다시 실행하세요.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "KStuff 일시 중지 후 충돌을 감지했습니다: %s. 자동 조정 지연을 업데이트하지 못했습니다.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s이(가) KStuff 일시 중지 후에 충돌했습니다:\n%s\n자동 조정 지연을 업데이트하지 못했습니다.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json 누락 또는 잘못됨:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "새 게임 %d개를 찾았습니다. 처리 중...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "라이브러리를 동기화했습니다.\n게임 %d개를 찾았습니다.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: 설정을 다시 불러왔습니다.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "중복 %s 무시됨:\n%s\n기존: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR 에뮬레이터가 업데이트되었습니다",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore 후크 설치에 실패했습니다",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "목표 온도 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "외장 드라이브 %s 연결됨.\n여유 공간: %llu.%llu / %llu.%llu GB (블록 크기: %llu KB)\n드라이브의 게임: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "외장 드라이브 %s 연결 해제됨",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_nl_nl[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ga naar debug-instellingen",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor en de community",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 is verouderd, scan_depth=2 wordt gebruikt.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: scandiepte %u ingeschakeld.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-overlay mislukt: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport op game toegepast: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport op game toegepast: %s\nEmulators bijgewerkt",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Koppelen van image geweigerd:\n%s\nDe clustergrootte is te klein (%llu < %u).\nimage_sector-waarde opgeslagen: %u.\nProbeer opnieuw te koppelen.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Koppelen van image geweigerd:\n%s\nDe clustergrootte is te klein (%llu < %u).\nGebruik een groter cluster in de image of een kleinere sectorgrootte in de configuratie.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registreren mislukt: %s (%s)\nAppInstallTitleDir niet beschikbaar",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registratie mislukt: %s (%s)\nTitleDir-bridge niet beschikbaar",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registreren mislukt: %s (%s)\ncode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installeren: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff weer actief",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff gepauzeerd terwijl de game draait",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Geïnstalleerd: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Onbekend",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Koppelen van image mislukt: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Clustergrootte van bestandssysteem (%llu) is kleiner dan apparaatsectorgrootte (%u) voor %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Gemounte image is onleesbaar of beschadigd",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Imagecache vol (%u vermeldingen), koppeling %s kan niet worden gevolgd",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Imagecache vol (%u vermeldingen), koppeling wordt ongedaan gemaakt",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Installatie mislukt: %s (%s)\nTime-out na 5 minuten",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batchinstallatie in wachtrij (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... en nog %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Batchinstallatie mislukt voor %d apps.\ncode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s is gecrasht voordat KStuff werd gepauzeerd. KStuff is niet de oorzaak.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s is gecrasht voordat KStuff werd gepauzeerd:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s is gecrasht nadat KStuff werd gepauzeerd. Automatische afstelling overgeslagen.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s is gecrasht nadat KStuff werd gepauzeerd:\n%s\nAutomatische afstelling overgeslagen.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Kan module niet laden na KStuff-pauze",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "kan module %.*s niet laden nadat KStuff is gepauzeerd",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Vertraging verhoogd naar %u s.\nStart de game opnieuw.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Vastloper gedetecteerd nadat KStuff werd gepauzeerd: de pauzevertraging voor %s is verhoogd naar %u s. Start de game opnieuw.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s is gecrasht nadat KStuff werd gepauzeerd:\n%s\nVertraging verhoogd naar %u s.\nStart de game opnieuw.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Vastloper gedetecteerd nadat KStuff werd gepauzeerd: %s. De vertraging voor automatische afstelling kon niet worden bijgewerkt.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s is gecrasht nadat KStuff werd gepauzeerd:\n%s\nKan de vertraging voor automatische afstelling niet bijwerken.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json ontbreekt of is ongeldig:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d nieuwe games gevonden. Verwerken...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliotheek gesynchroniseerd.\n%d games gevonden.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configuratie opnieuw geladen.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicaat %s genegeerd:\n%s\nbestaand: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR-emulator bijgewerkt",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: installeren van ShellCore-hooks mislukt",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Doeltemperatuur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Externe schijf %s aangesloten.\nVrij: %llu.%llu van %llu.%llu GB (blokgrootte %llu KB)\nGames op de schijf: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Externe schijf %s losgekoppeld",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_no_no[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Gå til feilsøkingsinnstillinger",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor og fellesskapet",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 er utdatert, bruker scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: skannedybde %u aktivert.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-overlay mislyktes: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport brukt på spillet: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport brukt på spillet: %s\nEmulatorer oppdatert",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montering av image avvist:\n%s\nKlyngestørrelsen er for liten (%llu < %u).\nimage_sector-overstyring lagret: %u.\nPrøv å montere igjen.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montering av image avvist:\n%s\nKlyngestørrelsen er for liten (%llu < %u).\nBruk en større klyngestørrelse i imaget eller en mindre sektorstørrelse i konfigurasjonen.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrering mislyktes: %s (%s)\nAppInstallTitleDir utilgjengelig",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registrering mislyktes: %s (%s)\nTitleDir-broen er ikke tilgjengelig",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrering mislyktes: %s (%s)\nkode=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installerer: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff aktiv igjen",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pauset mens spillet kjører",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installert: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Ukjent",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Montering av image mislyktes: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Filsystemets klyngestørrelse (%llu) er mindre enn enhetens sektorstørrelse (%u) for %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Mountet image er uleselig eller skadet",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Image-cachen er full (%u oppføringer), monteringen %s kan ikke spores",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-cachen er full (%u oppføringer), monteringen rulles tilbake",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Installasjon mislyktes: %s (%s)\nTidsavbrudd etter 5 minutter",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batchinstallasjon i kø (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... og %d til",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Batchinstallasjonen mislyktes for %d apper.\nkode=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Appen %s krasjet før KStuff ble satt på pause. KStuff er ikke årsaken.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s krasjet før KStuff ble satt på pause:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Appen %s krasjet etter at KStuff ble satt på pause. Automatisk justering ble hoppet over.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s krasjet etter at KStuff ble satt på pause:\n%s\nAutomatisk justering ble hoppet over.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Kan ikke laste modul etter KStuff-pause",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "kan ikke laste modulen %.*s etter KStuff-pausen",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Forsinkelse økt til %u s.\nStart spillet igjen.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Krasj oppdaget etter KStuff-pausen: pauseforsinkelsen for %s ble økt til %u s. Start spillet på nytt.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s krasjet etter at KStuff ble satt på pause:\n%s\nForsinkelsen ble økt til %u s.\nStart spillet på nytt.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Krasj oppdaget etter KStuff-pausen: %s. Forsinkelsen for automatisk justering kunne ikke oppdateres.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s krasjet etter at KStuff ble satt på pause:\n%s\nKunne ikke oppdatere forsinkelsen for automatisk justering.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json mangler eller er ugyldig:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Fant %d nye spill. Behandler...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliotek synkronisert.\nFant %d spill.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: konfigurasjonen ble lastet inn på nytt.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s ignorert:\n%s\neksisterende: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR-emulator oppdatert",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: kunne ikke installere ShellCore-hooks",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Måltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Ekstern disk %s tilkoblet.\nLedig: %llu.%llu av %llu.%llu GB (blokkstørrelse %llu KB)\nSpill på disken: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Ekstern disk %s frakoblet",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_pl_pl[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Przejdź do ustawień debugowania",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor i społeczność",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 jest przestarzałe, zastosowano scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: włączono głębokość skanowania %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Nakładka backportu nie powiodła się: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Zastosowano backport gry: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Zastosowano backport gry: %s\nEmulatory zaktualizowane",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Odrzucono montowanie obrazu:\n%s\nRozmiar klastra jest za mały (%llu < %u).\nZastosowano nadpisanie image_sector: %u.\nSpróbuj zamontować ponownie.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Odrzucono montowanie obrazu:\n%s\nRozmiar klastra jest za mały (%llu < %u).\nUżyj większego rozmiaru klastra w obrazie lub mniejszego rozmiaru sektora w konfiguracji.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Rejestracja nie powiodła się: %s (%s)\nAppInstallTitleDir niedostępne",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Rejestracja nie powiodła się: %s (%s)\nMost TitleDir jest niedostępny",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Rejestracja nie powiodła się: %s (%s)\nkod=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Instalowanie: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff ponownie aktywny",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff wstrzymany podczas działania gry",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Zainstalowano: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Nieznane",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Montowanie obrazu nie powiodło się: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Rozmiar klastra systemu plików (%llu) jest mniejszy niż rozmiar sektora urządzenia (%u) dla %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Zamontowany obraz jest nieczytelny lub uszkodzony",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Pamięć podręczna obrazów jest pełna (%u wpisów), nie można śledzić montowania %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Pamięć podręczna obrazów jest pełna (%u wpisów), montowanie zostanie wycofane",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Instalacja nie powiodła się: %s (%s)\nPrzekroczono limit czasu po 5 minutach",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalacja zbiorcza dodana do kolejki (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... i jeszcze %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Instalacja zbiorcza nie powiodła się dla %d aplikacji.\nkod=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Aplikacja %s uległa awarii przed wstrzymaniem KStuff. KStuff nie jest przyczyną.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Aplikacja %s uległa awarii przed wstrzymaniem KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Aplikacja %s uległa awarii po wstrzymaniu KStuff. Pominięto automatyczne dostrajanie.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Aplikacja %s uległa awarii po wstrzymaniu KStuff:\n%s\nPominięto automatyczne dostrajanie.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Nie można załadować modułu po wstrzymaniu KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "nie można załadować modułu %.*s po wstrzymaniu KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Opóźnienie zwiększono do %u s.\nUruchom grę ponownie.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Wykryto awarię po wstrzymaniu KStuff: opóźnienie dla %s zwiększono do %u s. Uruchom grę ponownie.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "Aplikacja %s uległa awarii po wstrzymaniu KStuff:\n%s\nOpóźnienie zwiększono do %u s.\nUruchom grę ponownie.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Wykryto awarię po wstrzymaniu KStuff: %s. Nie udało się zaktualizować opóźnienia automatycznego dostrajania.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "Aplikacja %s uległa awarii po wstrzymaniu KStuff:\n%s\nNie udało się zaktualizować opóźnienia automatycznego dostrajania.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Brak pliku param.json lub plik jest nieprawidłowy:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Znaleziono %d nowych gier. Przetwarzanie...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Biblioteka zsynchronizowana.\nZnaleziono %d gier.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: konfiguracja przeładowana.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s zignorowany:\n%s\nistniejący: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulator AMPR został zaktualizowany",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: nie udało się zainstalować hooków ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura docelowa %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Dysk zewnętrzny %s podłączony.\nWolne: %llu.%llu z %llu.%llu GB (rozmiar bloku %llu KB)\nGry na dysku: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Dysk zewnętrzny %s odłączony",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_pt_br[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir para as Configurações de Depuração",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor e a comunidade",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, usando scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: profundidade de varredura %u ativada.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Falha no overlay de backport: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport aplicado ao jogo: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport aplicado ao jogo: %s\nEmuladores atualizados",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montagem da imagem rejeitada:\n%s\nO tamanho do cluster é pequeno demais (%llu < %u).\nNovo valor para o parâmetro image_sector salvo: %u.\nTente montar novamente.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montagem da imagem rejeitada:\n%s\nO tamanho do cluster é pequeno demais (%llu < %u).\nUse um cluster maior na imagem ou um tamanho de setor menor na configuração.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Falha no registro: %s (%s)\nAppInstallTitleDir indisponível",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Falha no registro: %s (%s)\nA ponte TitleDir não está disponível",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registro falhou: %s (%s)\ncódigo=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Instalando: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff ativo novamente",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pausado enquanto o jogo está em execução",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Instalado: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Desconhecido",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Falha ao montar imagem: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "O tamanho do cluster do sistema de arquivos (%llu) é menor que o tamanho do setor do dispositivo (%u) para %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "A imagem montada está ilegível ou danificada",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Cache de imagens cheio (%u entradas), não é possível rastrear a montagem %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache de imagens cheio (%u entradas), revertendo a montagem",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "A instalação falhou: %s (%s)\nTempo esgotado após 5 minutos",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalação em lote enfileirada (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... e mais %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "A instalação em lote falhou para %d aplicativos.\ncódigo=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "O aplicativo %s travou antes da pausa do KStuff. O KStuff não é o responsável.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s travou antes da pausa do KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "O aplicativo %s travou após a pausa do KStuff. O ajuste automático foi ignorado.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s travou após a pausa do KStuff:\n%s\nO ajuste automático foi ignorado.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Não é possível carregar o módulo após a pausa do KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "não foi possível carregar o módulo %.*s após a pausa do KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. O atraso aumentou para %u s.\nInicie o jogo novamente.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Falha detectada após a pausa do KStuff: o atraso de pausa de %s aumentou para %u s. Inicie o jogo novamente.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s travou após a pausa do KStuff:\n%s\nO atraso aumentou para %u s.\nInicie o jogo novamente.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Falha detectada após a pausa do KStuff: %s. Não foi possível atualizar o atraso do ajuste automático.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s travou após a pausa do KStuff:\n%s\nNão foi possível atualizar o atraso do ajuste automático.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Arquivo param.json ausente ou inválido:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d novos jogos encontrados. Processando...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Biblioteca sincronizada.\n%d jogos encontrados.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configuração recarregada.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Cópia duplicada de %s ignorada:\n%s\nExistente: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulador AMPR atualizado",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: falha ao instalar hooks do ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura-alvo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidade externa %s conectada.\nLivre: %llu.%llu de %llu.%llu GB (bloco %llu KB)\nJogos na unidade: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidade externa %s desconectada",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_pt_pt[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir para Definições de depuração",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor e a comunidade",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, será usado scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: profundidade de pesquisa %u ativada.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Falha no overlay de backport: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Jogo com backport aplicado: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Jogo com backport aplicado: %s\nEmuladores atualizados",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montagem da imagem rejeitada:\n%s\nO tamanho do cluster é demasiado pequeno (%llu < %u).\nSubstituição image_sector guardada: %u.\nTente montar novamente.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montagem da imagem rejeitada:\n%s\nO tamanho do cluster é demasiado pequeno (%llu < %u).\nUtilize um cluster maior na imagem ou um tamanho de setor menor na configuração.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registo falhou: %s (%s)\nAppInstallTitleDir indisponível",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Falha no registo: %s (%s)\nA ponte TitleDir não está disponível",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registo falhou: %s (%s)\ncódigo=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "A instalar: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff ativo novamente",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff em pausa enquanto o jogo está em execução",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Instalado: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Desconhecido",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Falha ao montar imagem: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "O tamanho do cluster do sistema de ficheiros (%llu) é menor que o tamanho do setor do dispositivo (%u) para %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "A imagem montada está ilegível ou danificada",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "A cache de imagens está cheia (%u entradas), não é possível acompanhar a montagem %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "A cache de imagens está cheia (%u entradas), a reverter a montagem",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Instalação falhou: %s (%s)\nTempo esgotado após 5 minutos",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalação em lote na fila (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... e mais %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "A instalação em lote falhou para %d aplicações.\ncódigo=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "A aplicação %s falhou antes da pausa do KStuff. O KStuff não é a causa.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s falhou antes da pausa do KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "A aplicação %s falhou após a pausa do KStuff. O ajuste automático foi ignorado.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s falhou após a pausa do KStuff:\n%s\nO ajuste automático foi ignorado.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Não é possível carregar o módulo após a pausa do KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "não foi possível carregar o módulo %.*s após a pausa do KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Atraso aumentado para %u s.\nInicie o jogo novamente.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Falha detetada após a pausa do KStuff: o atraso de pausa de %s aumentou para %u s. Inicie o jogo novamente.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s falhou após a pausa do KStuff:\n%s\nO atraso aumentou para %u s.\nInicie o jogo novamente.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Falha detetada após a pausa do KStuff: %s. Não foi possível atualizar o atraso do ajuste automático.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s falhou após a pausa do KStuff:\n%s\nNão foi possível atualizar o atraso do ajuste automático.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json em falta ou inválido:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d novos jogos encontrados. A processar...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Biblioteca sincronizada.\n%d jogos encontrados.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configuração recarregada.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicado %s ignorado:\n%s\nexistente: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulador AMPR atualizado",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: não foi possível instalar os hooks do ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura-alvo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidade externa %s ligada.\nLivre: %llu.%llu de %llu.%llu GB (bloco %llu KB)\nJogos na unidade: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidade externa %s desligada",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_ro_ro[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Mergi la setările de depanare",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor și comunitatea",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 este depășit, se folosește scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: adâncimea de scanare %u este activată.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Overlay backport eșuat: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport aplicat jocului: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport aplicat jocului: %s\nEmulatoare actualizate",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Montarea imaginii respinsă:\n%s\nDimensiunea clusterului este prea mică (%llu < %u).\nOverride image_sector salvat: %u.\nÎncercați montarea din nou.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montarea imaginii a fost respinsă:\n%s\nDimensiunea clusterului este prea mică (%llu < %u).\nFolosiți un cluster mai mare în imagine sau o dimensiune mai mică a sectorului în configurație.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Înregistrare eșuată: %s (%s)\nAppInstallTitleDir indisponibil",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Înregistrarea a eșuat: %s (%s)\nPuntea TitleDir nu este disponibilă",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Înregistrare eșuată: %s (%s)\ncod=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Se instalează: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff este activ din nou",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff este pauzat cât timp jocul rulează",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Instalat: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Necunoscut",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Montarea imaginii a eșuat: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Dimensiunea clusterului sistemului de fișiere (%llu) este mai mică decât sectorul dispozitivului (%u) pentru %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Imaginea montată este ilizibilă sau deteriorată",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Cache-ul de imagini este plin (%u intrări), montarea %s nu poate fi urmărită",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache-ul de imagini este plin (%u intrări), montarea este anulată",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Instalare eșuată: %s (%s)\nTimeout după 5 minute",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Instalare în lot pusă în coadă (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... și încă %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Instalarea în lot a eșuat pentru %d aplicații.\ncod=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Aplicația %s s-a blocat înainte de suspendarea KStuff. KStuff nu este cauza.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s s-a blocat înainte de suspendarea KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Aplicația %s s-a blocat după suspendarea KStuff. Reglarea automată a fost omisă.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s s-a blocat după suspendarea KStuff:\n%s\nReglarea automată a fost omisă.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Nu se poate încărca modulul după pauza KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "modulul %.*s nu poate fi încărcat după suspendarea KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Întârzierea a crescut la %u s.\nPorniți jocul din nou.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Blocare detectată după suspendarea KStuff: întârzierea pentru %s a crescut la %u s. Porniți jocul din nou.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s s-a blocat după suspendarea KStuff:\n%s\nÎntârzierea a crescut la %u s.\nPorniți jocul din nou.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Blocare detectată după suspendarea KStuff: %s. Întârzierea reglării automate nu a putut fi actualizată.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s s-a blocat după suspendarea KStuff:\n%s\nÎntârzierea reglării automate nu a putut fi actualizată.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json lipsește sau este invalid:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "S-au găsit %d jocuri noi. Se procesează...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliotecă sincronizată.\nS-au găsit %d jocuri.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: configurația a fost reîncărcată.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Duplicatul %s ignorat:\n%s\nexistent: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Emulatorul AMPR a fost actualizat",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: instalarea hook-urilor ShellCore a eșuat",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura țintă %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unitatea externă %s a fost conectată.\nLiber: %llu.%llu din %llu.%llu GB (dimensiunea blocului %llu KB)\nJocuri pe unitate: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unitatea externă %s a fost deconectată",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_ru_ru[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Перейти к настройкам отладки",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor и Сообщество",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: параметр recursive_scan=1 устарел; используется scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: включена глубина сканирования %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Не удалось смонтировать оверлей бэкпорта: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "К игре %s применён бэкпорт",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "К игре %s применён бэкпорт\nЭмуляторы обновлены",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Не удалось смонтировать образ:\n%s\nРазмер кластера слишком мал (%llu < %u).\nСохранено значение image_sector: %u.\nПопробуйте смонтировать снова.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Не удалось смонтировать образ:\n%s\nРазмер кластера слишком мал (%llu < %u).\nУвеличьте размер кластера в образе или уменьшите размер сектора в конфигурации.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Не удалось зарегистрировать: %s (%s)\nAppInstallTitleDir недоступен",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Не удалось зарегистрировать: %s (%s)\nМост TitleDir недоступен",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Не удалось зарегистрировать: %s (%s)\nкод=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Устанавливается: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff снова активен",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "Пока игра запущена, KStuff приостановлен",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Установлено: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Неизвестно",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Не удалось смонтировать образ: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Размер кластера файловой системы (%llu) меньше размера сектора устройства (%u) для %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Смонтированный образ не читается или повреждён",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Кэш образов заполнен (%u записей), невозможно отследить монтирование %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Кэш образов заполнен (%u записей), монтирование отменяется",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Сбой установки: %s (%s)\nПревышено время ожидания (5 минут)",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Пакетная установка добавлена в очередь (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... и ещё %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Сбой пакетной установки для %d приложений.\nкод=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Приложение аварийно завершилось до приостановки KStuff: %s. Причина не в KStuff.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Приложение %s аварийно завершилось до приостановки KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Приложение аварийно завершилось после приостановки KStuff: %s. Автоподбор задержки пропущен.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Приложение %s аварийно завершилось после приостановки KStuff:\n%s\nАвтоподбор задержки пропущен.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Не удалось загрузить модуль после приостановки KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "не удалось загрузить модуль %.*s после приостановки KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Задержка увеличена до %u с.\nЗапустите игру снова.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Обнаружен сбой после приостановки KStuff: задержка для %s увеличена до %u с. Запустите игру снова.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "Приложение %s аварийно завершилось после приостановки KStuff:\n%s\nЗадержка увеличена до %u с.\nЗапустите игру снова.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Обнаружен сбой после приостановки KStuff: %s. Не удалось обновить задержку автоподбора.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "Приложение %s аварийно завершилось после приостановки KStuff:\n%s\nНе удалось обновить задержку автоподбора.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Отсутствует или некорректен файл param.json:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Найдено %d новых игр.\nВыполняется обновление библиотеки.",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Библиотека обновлена\nНайдено %d игр(ы)",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: конфигурация перезагружена.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Дубликат %s пропущен:\n%s\nуже существует: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Эмулятор AMPR обновлён",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: не удалось установить хуки ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Целевая температура %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Внешний диск %s подключён.\nСвободно %llu.%llu из %llu.%llu ГБ (блок %llu КБ)\nИгр на диске: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Внешний диск %s отключён.",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_sv_se[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Gå till felsökningsinställningar",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor och gemenskapen",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 är föråldrat, använder scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: skanningsdjup %u aktiverat.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-overlay misslyckades: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Backport tillämpad på spelet: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Backport tillämpad på spelet: %s\nEmulatorer uppdaterade",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Image-montering nekades:\n%s\nKlusterstorleken är för liten (%llu < %u).\nimage_sector-åsidosättning sparad: %u.\nFörsök montera igen.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Montering av image nekades:\n%s\nKlusterstorleken är för liten (%llu < %u).\nAnvänd en större klusterstorlek i imagen eller en mindre sektorstorlek i konfigurationen.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrering misslyckades: %s (%s)\nAppInstallTitleDir inte tillgänglig",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Registreringen misslyckades: %s (%s)\nTitleDir-bryggan är inte tillgänglig",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Registrering misslyckades: %s (%s)\nkod=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Installerar: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff aktivt igen",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff pausat medan spelet körs",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Installerat: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Okänd",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Image-montering misslyckades: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Filsystemets klusterstorlek (%llu) är mindre än enhetens sektorstorlek (%u) för %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Monterad image är oläsbar eller skadad",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Image-cachen är full (%u poster), monteringen %s kan inte spåras",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-cachen är full (%u poster), monteringen återställs",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Installation misslyckades: %s (%s)\nTidsgräns efter 5 minuter",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Batchinstallation köad (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... och %d till",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Batchinstallationen misslyckades för %d appar.\nkod=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Appen %s kraschade innan KStuff pausades. KStuff är inte orsaken.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s kraschade innan KStuff pausades:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Appen %s kraschade efter att KStuff pausades. Automatisk justering hoppades över.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s kraschade efter att KStuff pausades:\n%s\nAutomatisk justering hoppades över.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Kan inte läsa in modul efter KStuff-paus",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "det går inte att läsa in modulen %.*s efter KStuff-pausen",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Fördröjning ökad till %u s.\nStarta spelet igen.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Krasch upptäckt efter KStuff-pausen: pausfördröjningen för %s ökades till %u s. Starta spelet igen.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s kraschade efter att KStuff pausades:\n%s\nFördröjningen ökades till %u s.\nStarta spelet igen.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Krasch upptäckt efter KStuff-pausen: %s. Fördröjningen för automatisk justering kunde inte uppdateras.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s kraschade efter att KStuff pausades:\n%s\nDet gick inte att uppdatera fördröjningen för automatisk justering.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json saknas eller är ogiltig:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Hittade %d nya spel. Bearbetar...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Bibliotek synkroniserat.\nHittade %d spel.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: konfigurationen lästes in på nytt.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Dubblett %s ignorerad:\n%s\nbefintlig: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR-emulatorn har uppdaterats",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: det gick inte att installera ShellCore-hooks",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Måltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Extern enhet %s ansluten.\nLedigt: %llu.%llu av %llu.%llu GB (blockstorlek %llu KB)\nSpel på enheten: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Extern enhet %s frånkopplad",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_th_th[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "ไปที่การตั้งค่าดีบัก",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor และชุมชน",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 เลิกใช้แล้ว จะใช้ scan_depth=2",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: เปิดใช้ความลึกการสแกน %u",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "backport overlay ล้มเหลว: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "ใช้ backport กับเกมแล้ว: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "ใช้ backport กับเกมแล้ว: %s\nอัปเดตอีมูเลเตอร์หลายตัวแล้ว",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "ปฏิเสธการเมานต์อิมเมจ:\n%s\nขนาดคลัสเตอร์เล็กเกินไป (%llu < %u)\nบันทึกค่า image_sector แล้ว: %u\nลองเมานต์อีกครั้ง",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "ปฏิเสธการเมานต์อิมเมจ:\n%s\nขนาดคลัสเตอร์เล็กเกินไป (%llu < %u)\nใช้คลัสเตอร์ที่ใหญ่ขึ้นในอิมเมจ หรือลดขนาดเซกเตอร์ในการตั้งค่า",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "ลงทะเบียนล้มเหลว: %s (%s)\nAppInstallTitleDir ไม่พร้อมใช้งาน",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "ลงทะเบียนไม่สำเร็จ: %s (%s)\nบริดจ์ TitleDir ไม่พร้อมใช้งาน",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "ลงทะเบียนล้มเหลว: %s (%s)\nรหัส=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "กำลังติดตั้ง: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff ทำงานอีกครั้ง",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "พัก KStuff ขณะเกมกำลังทำงาน",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "ติดตั้งแล้ว: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "ไม่ทราบ",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "เมานต์อิมเมจไม่สำเร็จ: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "ขนาด cluster ของ filesystem (%llu) เล็กกว่าขนาด sector ของอุปกรณ์ (%u) สำหรับ %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "อิมเมจที่เมานต์ไว้อ่านไม่ได้หรือเสียหาย",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "แคชอิมเมจเต็ม (%u รายการ) จึงติดตามการเมานต์ %s ไม่ได้",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "แคชอิมเมจเต็ม (%u รายการ) กำลังย้อนกลับการเมานต์",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "ติดตั้งล้มเหลว: %s (%s)\nหมดเวลาหลัง 5 นาที",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "เพิ่มการติดตั้งแบบชุดเข้าคิวแล้ว (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... และอีก %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "การติดตั้งแบบชุดล้มเหลวสำหรับ %d แอป\nรหัส=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "แอป %s ขัดข้องก่อนพัก KStuff โดย KStuff ไม่ใช่สาเหตุ",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s ขัดข้องก่อนหยุด KStuff ชั่วคราว:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "แอป %s ขัดข้องหลังพัก KStuff จึงข้ามการปรับอัตโนมัติ",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s ขัดข้องหลังหยุด KStuff ชั่วคราว:\n%s\nข้ามการปรับอัตโนมัติ",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "โหลดโมดูลหลังพัก KStuff ไม่ได้",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "ไม่สามารถโหลดโมดูล %.*s หลังหยุด KStuff ชั่วคราว",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. เพิ่ม delay เป็น %u วินาที\nเปิดเกมอีกครั้ง",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "พบการขัดข้องหลังพัก KStuff: เพิ่มเวลาหน่วงของ %s เป็น %u วินาที เปิดเกมอีกครั้ง",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s ขัดข้องหลังหยุด KStuff ชั่วคราว:\n%s\nเพิ่มเวลาหน่วงเป็น %u วินาที\nเปิดเกมอีกครั้ง",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "พบการขัดข้องหลังพัก KStuff: %s ไม่สามารถอัปเดตเวลาหน่วงการปรับอัตโนมัติ",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s ขัดข้องหลังหยุด KStuff ชั่วคราว:\n%s\nไม่สามารถอัปเดตเวลาหน่วงการปรับอัตโนมัติ",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json หายไปหรือไม่ถูกต้อง:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "พบเกมใหม่ %d เกม กำลังประมวลผล...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "ซิงค์ไลบรารีแล้ว\nพบ %d เกม",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: โหลดการตั้งค่าใหม่แล้ว",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "ข้ามรายการซ้ำ %s:\n%s\nที่มีอยู่: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "อัปเดตโปรแกรมจำลอง AMPR แล้ว",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ไม่สามารถติดตั้งฮุก ShellCore ได้",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "อุณหภูมิเป้าหมาย %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "เชื่อมต่อไดรฟ์ภายนอก %s แล้ว\nว่าง: %llu.%llu จาก %llu.%llu GB (ขนาดบล็อก %llu KB)\nเกมในไดรฟ์: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "ตัดการเชื่อมต่อไดรฟ์ภายนอก %s แล้ว",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_tr_tr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Hata ayıklama ayarlarına git",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor ve Topluluk",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 kullanımdan kaldırıldı; scan_depth=2 kullanılacak.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: tarama derinliği %u etkin.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay başarısız: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Oyuna backport uygulandı: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Oyuna backport uygulandı: %s\nEmülatörler güncellendi",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "İmaj bağlama işlemi reddedildi:\n%s\nKüme boyutu çok küçük (%llu < %u).\nimage_sector değeri kaydedildi: %u.\nBağlamayı yeniden deneyin.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "İmaj bağlama işlemi reddedildi:\n%s\nKüme boyutu çok küçük (%llu < %u).\nİmajda daha büyük bir küme veya yapılandırmada daha küçük bir sektör boyutu kullanın.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Kayıt başarısız: %s (%s)\nAppInstallTitleDir kullanılamıyor",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Kayıt başarısız: %s (%s)\nTitleDir köprüsü kullanılamıyor",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Kayıt başarısız: %s (%s)\nkod=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Yükleniyor: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff tekrar etkin",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "Oyun çalışırken KStuff duraklatıldı",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Yüklendi: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Bilinmiyor",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "İmaj bağlama işlemi başarısız: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Dosya sistemi küme boyutu (%llu), aygıt sektör boyutundan (%u) küçük: %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Bağlanan imaj okunamıyor veya hasarlı",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "İmaj önbelleği dolu (%u kayıt), %s bağlama işlemi izlenemiyor",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "İmaj önbelleği dolu (%u kayıt), bağlama işlemi geri alınıyor",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Yükleme başarısız: %s (%s)\n5 dakika sonra zaman aşımı",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Toplu yükleme sıraya alındı (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... ve %d tane daha",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "%d uygulama için toplu yükleme başarısız.\nkod=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s, KStuff duraklatılmadan önce çöktü. Sorunun nedeni KStuff değil.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s, KStuff duraklatılmadan önce çöktü:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s, KStuff duraklatıldıktan sonra çöktü. Otomatik ayarlama atlandı.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s, KStuff duraklatıldıktan sonra çöktü:\n%s\nOtomatik ayarlama atlandı.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "KStuff duraklamasından sonra modül yüklenemiyor",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "KStuff duraklatıldıktan sonra %.*s modülü yüklenemiyor",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Gecikme %u s değerine çıkarıldı.\nOyunu tekrar başlatın.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "KStuff duraklatıldıktan sonra çökme algılandı: %s için gecikme %u sn. olarak artırıldı. Oyunu yeniden başlatın.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s, KStuff duraklatıldıktan sonra çöktü:\n%s\nGecikme %u sn. olarak artırıldı.\nOyunu yeniden başlatın.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "KStuff duraklatıldıktan sonra çökme algılandı: %s. Otomatik ayarlama gecikmesi güncellenemedi.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s, KStuff duraklatıldıktan sonra çöktü:\n%s\nOtomatik ayarlama gecikmesi güncellenemedi.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json eksik veya geçersiz:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "%d yeni oyun bulundu. İşleniyor...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Kütüphane senkronize edildi.\n%d oyun bulundu.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: yapılandırma yeniden yüklendi.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Yinelenen %s yok sayıldı:\n%s\nmevcut: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR emülatörü güncellendi",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore hook'ları yüklenemedi",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Hedef sıcaklık %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Harici sürücü %s bağlandı.\nBoş: %llu.%llu / %llu.%llu GB (blok boyutu %llu KB)\nSürücüdeki oyunlar: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Harici sürücü %s çıkarıldı",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_uk_ua[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Перейти до налаштувань налагодження",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor і спільнота",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: параметр recursive_scan=1 застарів; використовується scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: увімкнено глибину сканування %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Не вдалося змонтувати оверлей бекпорту: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "До гри %s застосовано бекпорт",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "До гри %s застосовано бекпорт\nЕмулятори оновлено",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Монтування образу відхилено:\n%s\nРозмір кластера замалий (%llu < %u).\nЗбережено перевизначення image_sector: %u.\nСпробуйте змонтувати знову.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Монтування образу відхилено:\n%s\nРозмір кластера замалий (%llu < %u).\nЗбільште розмір кластера в образі або зменште розмір сектора в конфігурації.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Не вдалося зареєструвати: %s (%s)\nAppInstallTitleDir недоступний",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Не вдалося зареєструвати: %s (%s)\nМіст TitleDir недоступний",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Не вдалося зареєструвати: %s (%s)\nкод=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Встановлення: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff знову активний",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff призупинено на час роботи гри",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Встановлено: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Невідомо",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Не вдалося змонтувати образ: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Розмір кластера файлової системи (%llu) менший за розмір сектора пристрою (%u) для %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Змонтований образ не читається або пошкоджений",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Кеш образів заповнений (%u записів), неможливо відстежити монтування %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Кеш образів заповнений (%u записів), монтування скасовується",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Не вдалося встановити: %s (%s)\nЧас очікування вичерпано через 5 хвилин",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Пакетне встановлення додано до черги (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... і ще %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Не вдалося пакетно встановити %d застосунків.\nкод=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Застосунок %s аварійно завершився до призупинення KStuff. Причина не в KStuff.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Застосунок %s аварійно завершився до призупинення KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Застосунок %s аварійно завершився після призупинення KStuff. Автоналаштування пропущено.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Застосунок %s аварійно завершився після призупинення KStuff:\n%s\nАвтоналаштування пропущено.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Не вдалося завантажити модуль після призупинення KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "не вдалося завантажити модуль %.*s після призупинення KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Затримку збільшено до %u с.\nЗапустіть гру знову.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Виявлено збій після призупинення KStuff: затримку для %s збільшено до %u с. Запустіть гру знову.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "Застосунок %s аварійно завершився після призупинення KStuff:\n%s\nЗатримку збільшено до %u с.\nЗапустіть гру знову.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Виявлено збій після призупинення KStuff: %s. Не вдалося оновити затримку автоналаштування.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "Застосунок %s аварійно завершився після призупинення KStuff:\n%s\nНе вдалося оновити затримку автоналаштування.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Файл param.json відсутній або некоректний:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Знайдено %d нових ігор. Обробка...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Бібліотеку синхронізовано.\nЗнайдено %d ігор.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: конфігурацію перезавантажено.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Дублікат %s проігноровано:\n%s\nнаявний: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Емулятор AMPR оновлено",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: не вдалося встановити хуки ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Цільова температура %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Зовнішній диск %s підключено.\nВільно %llu.%llu із %llu.%llu ГБ (блок %llu КБ)\nІгор на диску: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Зовнішній диск %s відключено",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_vi_vn[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Đi tới cài đặt gỡ lỗi",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor và Cộng đồng",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 đã lỗi thời; sẽ sử dụng scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: đã bật độ sâu quét %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay thất bại: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "Đã áp dụng backport cho game: %s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "Đã áp dụng backport cho game: %s\nĐã cập nhật các trình giả lập",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Từ chối gắn ảnh đĩa:\n%s\nKích thước cụm quá nhỏ (%llu < %u).\nĐã lưu giá trị image_sector: %u.\nHãy thử gắn lại.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Từ chối gắn ảnh đĩa:\n%s\nKích thước cụm quá nhỏ (%llu < %u).\nHãy dùng cụm lớn hơn trong ảnh đĩa hoặc kích thước cung từ nhỏ hơn trong cấu hình.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Đăng ký thất bại: %s (%s)\nAppInstallTitleDir không khả dụng",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Đăng ký thất bại: %s (%s)\nCầu nối TitleDir không khả dụng",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Đăng ký thất bại: %s (%s)\nmã=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Đang cài đặt: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff hoạt động lại",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff tạm dừng khi game đang chạy",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Đã cài đặt: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Không xác định",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Không thể gắn ảnh đĩa: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Kích thước cluster của hệ thống tệp (%llu) nhỏ hơn kích thước sector thiết bị (%u) cho %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Ảnh đĩa đã gắn không thể đọc được hoặc bị hỏng",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Bộ nhớ đệm ảnh đĩa đã đầy (%u mục), không thể theo dõi điểm gắn %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Bộ nhớ đệm ảnh đĩa đã đầy (%u mục), đang hoàn tác việc gắn",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Cài đặt thất bại: %s (%s)\nHết thời gian sau 5 phút",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Cài đặt hàng loạt đã vào hàng đợi (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... và thêm %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Cài đặt hàng loạt thất bại cho %d ứng dụng.\nmã=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Ứng dụng %s gặp sự cố trước khi KStuff tạm dừng. KStuff không phải là nguyên nhân.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s gặp sự cố trước khi tạm dừng KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Ứng dụng %s gặp sự cố sau khi KStuff tạm dừng. Đã bỏ qua tinh chỉnh tự động.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s gặp sự cố sau khi tạm dừng KStuff:\n%s\nĐã bỏ qua tự động tinh chỉnh.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Không thể tải module sau khi KStuff tạm dừng",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "không thể tải mô-đun %.*s sau khi tạm dừng KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Độ trễ đã tăng lên %u giây.\nHãy khởi chạy lại trò chơi.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Phát hiện sự cố sau khi KStuff tạm dừng: độ trễ của %s đã tăng lên %u giây. Hãy khởi chạy lại trò chơi.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s gặp sự cố sau khi tạm dừng KStuff:\n%s\nĐộ trễ đã tăng lên %u giây.\nHãy khởi chạy lại trò chơi.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Phát hiện sự cố sau khi KStuff tạm dừng: %s. Không thể cập nhật độ trễ tinh chỉnh tự động.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s gặp sự cố sau khi tạm dừng KStuff:\n%s\nKhông thể cập nhật độ trễ tự động tinh chỉnh.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json thiếu hoặc không hợp lệ:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Tìm thấy %d trò chơi mới. Đang xử lý...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Đã đồng bộ thư viện.\nTìm thấy %d trò chơi.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: đã tải lại cấu hình.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Đã bỏ qua bản trùng %s:\n%s\nđã có: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Đã cập nhật trình giả lập AMPR",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: không thể cài đặt hook ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Nhiệt độ mục tiêu %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Đã kết nối ổ đĩa ngoài %s.\nTrống: %llu.%llu / %llu.%llu GB (kích thước khối %llu KB)\nTrò chơi trên ổ đĩa: %d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Đã ngắt kết nối ổ đĩa ngoài %s",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_zh_cn[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "前往调试设置",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor 与社区",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 已弃用,将使用 scan_depth=2。",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: 已启用扫描深度 %u。",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "向下兼容覆盖层挂载失败:%s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "已为游戏应用向下兼容补丁:%s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "已为游戏应用向下兼容补丁:%s\n多个模拟器已更新",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "镜像挂载被拒绝:\n%s\n簇大小太小 (%llu < %u)。\n已保存 image_sector 覆盖值: %u。\n请再次尝试挂载。",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "镜像挂载被拒绝:\n%s\n簇大小过小(%llu < %u)。\n请增大镜像的簇大小,或在配置中减小扇区大小。",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "注册失败: %s (%s)\nAppInstallTitleDir 不可用",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "注册失败:%s(%s)\nTitleDir 桥接不可用",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "注册失败: %s (%s)\n代码=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "正在安装: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff 已重新启用",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "游戏运行时 KStuff 已暂停",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "已安装: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "未知",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "镜像挂载失败: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "文件系统簇大小 (%llu) 小于设备扇区大小 (%u):%s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "已挂载的镜像不可读或已损坏",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "镜像缓存已满(%u 项),无法跟踪挂载 %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "镜像缓存已满(%u 项),正在撤销挂载",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "安装失败: %s (%s)\n5 分钟后超时",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "批量安装已排队 (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... 以及另外 %d 个",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "%d 个应用批量安装失败。\n代码=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s 在暂停 KStuff 前崩溃。问题并非由 KStuff 引起。",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s 在暂停 KStuff 前崩溃:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s 在暂停 KStuff 后崩溃。已跳过自动调节。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s 在暂停 KStuff 后崩溃:\n%s\n已跳过自动调节。",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "KStuff 暂停后无法加载模块",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "暂停 KStuff 后无法加载模块 %.*s",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s。延迟已增加到 %u 秒。\n请重新启动游戏。",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "检测到暂停 KStuff 后发生崩溃:%s 的延迟已增加至 %u 秒。请重新启动游戏。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s 在暂停 KStuff 后崩溃:\n%s\n延迟已增加至 %u 秒。\n请重新启动游戏。",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "检测到暂停 KStuff 后发生崩溃:%s。无法更新自动调节延迟。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s 在暂停 KStuff 后崩溃:\n%s\n无法更新自动调节延迟。",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json 缺失或无效:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "发现 %d 个新游戏。正在处理...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "游戏库已同步。\n共发现 %d 个游戏。",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+:配置已重新加载。",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "已忽略重复项 %s:\n%s\n现有项:%s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR 模拟器已更新",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+:无法安装 ShellCore 钩子",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "目标温度 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "外部驱动器 %s 已连接。\n可用:%llu.%llu / %llu.%llu GB(块大小 %llu KB)\n驱动器上的游戏:%d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "外部驱动器 %s 已断开",
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
static const char *const g_zh_tw[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "前往除錯設定",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor 與社群",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 已淘汰,將使用 scan_depth=2。",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: 已啟用掃描深度 %u。",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "向下相容覆蓋層掛載失敗:%s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "已為遊戲套用向下相容修補程式:%s",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "已為遊戲套用向下相容修補程式:%s\n多個模擬器已更新",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "映像掛載被拒絕:\n%s\n叢集大小太小 (%llu < %u)。\n已儲存 image_sector 覆寫值: %u。\n請再次嘗試掛載。",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "映像掛載遭拒:\n%s\n叢集大小過小(%llu < %u)。\n請增大映像的叢集大小,或在設定中減小磁區大小。",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "註冊失敗: %s (%s)\nAppInstallTitleDir 無法使用",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "註冊失敗:%s(%s)\nTitleDir 橋接無法使用",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "註冊失敗: %s (%s)\n代碼=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "正在安裝: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff 已重新啟用",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "遊戲執行時 KStuff 已暫停",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "已安裝: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "未知",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "映像掛載失敗: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "檔案系統叢集大小 (%llu) 小於裝置磁區大小 (%u):%s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "已掛載的映像無法讀取或已損壞",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "映像快取已滿(%u 筆),無法追蹤掛載 %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "映像快取已滿(%u 筆),正在復原掛載",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "安裝失敗: %s (%s)\n5 分鐘後逾時",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "批次安裝已排入佇列 (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... 以及另外 %d 個",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "%d 個應用程式批次安裝失敗。\n代碼=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "%s 在暫停 KStuff 前當機。問題並非由 KStuff 引起。",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "%s 在暫停 KStuff 前當機:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "%s 在暫停 KStuff 後當機。已略過自動調整。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "%s 在暫停 KStuff 後當機:\n%s\n已略過自動調整。",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "KStuff 暫停後無法載入模組",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "暫停 KStuff 後無法載入模組 %.*s",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s。延遲已增加到 %u 秒。\n請重新啟動遊戲。",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "偵測到暫停 KStuff 後發生當機:%s 的延遲已增加至 %u 秒。請重新啟動遊戲。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "%s 在暫停 KStuff 後當機:\n%s\n延遲已增加至 %u 秒。\n請重新啟動遊戲。",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "偵測到暫停 KStuff 後發生當機:%s。無法更新自動調整延遲。",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "%s 在暫停 KStuff 後當機:\n%s\n無法更新自動調整延遲。",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "param.json 遺失或無效:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "找到 %d 個新遊戲。正在處理...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "遊戲庫已同步。\n共找到 %d 個遊戲。",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+:設定已重新載入。",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "已略過重複項目 %s:\n%s\n現有項目:%s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "AMPR 模擬器已更新",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+:無法安裝 ShellCore 掛鉤",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "目標溫度 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "外接磁碟機 %s 已連接。\n可用:%llu.%llu / %llu.%llu GB(區塊大小 %llu KB)\n磁碟機上的遊戲:%d",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "外接磁碟機 %s 已中斷連接",
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_AMPR_UPDATER_H
|
||||
#define SM_AMPR_UPDATER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Start the dormant-by-default AMPR update worker.
|
||||
bool sm_ampr_updater_start(void);
|
||||
// Interrupt any active request and join the update worker.
|
||||
void sm_ampr_updater_stop(void);
|
||||
// Wake the worker when its enabled state, URL, or destination changes.
|
||||
void sm_ampr_updater_on_config_reload(const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_API_PROTOCOL_H
|
||||
#define SM_API_PROTOCOL_H
|
||||
|
||||
#define SM_API_DEFAULT_BIND_ADDRESS "127.0.0.1"
|
||||
#define SM_API_DEFAULT_PORT 10101u
|
||||
#define SM_API_VERSION 1u
|
||||
#define SM_API_MAX_HTTP_HEADER_SIZE 8192u
|
||||
#define SM_API_MAX_JSON_BODY_SIZE 4096u
|
||||
|
||||
#define SM_API_ROUTE_VERSION "/api/v1/version"
|
||||
#define SM_API_ROUTE_IMAGES "/api/v1/images"
|
||||
#define SM_API_ROUTE_GAMES "/api/v1/games"
|
||||
#define SM_API_ROUTE_MOUNT "/api/v1/games/mount"
|
||||
#define SM_API_ROUTE_UNMOUNT "/api/v1/games/unmount"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef SM_API_SERVICE_H
|
||||
#define SM_API_SERVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
bool sm_api_service_start(void);
|
||||
void sm_api_service_stop(void);
|
||||
bool sm_api_service_reconfigure(void);
|
||||
|
||||
#endif
|
||||
@@ -22,6 +22,8 @@ void free_app_db_title_list(struct AppDbTitleList *list);
|
||||
void invalidate_app_db_title_cache(void);
|
||||
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
|
||||
// Copy titles whose metadata path matches /user/app/PPSA*/sce_sys.
|
||||
bool get_app_db_auto_remove_title_list(struct AppDbTitleList *list_out);
|
||||
// Copy the cached PPSA titles that app.db marks as not uninstallable.
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
|
||||
|
||||
|
||||
@@ -7,8 +7,15 @@
|
||||
|
||||
// Return true when per-game fakelib backport automation is enabled.
|
||||
bool sm_fakelib_game_feature_enabled(void);
|
||||
// Prepare or refresh the combined fakelib cache before title runtime mounting.
|
||||
void sm_fakelib_prepare_title_cache(const char *title_id,
|
||||
const char *game_path);
|
||||
// Remove invalid and unused fakelib caches during the scanner's rare cycle.
|
||||
void sm_fakelib_cleanup_caches(void);
|
||||
// Track a supported game launch and mount its fakelib overlay immediately.
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id);
|
||||
// notify_user is false for an internal process handoff of the same application.
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id,
|
||||
bool notify_user);
|
||||
// Remove a tracked fakelib overlay for a stopped game.
|
||||
void sm_fakelib_game_on_exit(pid_t pid);
|
||||
// Unmount all tracked overlays during watcher shutdown.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef SM_FAN_H
|
||||
#define SM_FAN_H
|
||||
|
||||
// Apply the configured automatic fan-controller target for a confirmed running
|
||||
// game. System-managed configuration is a no-op.
|
||||
void sm_fan_apply_game_target(const char *title_id);
|
||||
|
||||
#endif
|
||||
+26
-1
@@ -4,6 +4,9 @@
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
// Check whether a title is present in the installed app set.
|
||||
bool is_installed(const char *title_id);
|
||||
@@ -13,6 +16,18 @@ bool has_appmeta_data(const char *title_id);
|
||||
bool is_data_mounted(const char *title_id);
|
||||
// Read the mount.lnk source path for a title tracked under /user/app.
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size);
|
||||
// Atomically persist the mount.lnk source path for a title.
|
||||
bool write_mount_link(const char *title_id, const char *source_path);
|
||||
// Read the optional backing image path retained for an image-backed title.
|
||||
bool read_mount_image_link(const char *title_id, char *out, size_t out_size);
|
||||
// Persist the outer backing-image path for an already staged title.
|
||||
bool write_mount_image_link(const char *title_id, const char *image_path);
|
||||
bool read_mount_image_chain(
|
||||
const char *title_id,
|
||||
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out);
|
||||
bool write_mount_image_chain(
|
||||
const char *title_id,
|
||||
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count);
|
||||
// Validate a /user/app/<TITLE_ID> tracker entry and optionally build its path.
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size);
|
||||
@@ -26,6 +41,12 @@ int remount_system_ex(void);
|
||||
bool mount_title_nullfs(const char *title_id, const char *src_path);
|
||||
// Roll back a just-mounted title nullfs layer when publication is aborted.
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
|
||||
// Drop runtime layers for a title while preserving its metadata/link files.
|
||||
bool unmount_title_runtime_layers(const char *title_id);
|
||||
// Same operation without expected EBUSY diagnostics during a bounded retry.
|
||||
bool unmount_title_runtime_layers_quiet(const char *title_id);
|
||||
// Release every managed title layer while preserving link metadata.
|
||||
bool unmount_all_title_runtime_layers(void);
|
||||
// Reconcile the title mount stack against the expected source/backport state.
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
@@ -36,7 +57,7 @@ bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const char *title_id);
|
||||
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
|
||||
void shutdown_title_mounts(void);
|
||||
bool shutdown_title_mounts(void);
|
||||
// Remove stale mount links and optionally restore image-backed mounts.
|
||||
void cleanup_mount_links(const char *removed_source_root,
|
||||
bool unmount_system_ex_bind);
|
||||
@@ -46,7 +67,11 @@ void cleanup_mount_links_for_source_unmount(const char *source_root);
|
||||
void cleanup_usb_mount_links_for_suspend(void);
|
||||
// Recursively copy a directory tree.
|
||||
int copy_dir(const char *src, const char *dst);
|
||||
// Merge a directory tree and set mode as each destination entry is created.
|
||||
int copy_dir_with_mode(const char *src, const char *dst, mode_t mode);
|
||||
// Copy a single file.
|
||||
int copy_file(const char *src, const char *dst);
|
||||
// Copy a single file and set its mode immediately after creation.
|
||||
int copy_file_with_mode(const char *src, const char *dst, mode_t mode);
|
||||
|
||||
#endif
|
||||
@@ -2,6 +2,15 @@
|
||||
#define SM_GAME_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
} sm_game_cache_snapshot_entry_t;
|
||||
|
||||
typedef bool (*game_cache_iter_fn)(const char *path, const char *title_id,
|
||||
const char *title_name,
|
||||
@@ -14,12 +23,22 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
void prune_game_cache(void);
|
||||
// Drop invalid or stale entries that belong to a specific scan root.
|
||||
void prune_game_cache_for_root(const char *root);
|
||||
// Cancel pending removal when the same title is discovered at a valid source.
|
||||
void note_game_cache_source_seen(const char *path, const char *title_id,
|
||||
const char *title_name);
|
||||
// Reconcile app.db auto-remove candidates and uninstall expired missing entries.
|
||||
void reconcile_missing_app_db_games(void);
|
||||
// Reset pending missing timers after relevant runtime config changes.
|
||||
void reset_missing_game_cache_timers(void);
|
||||
// Look up a cached game entry by path or title ID.
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out);
|
||||
// Visit cached games, optionally limited to one source root.
|
||||
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
|
||||
void *ctx);
|
||||
// Allocate a stable snapshot of all cached games. Caller frees it.
|
||||
bool sm_game_cache_snapshot(sm_game_cache_snapshot_entry_t **entries_out,
|
||||
size_t *count_out);
|
||||
// Remove a game cache entry by path.
|
||||
void clear_cached_game(const char *path);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define SM_GAME_LIFECYCLE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Start the shared game lifecycle watcher used by runtime game features.
|
||||
bool start_game_lifecycle_watcher(void);
|
||||
@@ -11,6 +12,8 @@ void wake_game_lifecycle_watcher(void);
|
||||
void stop_game_lifecycle_watcher(void);
|
||||
// Return true when a supported game is currently being tracked as running.
|
||||
bool sm_game_lifecycle_has_active_game(void);
|
||||
// Publish and wake lifecycle/kstuff processing for an AppFocus change.
|
||||
void sm_game_lifecycle_note_app_focus(uint32_t app_id);
|
||||
// Ensure the shared watcher is running for runtime game-state tracking.
|
||||
bool refresh_game_lifecycle_watcher(void);
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// Read title ID and title name from a mounted game directory.
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name);
|
||||
// Accept canonical PS4/PS5 game title IDs used in filesystem paths.
|
||||
bool is_supported_game_title_id(const char *title_id);
|
||||
// Check whether a directory contains sce_sys/param.json and stat it.
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out);
|
||||
|
||||
|
||||
+17
-2
@@ -5,12 +5,27 @@
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct unmount_result {
|
||||
bool filesystem_released;
|
||||
bool device_released;
|
||||
bool directory_removed;
|
||||
int error;
|
||||
} unmount_result_t;
|
||||
|
||||
// Log filesystem statistics for a mounted path.
|
||||
void log_fs_stats(const char *tag, const char *path, const char *type_hint);
|
||||
// Attach and mount an image file to its runtime mount point.
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
// Unmount an image mount point and detach its backing device.
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
|
||||
unmount_result_t unmount_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend);
|
||||
// Unmount a runtime image without deleting persistent title metadata links.
|
||||
unmount_result_t unmount_runtime_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend);
|
||||
// Release one cached runtime image mount. An absent live mount is harmless.
|
||||
bool release_runtime_image_mount(const char *file_path);
|
||||
// Release all discovery/runtime image mounts without deleting title metadata.
|
||||
bool release_runtime_image_mounts(void);
|
||||
// Reconcile cached image mounts with current sources and remount if needed.
|
||||
void cleanup_stale_image_mounts(void);
|
||||
// Reconcile cached image mounts that belong to a specific scan root.
|
||||
@@ -22,7 +37,7 @@ bool shutdown_image_mounts(void);
|
||||
// Remove empty directories left under the image mount root.
|
||||
void cleanup_mount_dirs(void);
|
||||
// Mount an image file if it is stable and not currently rate-limited.
|
||||
bool maybe_mount_image_file(const char *full_path, const char *name,
|
||||
bool maybe_mount_image_file(const char *full_path, const char *display_name,
|
||||
bool *unstable_out);
|
||||
// Build the deterministic runtime mount point for an image source path.
|
||||
void get_image_mount_point_for_source(const char *file_path,
|
||||
|
||||
@@ -6,22 +6,54 @@
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct {
|
||||
uint64_t generation;
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
attached_device_state_t state;
|
||||
attached_unit_detach_state_t detach_state;
|
||||
} image_cache_entry_t;
|
||||
|
||||
// Reserve a cache record before issuing an attach ioctl.
|
||||
bool begin_image_attachment(const char *path, const char *mount_point,
|
||||
uint64_t *generation_out);
|
||||
// Cancel an unpublished attachment reservation of the same generation.
|
||||
bool cancel_image_attachment(const char *path, uint64_t generation);
|
||||
// Publish the kernel unit immediately after a successful attach ioctl.
|
||||
bool publish_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Mark a published device as a verified filesystem mount.
|
||||
bool complete_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Cache a successful image mount and its attached device.
|
||||
// Returns false when no free cache slot is available.
|
||||
// Returns false when no slot is available or either key has another owner.
|
||||
bool cache_image_mount(const char *path, const char *mount_point,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Cache an image source mapping without attach metadata.
|
||||
// Cache an image source mapping without attach metadata. Conflicting active
|
||||
// source-path and mount-point owners are never retargeted.
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point);
|
||||
// Return a cached image mount entry by index.
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
|
||||
// Mark a cached image mount entry as invalid.
|
||||
void invalidate_image_cache_entry(int index);
|
||||
// Find a cached source path with one cache lock acquisition.
|
||||
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
int *index_out);
|
||||
// Keep a device in the detach-retry queue until its node is released.
|
||||
bool mark_image_cache_detach_requested(const char *path, uint64_t generation,
|
||||
int unit_id,
|
||||
attach_backend_t backend);
|
||||
// Claim one detach attempt and return a stable snapshot of its record.
|
||||
bool claim_image_cache_detach(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend,
|
||||
image_cache_entry_t *entry_out);
|
||||
// Publish the result of a claimed detach attempt and release its claim.
|
||||
bool finish_image_cache_detach_attempt(
|
||||
const char *path, uint64_t generation, int unit_id,
|
||||
attach_backend_t backend,
|
||||
const attached_unit_detach_state_t *detach_state);
|
||||
// Invalidate only when the slot still owns the complete expected attachment.
|
||||
bool invalidate_image_cache_entry(int index,
|
||||
const image_cache_entry_t *expected);
|
||||
// Resolve a device mapping from the in-memory mount cache.
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
@@ -30,5 +62,16 @@ bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
// Resolve the image whose mount point owns a mounted child path.
|
||||
bool resolve_owning_image_source_from_mount_cache(const char *path,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
// Resolve every owning image from the outermost source to the innermost one.
|
||||
size_t resolve_image_source_chain_from_mount_cache(
|
||||
const char *path,
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]);
|
||||
// Resolve only the outermost owning image source.
|
||||
bool resolve_outermost_image_source_from_mount_cache(
|
||||
const char *path, char *path_out, size_t path_out_size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef SM_IMAGE_INDEX_H
|
||||
#define SM_IMAGE_INDEX_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
typedef void (*sm_image_index_title_visitor_t)(const char *title_id);
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
int64_t size;
|
||||
int64_t mtime_sec;
|
||||
int32_t mtime_nsec;
|
||||
bool complete;
|
||||
} sm_image_index_snapshot_entry_t;
|
||||
|
||||
// Return true when a complete unchanged index entry can be reused instead of
|
||||
// opening the image, visiting every retained title ID before returning.
|
||||
bool sm_image_index_visit_ready_titles(
|
||||
const char *path, const struct stat *st,
|
||||
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready,
|
||||
sm_image_index_title_visitor_t visitor);
|
||||
// Return true when a title is mapped to an existing backing image.
|
||||
bool sm_image_index_has_source_for_title(const char *title_id);
|
||||
// Start a new fingerprint generation before mounting an image.
|
||||
void sm_image_index_begin_scan(const char *path, const struct stat *st);
|
||||
// Associate a discovered mounted game with its outer backing image. Returns
|
||||
// false when an image-backed title could not be retained in the index.
|
||||
bool sm_image_index_record_game(const char *game_path, const char *title_id);
|
||||
// Commit one fingerprint after its mounted content was scanned.
|
||||
void sm_image_index_complete_scan(const char *path);
|
||||
// Atomically persist accumulated index changes at the scan-cycle boundary.
|
||||
void sm_image_index_flush(void);
|
||||
// Drop persistent entries whose backing image disappeared.
|
||||
void sm_image_index_prune(void);
|
||||
// Allocate a stable snapshot of all known backing images. Caller frees it.
|
||||
bool sm_image_index_snapshot(sm_image_index_snapshot_entry_t **entries_out,
|
||||
size_t *count_out);
|
||||
|
||||
#endif
|
||||
@@ -24,5 +24,7 @@ bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool sm_install_submit_queued(void);
|
||||
// Record retry state for every queued title after a batch submit failure.
|
||||
void sm_install_note_submit_failure(void);
|
||||
// Return true while an install still needs its staged mounts.
|
||||
bool sm_install_has_pending_work(void);
|
||||
|
||||
#endif
|
||||
+5
-2
@@ -21,14 +21,17 @@ bool sm_kstuff_game_feature_enabled(void);
|
||||
// Track a supported game launch for delayed auto-pause handling.
|
||||
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
uint64_t exec_time_us);
|
||||
// Preserve kstuff state across an ExitSpawn/LoadExec process replacement.
|
||||
bool sm_kstuff_game_handoff(pid_t old_pid, pid_t new_pid,
|
||||
const char *title_id, uint32_t app_id);
|
||||
// Publish an AppFocus change for processing on the lifecycle/kstuff thread.
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id);
|
||||
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
|
||||
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us);
|
||||
// Forget a tracked game and restore kstuff when no paused entries remain.
|
||||
void sm_kstuff_game_on_exit(pid_t pid);
|
||||
// Apply any delayed kstuff pause whose deadline has elapsed.
|
||||
void sm_kstuff_game_poll(void);
|
||||
// Apply config reloads and, while process_active, delayed per-process work.
|
||||
void sm_kstuff_game_poll(bool process_active);
|
||||
// Clear tracked game state and restore kstuff if the watcher paused it.
|
||||
void sm_kstuff_game_shutdown(void);
|
||||
// Pause kstuff for runtime sleep without restoring game-driven pauses.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef SM_L10N_H
|
||||
#define SM_L10N_H
|
||||
|
||||
#include "sm_platform.h"
|
||||
|
||||
#define SM_LANGUAGE_AUTO (-1)
|
||||
|
||||
typedef enum {
|
||||
SM_L10N_ACTION_DEBUG_SETTINGS = 0,
|
||||
SM_L10N_STARTUP,
|
||||
SM_L10N_RECURSIVE_SCAN_DEPRECATED,
|
||||
SM_L10N_SCAN_DEPTH_ENABLED,
|
||||
SM_L10N_BACKPORT_OVERLAY_FAILED,
|
||||
SM_L10N_GAME_BACKPORTED,
|
||||
SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED,
|
||||
SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED,
|
||||
SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG,
|
||||
SM_L10N_REGISTER_FAILED_UNAVAILABLE,
|
||||
SM_L10N_REGISTER_BRIDGE_UNAVAILABLE,
|
||||
SM_L10N_REGISTER_FAILED_CODE,
|
||||
SM_L10N_INSTALLING,
|
||||
SM_L10N_KSTUFF_ACTIVE,
|
||||
SM_L10N_KSTUFF_PAUSED,
|
||||
SM_L10N_INSTALLED,
|
||||
SM_L10N_UNKNOWN,
|
||||
SM_L10N_IMAGE_MOUNT_FAILED,
|
||||
SM_L10N_FS_CLUSTER_SMALL,
|
||||
SM_L10N_IMAGE_UNREADABLE_DAMAGED,
|
||||
SM_L10N_IMAGE_CACHE_FULL_TRACK,
|
||||
SM_L10N_IMAGE_CACHE_FULL_ROLLBACK,
|
||||
SM_L10N_INSTALL_FAILED_TIMEOUT,
|
||||
SM_L10N_BATCH_INSTALL_QUEUED,
|
||||
SM_L10N_BATCH_INSTALL_AND_MORE,
|
||||
SM_L10N_BATCH_INSTALL_FAILED,
|
||||
SM_L10N_CRASH_BEFORE_KSTUFF,
|
||||
SM_L10N_CRASH_BEFORE_KSTUFF_FATAL,
|
||||
SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED,
|
||||
SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED,
|
||||
SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK,
|
||||
SM_L10N_RTLD_MODULE_AFTER_KSTUFF,
|
||||
SM_L10N_DELAY_INCREASED_RELAUNCH,
|
||||
SM_L10N_CRASH_DELAY_INCREASED,
|
||||
SM_L10N_CRASH_FATAL_DELAY_INCREASED,
|
||||
SM_L10N_CRASH_DELAY_UPDATE_FAILED,
|
||||
SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED,
|
||||
SM_L10N_MISSING_PARAM,
|
||||
SM_L10N_FOUND_NEW_GAMES,
|
||||
SM_L10N_LIBRARY_SYNCED,
|
||||
SM_L10N_CONFIG_RELOADED,
|
||||
SM_L10N_DUPLICATE_IGNORED,
|
||||
SM_L10N_AMPR_EMULATOR_UPDATED,
|
||||
SM_L10N_SHELLCORE_HOOKS_FAILED,
|
||||
SM_L10N_TEMPERATURE_TRANSITION,
|
||||
SM_L10N_USB_CONNECTED_SCANNING,
|
||||
SM_L10N_USB_DISCONNECTED,
|
||||
SM_L10N_COUNT
|
||||
} sm_l10n_key_t;
|
||||
|
||||
void sm_l10n_init(void);
|
||||
bool sm_l10n_parse_language_id(const char *value, int32_t *language_id_out);
|
||||
const char *sm_l10n_language_name(int32_t language_id);
|
||||
const char *sm_l10n_get(sm_l10n_key_t key);
|
||||
|
||||
#endif
|
||||
+20
-2
@@ -4,11 +4,15 @@
|
||||
#define DEFAULT_SCAN_INTERVAL_US 15000000u
|
||||
#define DEFAULT_SCAN_DEPTH 1u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_AUTO_REMOVE_MISSING_DELAY_SECONDS 300u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
|
||||
|
||||
#define FAN_TARGET_TEMPERATURE_SYSTEM 0u
|
||||
#define MIN_FAN_TARGET_TEMPERATURE_C 50u
|
||||
#define MAX_FAN_TARGET_TEMPERATURE_C 91u
|
||||
#define MAX_PENDING 512
|
||||
#define MAX_IMAGE_MOUNTS 256
|
||||
#define MAX_IMAGE_MOUNTS 2048
|
||||
#define MAX_IMAGE_TITLES 8192
|
||||
#define MAX_IMAGE_MODE_RULES 128
|
||||
#define MAX_KSTUFF_TITLE_RULES 128
|
||||
#define MAX_FAKELIB_EXCLUDE_RULES 128
|
||||
@@ -22,11 +26,22 @@
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
|
||||
#define MAX_IMAGE_CHAIN_DEPTH 8
|
||||
|
||||
#define LVD_RELEASE_WAIT_MAX_US 15000000u
|
||||
#define LVD_RELEASE_WAIT_POLL_US 500000u
|
||||
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
|
||||
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
|
||||
// Keep a dead game PID eligible for ExitSpawn/LoadExec replacement while the
|
||||
// title sandbox still exists. Once the sandbox disappears, retain a short
|
||||
// grace window so a transient sandbox transition cannot tear down the runtime
|
||||
// before the replacement NOTE_EXEC is resolved.
|
||||
#define GAME_PROCESS_HANDOFF_GRACE_US 1000000u
|
||||
// Keep a slow fallback in case the /mnt/sandbox vnode watch is revoked.
|
||||
#define GAME_PROCESS_HANDOFF_FALLBACK_POLL_US 5000000u
|
||||
#define GAME_SLEEP_EXIT_TIMEOUT_US 5000000u
|
||||
#define RUNTIME_RESUME_GRACE_US 10000000u
|
||||
#define RUNTIME_RESUME_POLL_US 100000u
|
||||
#define KSTUFF_RESTORE_RETRY_US 1000000u
|
||||
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
|
||||
#define MIN_SCAN_DEPTH 1u
|
||||
@@ -34,6 +49,8 @@
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
#define MAX_SCAN_INTERVAL_SECONDS 3600u
|
||||
#define MAX_STABILITY_WAIT_SECONDS 3600u
|
||||
#define MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS 1u
|
||||
#define MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS 86400u
|
||||
#define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u
|
||||
|
||||
#define APP_DB_QUERY_BUSY_RETRIES 3
|
||||
@@ -45,6 +62,7 @@
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define MAX_API_BIND_ADDRESS 16
|
||||
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
|
||||
|
||||
#endif
|
||||
+8
-2
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_l10n.h"
|
||||
|
||||
typedef struct sm_error sm_error_t;
|
||||
|
||||
// Write a formatted line to the debug log.
|
||||
@@ -11,19 +13,23 @@ void log_debug(const char *fmt, ...);
|
||||
void sm_notifications_init(void);
|
||||
// Flush and close persistent log resources.
|
||||
void sm_log_shutdown(void);
|
||||
// Send a rich toast notification with packaged icon and version header.
|
||||
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...);
|
||||
// Send a localized rich toast notification with packaged icon and version header.
|
||||
void notify_system_rich_l10n(bool allow_in_quiet_mode, sm_l10n_key_t key, ...);
|
||||
// Send the "game installed" rich toast for the given title ID.
|
||||
void notify_game_installed_rich(const char *title_id);
|
||||
// Send an informational system notification (suppressed in quiet mode).
|
||||
void notify_system_info(const char *fmt, ...);
|
||||
void notify_system_info_l10n(sm_l10n_key_t key, ...);
|
||||
// Send a system notification with formatted text.
|
||||
void notify_system(const char *fmt, ...);
|
||||
void notify_system_l10n(sm_l10n_key_t key, ...);
|
||||
// Clear the last recorded subsystem error.
|
||||
void sm_error_clear(void);
|
||||
// Store the last subsystem error with formatted details.
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...);
|
||||
void sm_error_set_l10n(const char *subsystem, int code, const char *path,
|
||||
sm_l10n_key_t key, ...);
|
||||
// Return the last recorded subsystem error.
|
||||
const sm_error_t *sm_last_error(void);
|
||||
// Return whether the current error was already notified.
|
||||
|
||||
+281
-39
@@ -1,26 +1,229 @@
|
||||
#ifndef SM_MOUNT_DEFS_H
|
||||
#define SM_MOUNT_DEFS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define IMAGE_MOUNT_READ_ONLY 1
|
||||
|
||||
// 1 = use legacy /dev/mdctl backend for .exfat images, 0 = use LVD for all.
|
||||
// Build-time backend defaults. runtime config.ini can override both values.
|
||||
// 1 = legacy /dev/mdctl, 0 = /dev/lvdctl.
|
||||
#define EXFAT_ATTACH_USE_MDCTL 0
|
||||
// 1 = allow mounting .ffpkg images via /dev/mdctl, 0 = keep UFS on LVD.
|
||||
#define UFS_ATTACH_USE_MDCTL 0
|
||||
|
||||
// --- LVD Definitions ---
|
||||
// Kernel exposes base attach (V0), extended attach (V1/Attach2) and detach.
|
||||
// This code only builds V0 layer descriptors, so it uses the base attach path.
|
||||
// Wrapper-side raw attach presets:
|
||||
// - 0x8/0x9 -> single-image/save-data family (normalized 0x14/0x1C)
|
||||
// - 0xC/0xD -> download-data/LWFS family (normalized 0x16/0x1E)
|
||||
// raw bit3 (0x8) sets the DONT_MAP_BM-style mode, raw bit2 (0x4) switches to
|
||||
// the DD/LWFS family, and raw bit0 (0x1) selects the alternate preset inside a
|
||||
// family. Firmware pairs bit0 with read-only mount variants, but this is not a
|
||||
// standalone LVD readonly bit by itself.
|
||||
// image_type values accepted by validator: 0..0xC (13 values total).
|
||||
// layer source_type observed: 1=file, 2=device/special source (/dev/sbram0, char/block).
|
||||
// layer descriptor flag bit0 is "no bitmap file specified".
|
||||
/*
|
||||
* Mount profiles and cache layers
|
||||
* ================================================================
|
||||
*
|
||||
* LVD geometry
|
||||
* ------------
|
||||
* sector_size is the logical sector exported by /dev/lvdN. It controls BIO
|
||||
* alignment and is not a filesystem allocation block. ShadowMount defaults to
|
||||
* 512 bytes for exFAT and 4096 bytes for UFS/PFS.
|
||||
*
|
||||
* secondary_unit is the LVD mapping/split granularity. The optimized profile
|
||||
* uses 64 KiB for all supported image filesystems. The legacy 1.6 profile uses
|
||||
* sector_size except for exFAT, which already used 64 KiB. The value must be
|
||||
* divisible by sector_size; 64 KiB is required by the type-5 BFS sdimg path.
|
||||
*
|
||||
* LVD attach ABI and image types
|
||||
* ------------------------------
|
||||
* The kernel exposes base attach V0, extended attach V1/Attach2 and detach.
|
||||
* ShadowMount builds one complete V0 layer and marks it NO_BITMAP. source_type
|
||||
* 1 is a regular file; source_type 2 is a device/special source.
|
||||
*
|
||||
* Accepted image types and kernel names:
|
||||
* 0 Dfl, 1 GmI, 2 GmO, 3 PtcI, 4 PtcO, 5 Sv, 6 Trp, 7 Dwl,
|
||||
* 8 AcI, 9 AcO, 10 MaI, 11 MaO, 12 Pcdwl.
|
||||
*
|
||||
* Types 0/5/6/7/12 use lvdstart_vnode_direct. Types 1-4/8-11 use
|
||||
* lvdstart_vnode and resolve each secondary unit through layered bitmap and
|
||||
* optional OTBL metadata. Optimized ShadowMount profiles are:
|
||||
*
|
||||
* .exfat type 5 Sv, direct + BFS sdimg batching
|
||||
* fallback: type 0 Dfl
|
||||
* .ffpkg (UFS) type 5 Sv, direct + BFS sdimg batching
|
||||
* standalone .ffpfs type 5 Sv, direct
|
||||
* outer .ffpfsc type 9 AcO, layered package outer
|
||||
* inner .ffpfs/pfs_image.dat type 8 AcI, layered package inner
|
||||
*
|
||||
* ShadowMount deliberately uses type 5 with single/save attach flags for UFS
|
||||
* images so a standalone .ffpkg on BFS can use sdimg batching. Type 7 with
|
||||
* Download Data instead favors HDD request isolation. UFS nmount has
|
||||
* no separate download-data flag: 0x10000000 is the generic MNT_NOATIME bit,
|
||||
* not an UFS or DD mode. Do not substitute GmO/PtcO only to request the UFS
|
||||
* block-map cache: that replaces direct dispatch with layered dispatch. The
|
||||
* kernel requests that cache only for type 2/4, an HDD-class regular backing
|
||||
* vnode, and a backing filesystem that is UFS.
|
||||
*
|
||||
* Type-5 BFS sdimg fast path
|
||||
* -------------------------
|
||||
* Type 5 labels the LVD provider "Ssd" and may enable BFS sdimg when the image
|
||||
* is a regular file on internal BFS and secondary_unit is exactly 64 KiB. The
|
||||
* kernel also applies platform/storage-mode and lower-provider guards.
|
||||
*
|
||||
* When enabled, lvd_worker accepts only requests whose length is exactly
|
||||
* 64 KiB and whose backing-file offset is 64-KiB aligned. It collects the
|
||||
* initial request plus up to 30 matching queued requests and submits them via
|
||||
* bfs_iosession_rw_sdimg/BfsSdimg. All other requests fall back to the normal
|
||||
* vnode path. This is block-I/O batching, not an exFAT cache or decompressor.
|
||||
*
|
||||
* For exFAT keep sector_size at 512 and create the filesystem with a 64-KiB
|
||||
* allocation unit. Setting sector_size to 65536 does not request this mode;
|
||||
* the relevant LVD value is secondary_unit. The path is unavailable through
|
||||
* MD or when the image is backed by USB/UFS/PFS/exFAT instead of BFS.
|
||||
*
|
||||
* Raw flags and validator
|
||||
* -----------------------
|
||||
* Download-data flags exist only in the LVD attach ABI. ShadowMount calls
|
||||
* /dev/lvdctl directly and normalizes wrapper-style presets:
|
||||
* raw 0x8/0x9 -> single/save-data family 0x14/0x1c;
|
||||
* raw 0xc/0xd -> download-data family 0x16/0x1e.
|
||||
* Firmware pairs raw bit 0 with read-only variants; it is not an independent
|
||||
* LVD read-only flag. The normalized validator masks are:
|
||||
*
|
||||
* type 0,5,6,8-11: (flags & 0x92) == 0x10
|
||||
* type 1: (flags & 0x82) == 0x80
|
||||
* type 2,3,4: (flags & 0x82) == 0
|
||||
* type 7,12: (flags & 0x92) == 0x12
|
||||
*
|
||||
* Download Data HDD worker policy
|
||||
* -------------------------------
|
||||
* Download Data is normalized LVD bit 0x02 and is required by types 7 and 12.
|
||||
* On an HDD-class provider it makes LVD create two request workers instead of
|
||||
* one: worker 0 is labelled System (lvdhdsw), worker 1 Game (lvdhdgw). The LVD
|
||||
* start routine routes each BIO using the kernel classification byte at
|
||||
* BIO + 0x172: value 1 selects Game and every other value selects System. LVD
|
||||
* also tracks and drains the workers separately during shutdown. The flag is
|
||||
* therefore an HDD queue-isolation/QoS policy, not an on-disk-format property.
|
||||
*
|
||||
* Download Data does not select direct BIO dispatch (image_type does), enable
|
||||
* bitmap/OTBL interpretation, populate the UFS block-map cache (types 2/4),
|
||||
* unlock BFS sdimg batching (type 5), or enable PFS/compression caches. For a
|
||||
* standalone image, type 7 + Download Data is the HDD-oriented choice when
|
||||
* System/Game queue separation matters. Type 5 + Single/Save is preferable for
|
||||
* a BFS sdimg-backed .ffpkg when maximum sequential/batched throughput matters
|
||||
* and the two-queue HDD policy is not needed.
|
||||
*
|
||||
* Layer metadata and independent caches
|
||||
* -------------------------------------
|
||||
* LVD bitmap says whether a logical unit is present in a layer. V1 OTBL maps a
|
||||
* logical unit to a physical unit inside the layer file. It is unrelated to a
|
||||
* PFSC compressed-offset table.
|
||||
*
|
||||
* LVD bitmap/OTBL image-layer presence/remapping
|
||||
* UFS block-map cache backing UFS file blocks -> physical UFS blocks
|
||||
* PFS GDDR5 ioctl PFSC compressed offsets and optional full ICV
|
||||
*
|
||||
* The UFS cache depends on the filesystem containing the image, not UFS stored
|
||||
* inside .ffpkg, and is unavailable for BFS/PFS/exFAT backing files. The PFS
|
||||
* metadata ioctl applies to every nested image backed by pfs, ppr_pfs,
|
||||
* transaction_pfs, or a forwarding overlay; the filesystem inside the image
|
||||
* does not need to be PFS.
|
||||
*
|
||||
* PFS/PPR compression dispatch
|
||||
* ----------------------------
|
||||
* Classic pfs and ppr_pfs are distinct kernel read paths. A classic compressed
|
||||
* PFS block uses the classic PFSC header/table interpretation and the pfs path
|
||||
* dispatches its compressed payload to zlib. The PPR variant interprets the
|
||||
* PPR block metadata and can dispatch Kraken. LVD image_type, the nested
|
||||
* selector and PFS_GDDR5_CACHE_IOCTL do not select a decompressor and cannot
|
||||
* turn a classic zlib block header into a PPR/Kraken block. The image creator
|
||||
* must emit the matching on-disk block records and offset table.
|
||||
*
|
||||
* Compression is decided per block. Files excluded from compression and blocks
|
||||
* that are not smaller after compression are stored as raw blocks but remain
|
||||
* represented in the same image offset mapping. This is independent of the
|
||||
* mount-time caches described below.
|
||||
*
|
||||
* PFS compression caches and big-app reserve
|
||||
* ------------------------------------------
|
||||
* Three independent mechanisms have similar historical GDDR5/SBRAM cache
|
||||
* names and must not be treated as one allocation:
|
||||
*
|
||||
* 1. A compressed PFS/PFSC mount can allocate its own cmp_offtbl cache. The
|
||||
* kernel message
|
||||
*
|
||||
* pfs_allocate_cmp_offtbl_cache(): cmp_offtbl is cached
|
||||
*
|
||||
* confirms this mount-local compressed-block offset table. It is created
|
||||
* by the PFS mount path and is not evidence that a big-app memory reserve
|
||||
* exists.
|
||||
*
|
||||
* 2. PFS_GDDR5_CACHE_IOCTL can be issued on a nested image's backing vnode
|
||||
* before LVD attach. The optimized profile requests it automatically; the
|
||||
* legacy profile requires nested_pfs_index_cache=1. It requests the
|
||||
* containing PFS/PPR read path to cache the
|
||||
* compressed-offset table used to reach that vnode's data; it does not
|
||||
* inspect or impose a format on the bytes stored inside the file. Thus the
|
||||
* nested file may contain PFS, UFS, exFAT or arbitrary block data and need
|
||||
* not itself be compressed. The hard requirement for the offset request is
|
||||
* that the containing PFS/PPR image has its compressed-image flag set. On
|
||||
* an uncompressed containing image the kernel logs "NOT a compressed
|
||||
* image" and accepts the request as a compatibility no-op.
|
||||
*
|
||||
* cmp_cache_units is a count of shared-cache allocation units, not bytes.
|
||||
* The compressed-offset pool uses 0x10000-byte units and has 600 units
|
||||
* (37.5 MiB) with at most eight simultaneous slots. A zero count makes the
|
||||
* kernel derive the required count from the on-disk offset-table geometry;
|
||||
* this is the preferred full-table/default request. An explicit nonzero
|
||||
* count limits the reservation and can force more LRU refills. EBUSY is
|
||||
* non-fatal: it means that this vnode already has a cache or that all eight
|
||||
* slots are occupied. ENOMEM means that the requested units exceed the
|
||||
* remaining total or contiguous shared-cache space.
|
||||
*
|
||||
* The optional full-ICV request is independent. It requires the containing
|
||||
* image's signed-image flag and usable ICV metadata; otherwise the kernel
|
||||
* rejects that half of the request. It also has its own eight-slot pool.
|
||||
* Neither half selects zlib/Kraken or changes the block-record format.
|
||||
*
|
||||
* 3. The 180-MiB big-app reserve is application-lifecycle memory. It is not
|
||||
* allocated by the ioctl, LVD, PFS nmount, EnsureBigAppBudget, or a visible
|
||||
* /dev/sbram device. During BEGIN_APP_MOUNT_EVENT the kernel allocates
|
||||
* 0x0B400000 bytes aligned to 0x200000 for selected category values:
|
||||
*
|
||||
* 0x0000F000, 0x0000F100, 0x0001F000, 0x0001F300,
|
||||
* 0x0002F000, 0x0200F100.
|
||||
*
|
||||
* The allocation belongs to the big-app/application context. PFS can bind
|
||||
* to it through the application lifecycle callbacks; userland is not given
|
||||
* a buffer pointer. EndAppMount tears the context down and releases the
|
||||
* reserve after the application pre-delete path. Consequently a managed
|
||||
* image stack must remain mounted until the matching EndAppMount hook, and
|
||||
* no explicit metadata-cache clear should run while its PFS vnode is live.
|
||||
*
|
||||
* For unsigned ShadowMount PFS profiles sigverify=0, so request only compressed
|
||||
* offsets (cache_full_icv=0). Signing does not make ordinary reads faster: it
|
||||
* enables integrity verification and can add work; a cached full ICV table
|
||||
* only reduces metadata lookup cost when verification is actually enabled.
|
||||
* Both compressed-offset caches are properties of the outer PFS read path.
|
||||
* They can accelerate an LVD-backed nested exFAT/UFS/PFS image because the
|
||||
* inner filesystem format is irrelevant at that layer.
|
||||
*
|
||||
* Backend and nmount policy
|
||||
* -------------------------
|
||||
* LVD is the default for every format. MD is a compatibility backend for
|
||||
* UFS/exFAT; it uses AUTOUNIT|ASYNC and configurable logical sectors (modern
|
||||
* UFS default 4096, legacy UFS default 512, exFAT default 512). Read-only state
|
||||
* is applied to the backing device and nmount. The shared nmount profiles are:
|
||||
*
|
||||
* UFS ufs + budgetid + async + noatime + automounted
|
||||
* exFAT exfatfs + large + static-timezone + async + noatime + ignoreacl
|
||||
* PFS pfs + AC + game + sigverify/playgo/disc=0 + zero ekpfs
|
||||
* + async + noatime
|
||||
*
|
||||
* The optimized profile passes only MNT_RDONLY (0x1) in nmount's numeric flags
|
||||
* argument. Legacy UFS also passes MNT_NOATIME, matching version 1.6. Generic
|
||||
* named options are decoded by the kernel as async=MNT_ASYNC (0x40),
|
||||
* noatime=MNT_NOATIME (0x10000000), and automounted=MNT_AUTOMOUNTED
|
||||
* (0x200000000). UFS options compressedfile, iochunk, largewrite and related
|
||||
* zone-aware modes are separate filesystem features and are not DD flags.
|
||||
*
|
||||
* force is appended only for explicit recovery. Unsigned PFS keeps signature
|
||||
* verification disabled. The optimized profile primes every nested image on
|
||||
* PFS before LVD attach; the legacy profile does so only when explicitly
|
||||
* enabled.
|
||||
*/
|
||||
|
||||
#define LVD_CTRL_PATH "/dev/lvdctl"
|
||||
#define MD_CTRL_PATH "/dev/mdctl"
|
||||
#define SCE_LVD_IOC_ATTACH_V0 0xC0286D00
|
||||
@@ -28,43 +231,61 @@
|
||||
#define SCE_LVD_IOC_DETACH 0xC0286D01
|
||||
#define LVD_ATTACH_IO_VERSION_V0 0u
|
||||
#define LVD_ATTACH_IO_VERSION_V1 1u
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RO 0x9
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RW 0x8
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RO 0xD
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RW 0xC
|
||||
#
|
||||
#define LVD_ATTACH_RAW_BIT_RO_VARIANT 0x0001u
|
||||
#define LVD_ATTACH_RAW_BIT_INNER_PROFILE 0x0002u
|
||||
#define LVD_ATTACH_RAW_BIT_DOWNLOAD_DATA_PROFILE 0x0004u
|
||||
#define LVD_ATTACH_RAW_BIT_SINGLE_SAVE_PROFILE 0x0008u
|
||||
#define LVD_ATTACH_RAW_PASSTHROUGH_MASK 0x8000u
|
||||
|
||||
#define LVD_ATTACH_FLAG_DOWNLOAD_DATA_PROFILE 0x0002u
|
||||
#define LVD_ATTACH_FLAG_BASE 0x0004u
|
||||
#define LVD_ATTACH_FLAG_RO_VARIANT 0x0008u
|
||||
#define LVD_ATTACH_FLAG_SINGLE_SAVE_PROFILE 0x0010u
|
||||
#define LVD_ATTACH_FLAG_INNER_PROFILE 0x0080u
|
||||
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RW LVD_ATTACH_RAW_BIT_SINGLE_SAVE_PROFILE
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RO \
|
||||
(LVD_ATTACH_RAW_FLAGS_SINGLE_RW | LVD_ATTACH_RAW_BIT_RO_VARIANT)
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RW \
|
||||
(LVD_ATTACH_RAW_BIT_SINGLE_SAVE_PROFILE | \
|
||||
LVD_ATTACH_RAW_BIT_DOWNLOAD_DATA_PROFILE)
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RO \
|
||||
(LVD_ATTACH_RAW_FLAGS_DD_RW | LVD_ATTACH_RAW_BIT_RO_VARIANT)
|
||||
|
||||
#define LVD_ATTACH_FLAGS_SINGLE_RW \
|
||||
(LVD_ATTACH_FLAG_BASE | LVD_ATTACH_FLAG_SINGLE_SAVE_PROFILE)
|
||||
#define LVD_ATTACH_FLAGS_SINGLE_RO \
|
||||
(LVD_ATTACH_FLAGS_SINGLE_RW | LVD_ATTACH_FLAG_RO_VARIANT)
|
||||
#define LVD_ATTACH_FLAGS_DD_RW \
|
||||
(LVD_ATTACH_FLAGS_SINGLE_RW | LVD_ATTACH_FLAG_DOWNLOAD_DATA_PROFILE)
|
||||
#define LVD_ATTACH_FLAGS_DD_RO \
|
||||
(LVD_ATTACH_FLAGS_DD_RW | LVD_ATTACH_FLAG_RO_VARIANT)
|
||||
#define LVD_SECTOR_SIZE_EXFAT 512u
|
||||
#define LVD_SECTOR_SIZE_UFS 4096u
|
||||
#define LVD_SECTOR_SIZE_PFS 4096u
|
||||
#define LVD_SECONDARY_UNIT_SINGLE_IMAGE 0x10000u
|
||||
#define LVD_SECONDARY_UNIT_IMAGE_IO 0x10000u
|
||||
#define MD_SECTOR_SIZE_EXFAT 512u
|
||||
#define MD_SECTOR_SIZE_UFS 512u
|
||||
// Raw option bits are normalized by sceFsLvdAttachCommon before validation:
|
||||
// raw:0x1->norm:0x08, raw:0x2->norm:0x80, raw:0x4->norm:0x02, raw:0x8->norm:0x10.
|
||||
// The normalized masks are then checked against validator constraints (0x82/0x92).
|
||||
#define MD_SECTOR_SIZE_UFS_OPTIMIZED 4096u
|
||||
#define MD_SECTOR_SIZE_UFS_LEGACY 512u
|
||||
_Static_assert(LVD_SECONDARY_UNIT_IMAGE_IO % LVD_SECTOR_SIZE_EXFAT == 0,
|
||||
"exFAT LVD geometry must be aligned");
|
||||
_Static_assert(LVD_SECONDARY_UNIT_IMAGE_IO % LVD_SECTOR_SIZE_UFS == 0,
|
||||
"UFS LVD geometry must be aligned");
|
||||
_Static_assert(LVD_SECONDARY_UNIT_IMAGE_IO % LVD_SECTOR_SIZE_PFS == 0,
|
||||
"PFS LVD geometry must be aligned");
|
||||
#define LVD_ATTACH_IMAGE_TYPE_SINGLE 0
|
||||
#define LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA 7
|
||||
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 5
|
||||
#define LVD_ATTACH_IMAGE_TYPE_SAVE_DATA 5
|
||||
#define LVD_ATTACH_IMAGE_TYPE_DOWNLOAD_DATA 7
|
||||
#define PFS_NESTED_OUTER_IMG_TYPE 0x02u
|
||||
#define PFS_NESTED_INNER_IMG_TYPE 0x82u
|
||||
#define LVD_ATTACH_LAYER_COUNT 1
|
||||
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
|
||||
#define LVD_ENTRY_TYPE_FILE 1
|
||||
#define LVD_ENTRY_TYPE_SPECIAL 2
|
||||
#define LVD_ENTRY_FLAG_NO_BITMAP 0x1
|
||||
#define LVD_NODE_WAIT_US 100000
|
||||
#define LVD_NODE_WAIT_RETRIES 100
|
||||
#define UFS_NMOUNT_FLAG_RW 0x10000000u
|
||||
#define UFS_NMOUNT_FLAG_RO 0x10000001u
|
||||
|
||||
// --- devpfs/pfs option defaults ---
|
||||
// PFS nmount key/value variants observed in refs:
|
||||
// - fstype: "pfs", "transaction_pfs", "ppr_pfs"
|
||||
// - mkeymode: "SD"
|
||||
// - budgetid: "game"/"system" in init paths
|
||||
// - sigverify/playgo/disc: "0" or "1"
|
||||
// - optional keys in specific flows: ekpfs/eekpfs, eekc, pubkey_ver, key_ver,
|
||||
// finalized, ppkg_opt, sblock_offset, maxpkgszingib
|
||||
// PFS nmount values used by ShadowMount. Other firmware flows also expose
|
||||
// transaction_pfs/ppr_pfs, SD/GD key modes and system budget IDs.
|
||||
#define DEVPFS_BUDGET_GAME "game"
|
||||
#define DEVPFS_BUDGET_SYSTEM "system"
|
||||
#define DEVPFS_MKEYMODE_SD "SD"
|
||||
@@ -76,6 +297,27 @@
|
||||
#define PFS_MOUNT_PLAYGO 0
|
||||
#define PFS_MOUNT_DISC 0
|
||||
#define PFS_GDDR5_CACHE_IOCTL 0x80209168
|
||||
|
||||
// 0x80209168 is an IOC_IN request with a 0x20-byte payload and is accepted on a
|
||||
// read-only fd. Despite the userland "GDDR5 cache" name, the pfs and ppr_pfs
|
||||
// vnode handlers use it for two metadata caches belonging to the containing
|
||||
// filesystem: compressed offsets (only when its compressed-image flag is set)
|
||||
// and, optionally, full ICV (only for a signed image). The nested file's own
|
||||
// magic, filesystem and compression method are not inspected. Counts are
|
||||
// allocation units rather than bytes; zero asks the kernel to derive the full
|
||||
// size. The caller must have the kernel privilege checked as privilege 0x2ab.
|
||||
typedef struct pfs_gddr5_cache_request {
|
||||
uint8_t cache_cmp_offsets;
|
||||
uint8_t reserved01[7];
|
||||
uint64_t cmp_cache_units; // 0 = auto; each cmp unit is 0x10000 bytes
|
||||
uint8_t cache_full_icv;
|
||||
uint8_t reserved11[7];
|
||||
uint64_t icv_cache_units; // 0 = auto; independent signed-image ICV pool
|
||||
} pfs_gddr5_cache_request_t;
|
||||
|
||||
_Static_assert(sizeof(pfs_gddr5_cache_request_t) == 0x20,
|
||||
"PFS GDDR5 cache request must be 0x20 bytes");
|
||||
|
||||
// 4x64-bit PFS key encoded as 64 hex chars.
|
||||
#define PFS_ZERO_EKPFS_KEY_HEX \
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
@@ -19,7 +19,13 @@ bool resolve_device_from_mount(const char *mount_point,
|
||||
bool is_active_image_mount_point(const char *path);
|
||||
// Wait until /dev/lvd2 is no longer mounted during startup.
|
||||
bool wait_for_lvd_release(void);
|
||||
// Detach a previously attached MD or LVD unit.
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id);
|
||||
// Detach a previously attached MD or LVD unit. State and node identity are
|
||||
// required so an accepted asynchronous detach remains bound to one device.
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id,
|
||||
attached_unit_detach_state_t *state);
|
||||
// Wait for an accepted detach to remove its node, or detect unit reuse.
|
||||
bool wait_for_attached_unit_release(
|
||||
attach_backend_t backend, int unit_id,
|
||||
const attached_unit_detach_state_t *state);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef SM_MOUNT_DIAG_H
|
||||
#define SM_MOUNT_DIAG_H
|
||||
|
||||
// Log read-only evidence explaining why unmount(path) returned EBUSY.
|
||||
void sm_mount_diag_log_busy(const char *mount_point);
|
||||
|
||||
#endif
|
||||
@@ -18,6 +18,9 @@ bool is_pfsc_image_mount_base_or_child(const char *path);
|
||||
bool path_matches_root_or_child(const char *path, const char *root);
|
||||
// Return true when a path lives on USB-backed storage.
|
||||
bool is_usb_storage_path(const char *path);
|
||||
// Return true when the path's USB root is mounted, not just an empty mount
|
||||
// point under /mnt.
|
||||
bool usb_storage_root_mounted(const char *path);
|
||||
// Build "<scan_path>/backports" for a managed scan root.
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
|
||||
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
|
||||
#define DEFAULT_EMULATORS_PATH "/data/shadowmount/emus"
|
||||
#define DEFAULT_AMPR_UPDATE_URL \
|
||||
"https://github.com/drakmor/ampr_emu/releases/latest/download/" \
|
||||
"libSceAmpr.sprx"
|
||||
#define FAKELIB_CACHE_PATH "/data/shadowmount/cache"
|
||||
#define LOG_DIR "/data/shadowmount"
|
||||
#define LOG_FILE "/data/shadowmount/debug.log"
|
||||
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
|
||||
@@ -13,6 +18,7 @@
|
||||
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
|
||||
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
|
||||
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
|
||||
#define IMAGE_INDEX_FILE_PATH "/data/shadowmount/image_index.bin"
|
||||
#define APPMETA_BASE "/user/appmeta"
|
||||
#define APP_BASE "/user/app"
|
||||
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
|
||||
|
||||
@@ -41,12 +41,20 @@ static inline uint32_t sm_firmware_major_version(void) {
|
||||
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
|
||||
}
|
||||
|
||||
static inline bool sm_set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallAll(void);
|
||||
int sceAppInstUtilAppInstallAll(void *reserved);
|
||||
int sceAppInstUtilAppUnInstall(const char *title_id);
|
||||
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
|
||||
int sceKernelUsleep(unsigned int microseconds);
|
||||
int32_t sceSystemServiceParamGetInt(int32_t param_id, int32_t *value);
|
||||
int sceUserServiceInitialize(void *);
|
||||
void sceUserServiceTerminate(void);
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ bool should_stop_requested(void);
|
||||
void request_shutdown_stop(const char *reason);
|
||||
// Return true when the payload is paused for system suspend/resume.
|
||||
bool runtime_sleep_mode_active(void);
|
||||
// Return true while a launch may briefly wait for USB storage after resume.
|
||||
bool runtime_resume_grace_active(void);
|
||||
// Enter or leave sleep mode with a descriptive source string.
|
||||
// Returns true when the runtime sleep state changed.
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason);
|
||||
|
||||
+14
-2
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Unmount and clean up mounts whose backing sources disappeared.
|
||||
@@ -21,7 +23,17 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out);
|
||||
// Mount stable backport overlays for already mounted titles.
|
||||
void mount_backport_overlays(bool *unstable_found_out);
|
||||
// Resolve the source directory of a title backport without mounting it.
|
||||
bool resolve_backport_path_for_title(const char *title_id,
|
||||
const char *owning_scan_root,
|
||||
char backport_path[MAX_PATH]);
|
||||
// Ensure a stable backport overlay is mounted when one is present.
|
||||
// Absence is harmless; an unusable or failed required overlay returns false.
|
||||
bool mount_backport_overlay_for_title(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *owning_scan_root,
|
||||
bool *unstable_found_out);
|
||||
// Remove discovery title/image mounts while preserving persistent metadata.
|
||||
bool release_scan_runtime_mounts(void);
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,8 @@
|
||||
bool sm_scanner_init(void);
|
||||
// Wake a blocked scanner wait. Safe to call from signal context.
|
||||
void sm_scanner_wake(void);
|
||||
// Return true after scanner-owned USB directory descriptors are closed.
|
||||
bool sm_scanner_usb_watches_suspended(void);
|
||||
// Run the initial full synchronization cycle.
|
||||
bool sm_scanner_run_startup_sync(void);
|
||||
// Run the steady-state scanner wait/scan loop until shutdown is requested.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef SM_SHELLCORE_HOOKS_H
|
||||
#define SM_SHELLCORE_HOOKS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Install/remove the fail-open SceShellCore lifecycle bridge.
|
||||
bool sm_shellcore_hooks_start(void);
|
||||
void sm_shellcore_hooks_stop(void);
|
||||
|
||||
// Return whether this firmware uses the internal TitleDir bridge.
|
||||
bool sm_shellcore_install_bridge_enabled(void);
|
||||
|
||||
// Invoke the internal TitleDir path through one armed AppInstallAll dispatch.
|
||||
// Returns false when the bridge is unavailable or was not consumed.
|
||||
bool sm_shellcore_install_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef SM_SHELLCORE_OFFSETS_H
|
||||
#define SM_SHELLCORE_OFFSETS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_TARGET_LAUNCH_APP = 0,
|
||||
SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR,
|
||||
SM_SHELLCORE_TARGET_INSTALL_ALL,
|
||||
SM_SHELLCORE_TARGET_COUNT
|
||||
} sm_shellcore_target_t;
|
||||
|
||||
typedef struct {
|
||||
uintptr_t offset;
|
||||
uint8_t patch_size;
|
||||
} sm_shellcore_target_offset_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t firmware;
|
||||
const char *name;
|
||||
sm_shellcore_target_offset_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
} sm_shellcore_firmware_offsets_t;
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *
|
||||
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware);
|
||||
const char *sm_shellcore_target_name(sm_shellcore_target_t target);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SM_SHELLCORE_PROTOCOL_H
|
||||
#define SM_SHELLCORE_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
#include "sm_shellcore_protocol_defs.h"
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_REQUEST_LAUNCH = SM_SHELLCORE_REQUEST_LAUNCH_VALUE,
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED =
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED_VALUE
|
||||
} sm_shellcore_request_op_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t operation;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} sm_shellcore_request_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t status;
|
||||
} sm_shellcore_response_t;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef SM_SHELLCORE_PROTOCOL_DEFS_H
|
||||
#define SM_SHELLCORE_PROTOCOL_DEFS_H
|
||||
|
||||
/* Shared by C and the position-independent ShellCore bridge assembly. */
|
||||
#define SM_SHELLCORE_SOCKET_PATH "/system_tmp/shadowmount.sock"
|
||||
#define SM_SHELLCORE_PROTOCOL_MAGIC 0x534d5343
|
||||
#define SM_SHELLCORE_PROTOCOL_VERSION 1
|
||||
#define SM_SHELLCORE_REQUEST_LAUNCH_VALUE 1
|
||||
#define SM_SHELLCORE_REQUEST_LAUNCH_FAILED_VALUE 2
|
||||
#define SM_SHELLCORE_REQUEST_TITLE_ID_SIZE 32
|
||||
#define SM_SHELLCORE_REQUEST_SIZE 40
|
||||
#define SM_SHELLCORE_RESPONSE_SIZE 4
|
||||
#define SM_SHELLCORE_BRIDGE_INSTALL_DIR_SIZE 1024
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef SM_SHELLCORE_REMOTE_H
|
||||
#define SM_SHELLCORE_REMOTE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "sm_shellcore_offsets.h"
|
||||
|
||||
typedef struct {
|
||||
pid_t pid;
|
||||
uintptr_t image_base;
|
||||
uintptr_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
const sm_shellcore_firmware_offsets_t *offsets;
|
||||
} sm_shellcore_remote_t;
|
||||
|
||||
// Resolve and validate every lifecycle target in a running SceShellCore.
|
||||
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out);
|
||||
// Stop/continue a remote process for register-safe syscall injection.
|
||||
bool sm_remote_process_attach(pid_t pid);
|
||||
bool sm_remote_process_detach(pid_t pid);
|
||||
// Manage anonymous memory while the remote process is attached.
|
||||
uintptr_t sm_remote_process_map(pid_t pid, size_t size);
|
||||
bool sm_remote_process_unmap(pid_t pid, uintptr_t address, size_t size);
|
||||
// Read or write another process while handling post-8.20 write restrictions.
|
||||
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
|
||||
size_t size);
|
||||
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
|
||||
size_t size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef SM_SHELLCORE_SERVICE_H
|
||||
#define SM_SHELLCORE_SERVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
// Start/stop the local Unix-socket owner used by the SceShellCore bridge.
|
||||
bool sm_shellcore_service_start(void);
|
||||
void sm_shellcore_service_stop(void);
|
||||
|
||||
// Release transient title/image mounts after launch failure or explicit API use.
|
||||
bool sm_shellcore_release_title_runtime(const char *title_id);
|
||||
// Public API entry points. Return 0 or a positive errno value.
|
||||
int sm_shellcore_mount_title_runtime(const char *title_id);
|
||||
int sm_shellcore_unmount_title_runtime(const char *title_id);
|
||||
// Bind the prepared managed title to the app id stored in LncApplication.
|
||||
void sm_shellcore_service_bind_prepared_app(const char *title_id,
|
||||
uint32_t app_id, pid_t pid);
|
||||
// Publish process exit while ShellCore still owns the title sandbox and copy
|
||||
// the matching owned title to title_id_out.
|
||||
// Returns true when an owned incoming/outgoing title became exit-pending.
|
||||
bool sm_shellcore_service_note_game_exit(pid_t pid,
|
||||
char title_id_out[MAX_TITLE_ID]);
|
||||
// Release exit-pending runtime stacks whose /mnt/sandbox/<TITLE_ID>_* entries
|
||||
// have all disappeared. Returns 0 when no exit-pending runtime remains, EAGAIN
|
||||
// while a sandbox remains, or another positive errno after a release failure.
|
||||
int sm_shellcore_service_release_exited_titles(void);
|
||||
// Return true while ShellCore still exposes a sandbox for the title.
|
||||
bool sm_shellcore_service_title_sandbox_exists(const char *title_id);
|
||||
// Ensure that a managed title has its image/nullfs/backport runtime stack.
|
||||
// Unmanaged stock titles are treated as already ready.
|
||||
bool sm_shellcore_ensure_title_runtime(const char *title_id);
|
||||
// Return true when the named title owns an incoming or outgoing runtime mount.
|
||||
bool sm_shellcore_service_title_is_prepared(const char *title_id);
|
||||
// Return true while ShellCore owns any title mount prepared for launch or exit.
|
||||
bool sm_shellcore_service_has_prepared_mount(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef SM_SOCKET_IO_H
|
||||
#define SM_SOCKET_IO_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
bool sm_socket_read_full(int fd, void *buffer, size_t size);
|
||||
bool sm_socket_write_full(int fd, const void *buffer, size_t size);
|
||||
|
||||
#endif
|
||||
+36
-4
@@ -103,31 +103,63 @@ typedef enum {
|
||||
ATTACH_BACKEND_MD,
|
||||
} attach_backend_t;
|
||||
|
||||
typedef enum {
|
||||
ATTACHED_DEVICE_RELEASED = 0,
|
||||
ATTACHED_DEVICE_ATTACHING,
|
||||
ATTACHED_DEVICE_MOUNTED,
|
||||
ATTACHED_DEVICE_DETACH_REQUESTED,
|
||||
} attached_device_state_t;
|
||||
|
||||
typedef struct {
|
||||
// A detach ioctl was accepted for this specific device instance. Once set,
|
||||
// the same unit number must not receive another detach request because the
|
||||
// kernel may already have reused it for a different application.
|
||||
bool requested;
|
||||
bool node_identity_valid;
|
||||
uint64_t node_device;
|
||||
uint64_t node_inode;
|
||||
uint64_t node_rdev;
|
||||
} attached_unit_detach_state_t;
|
||||
|
||||
typedef struct runtime_config {
|
||||
bool debug_enabled;
|
||||
bool quiet_mode;
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool persistent_image_mounts;
|
||||
bool app_install_all_enabled;
|
||||
bool app_install_all_forced;
|
||||
bool auto_remove_missing_games;
|
||||
bool auto_remove_games_with_dlc;
|
||||
bool backport_fakelib_enabled;
|
||||
bool global_fakelib_enabled;
|
||||
bool global_fakelib_mount_first;
|
||||
bool global_fakelib_game_priority;
|
||||
bool update_emulators_enabled;
|
||||
bool auto_update_ampr_enabled;
|
||||
bool kstuff_game_auto_toggle;
|
||||
bool kstuff_crash_detection_enabled;
|
||||
bool legacy_recursive_scan_forced;
|
||||
bool watch_subdirs;
|
||||
bool watch_image_files;
|
||||
char api_bind_address[MAX_API_BIND_ADDRESS];
|
||||
uint32_t api_port;
|
||||
char global_fakelib_path[MAX_PATH];
|
||||
char emulators_path[MAX_PATH];
|
||||
char ampr_update_url[MAX_PATH];
|
||||
uint32_t global_fakelib_exclude_title_count;
|
||||
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
|
||||
uint32_t scan_depth;
|
||||
uint32_t scan_interval_us;
|
||||
uint32_t stability_wait_seconds;
|
||||
uint32_t auto_remove_missing_delay_seconds;
|
||||
uint32_t kstuff_pause_delay_image_seconds;
|
||||
uint32_t kstuff_pause_delay_direct_seconds;
|
||||
uint32_t fan_target_temperature_c;
|
||||
int32_t language_id;
|
||||
attach_backend_t exfat_backend;
|
||||
attach_backend_t ufs_backend;
|
||||
bool legacy_mount_ufs;
|
||||
bool legacy_mount_exfat;
|
||||
bool legacy_mount_pfs;
|
||||
bool legacy_mount_pfsc;
|
||||
bool nested_pfs_index_cache_enabled;
|
||||
uint32_t lvd_sector_exfat;
|
||||
uint32_t lvd_sector_ufs;
|
||||
uint32_t lvd_sector_pfs;
|
||||
|
||||
+4
-18
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# For WSL2/Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
|
||||
# Create an exFAT image from a directory
|
||||
# Create a 64 KiB-cluster exFAT image for the LVD type-5/BFS sdimg profile.
|
||||
# Usage: mkexfat.sh <input_dir> [output_file]
|
||||
|
||||
set -e
|
||||
@@ -28,9 +28,8 @@ fi
|
||||
# - FAT + allocation bitmap estimates from cluster count
|
||||
# - directory/entry metadata estimate
|
||||
# - fixed metadata and runtime headroom
|
||||
CLUSTER_SIZE=32768
|
||||
MKFS_CLUSTER_ARG="32K"
|
||||
LARGE_FILE_THRESHOLD=$((1024 * 1024))
|
||||
CLUSTER_SIZE=65536
|
||||
MKFS_CLUSTER_ARG="64K"
|
||||
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
|
||||
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
|
||||
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
|
||||
@@ -42,19 +41,6 @@ DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
|
||||
awk '{s += $1} END {print s + 0}')
|
||||
|
||||
AVG_FILE_BYTES=0
|
||||
if [ "$FILE_COUNT" -gt 0 ]; then
|
||||
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
|
||||
fi
|
||||
|
||||
# exFAT profile selection (same idea as UFS2 profile):
|
||||
# - large-file sets: 64K cluster
|
||||
# - small/mixed-file sets: 32K cluster
|
||||
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
|
||||
CLUSTER_SIZE=65536
|
||||
MKFS_CLUSTER_ARG="64K"
|
||||
fi
|
||||
|
||||
DATA_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
|
||||
awk -v cls="$CLUSTER_SIZE" '{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}')
|
||||
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
|
||||
@@ -82,7 +68,7 @@ MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "exFAT profile: -c $MKFS_CLUSTER_ARG (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "exFAT LVD/BFS profile: -c $MKFS_CLUSTER_ARG (fixed 64 KiB cluster)"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
truncate -s "${MB}M" "$OUTPUT"
|
||||
|
||||
+2
-15
@@ -29,8 +29,7 @@ fi
|
||||
|
||||
# ── sizing constants ──────────────────────────────────────────────────────────
|
||||
|
||||
CLUSTER_SIZE=32768
|
||||
LARGE_FILE_THRESHOLD=$((1024 * 1024))
|
||||
CLUSTER_SIZE=65536
|
||||
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
|
||||
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
|
||||
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
|
||||
@@ -66,18 +65,6 @@ EOF
|
||||
|
||||
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
|
||||
AVG_FILE_BYTES=0
|
||||
if [ "$FILE_COUNT" -gt 0 ]; then
|
||||
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
|
||||
fi
|
||||
|
||||
# exFAT profile selection:
|
||||
# - large-file sets: 64K cluster
|
||||
# - small/mixed-file sets: 32K cluster
|
||||
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
|
||||
CLUSTER_SIZE=65536
|
||||
fi
|
||||
|
||||
# DATA_BYTES re-reads the already-collected sizes from the temp file — no second traversal
|
||||
DATA_BYTES=$(awk -v cls="$CLUSTER_SIZE" \
|
||||
'{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}' "$SIZES_FILE")
|
||||
@@ -102,7 +89,7 @@ MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "exFAT profile: -b ${CLUSTER_SIZE} (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "exFAT LVD/BFS profile: -b ${CLUSTER_SIZE} (fixed 64 KiB cluster)"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
# ── create, attach, format, mount ────────────────────────────────────────────
|
||||
|
||||
+133
-39
@@ -5,9 +5,13 @@
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_ampr_updater.h"
|
||||
#include "sm_api_service.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
@@ -28,6 +32,10 @@
|
||||
#define SHADOWMOUNT_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
#ifndef SHADOWMOUNT_BUILD_TIME
|
||||
#define SHADOWMOUNT_BUILD_TIME __DATE__ " " __TIME__
|
||||
#endif
|
||||
|
||||
#define PAYLOAD_NAME "shadowmountplus.elf"
|
||||
#define BACKPORK_PROCESS_NAME "backpork.elf"
|
||||
#define BACKPORK_PROCESS_NAME_ALT "ps5-backpork.elf"
|
||||
@@ -44,6 +52,7 @@
|
||||
static volatile sig_atomic_t g_stop_requested = 0;
|
||||
static atomic_bool g_shutdown_on_going_stop_requested = false;
|
||||
static atomic_bool g_runtime_sleep_mode_active = false;
|
||||
static atomic_uint_fast64_t g_runtime_resume_grace_deadline_us = 0;
|
||||
static _Atomic(uintptr_t) g_shutdown_stop_reason_bits = 0;
|
||||
static atomic_uint_fast64_t g_next_stop_file_poll_us = 0;
|
||||
static pthread_mutex_t g_runtime_mount_state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
@@ -77,6 +86,8 @@ void install_signal_handlers(void) {
|
||||
sigaction(SIGHUP, &sa, NULL);
|
||||
sigaction(SIGQUIT, &sa, NULL);
|
||||
sigaction(SIGABRT, &sa, NULL);
|
||||
sa.sa_handler = SIG_IGN;
|
||||
sigaction(SIGPIPE, &sa, NULL);
|
||||
}
|
||||
|
||||
bool should_stop_requested(void) {
|
||||
@@ -126,6 +137,13 @@ bool runtime_sleep_mode_active(void) {
|
||||
memory_order_acquire);
|
||||
}
|
||||
|
||||
bool runtime_resume_grace_active(void) {
|
||||
uint64_t deadline_us = atomic_load_explicit(
|
||||
&g_runtime_resume_grace_deadline_us, memory_order_acquire);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
return deadline_us != 0 && now_us != 0 && now_us < deadline_us;
|
||||
}
|
||||
|
||||
static void clear_scan_now_request(void) {
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
g_scan_now.reason[0] = '\0';
|
||||
@@ -133,13 +151,29 @@ static void clear_scan_now_request(void) {
|
||||
}
|
||||
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason) {
|
||||
bool previous = atomic_exchange_explicit(&g_runtime_sleep_mode_active, active,
|
||||
memory_order_acq_rel);
|
||||
if (previous == active)
|
||||
return false;
|
||||
|
||||
if (active)
|
||||
if (active) {
|
||||
bool previous = atomic_exchange_explicit(&g_runtime_sleep_mode_active, true,
|
||||
memory_order_acq_rel);
|
||||
if (previous)
|
||||
return false;
|
||||
clear_scan_now_request();
|
||||
atomic_store_explicit(&g_runtime_resume_grace_deadline_us, 0,
|
||||
memory_order_release);
|
||||
} else {
|
||||
if (!runtime_sleep_mode_active())
|
||||
return false;
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t deadline_us =
|
||||
now_us != 0 ? now_us + RUNTIME_RESUME_GRACE_US : 0;
|
||||
// Publish the deadline before allowing launch requests to observe that
|
||||
// sleep mode ended.
|
||||
atomic_store_explicit(&g_runtime_resume_grace_deadline_us, deadline_us,
|
||||
memory_order_release);
|
||||
bool previous = atomic_exchange_explicit(&g_runtime_sleep_mode_active,
|
||||
false, memory_order_acq_rel);
|
||||
if (!previous)
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown sleep source";
|
||||
@@ -161,20 +195,17 @@ void runtime_mount_state_unlock(void) {
|
||||
void request_scan_now(const char *reason) {
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown scan source";
|
||||
bool resume_scan =
|
||||
strcmp(resolved_reason, "SceSystemStateMgrInfo=WORKING") == 0;
|
||||
if (runtime_sleep_mode_active() && !resume_scan)
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
char log_reason[sizeof(g_scan_now.reason)];
|
||||
bool should_log = !resume_scan;
|
||||
bool should_log = false;
|
||||
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
if (g_scan_now.reason[0] == '\0') {
|
||||
(void)strlcpy(g_scan_now.reason, resolved_reason, sizeof(g_scan_now.reason));
|
||||
(void)strlcpy(log_reason, g_scan_now.reason, sizeof(log_reason));
|
||||
} else {
|
||||
should_log = false;
|
||||
should_log = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
|
||||
@@ -250,12 +281,23 @@ pid_t find_pid_by_name(const char *name, bool exclude_self) {
|
||||
pid_t found_pid = 0;
|
||||
uint8_t *ptr = buf;
|
||||
uint8_t *end = buf + buf_size;
|
||||
while (ptr < end) {
|
||||
int ki_structsize = *(int *)ptr;
|
||||
pid_t ki_pid = *(pid_t *)&ptr[KINFO_PID_OFFSET];
|
||||
while ((size_t)(end - ptr) >= sizeof(int)) {
|
||||
int ki_structsize = 0;
|
||||
memcpy(&ki_structsize, ptr, sizeof(ki_structsize));
|
||||
if (ki_structsize <= KINFO_TDNAME_OFFSET ||
|
||||
(size_t)ki_structsize > (size_t)(end - ptr)) {
|
||||
found_pid = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
pid_t ki_pid = 0;
|
||||
memcpy(&ki_pid, &ptr[KINFO_PID_OFFSET], sizeof(ki_pid));
|
||||
const char *ki_tdname = (const char *)&ptr[KINFO_TDNAME_OFFSET];
|
||||
size_t tdname_size = (size_t)ki_structsize - KINFO_TDNAME_OFFSET;
|
||||
ptr += ki_structsize;
|
||||
if ((!exclude_self || ki_pid != mypid) && strcmp(ki_tdname, name) == 0) {
|
||||
if ((!exclude_self || ki_pid != mypid) &&
|
||||
strnlen(ki_tdname, tdname_size) < tdname_size &&
|
||||
strcmp(ki_tdname, name) == 0) {
|
||||
found_pid = ki_pid;
|
||||
break;
|
||||
}
|
||||
@@ -321,20 +363,21 @@ static void log_non_empty_scan_paths(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void ensure_kstuff_noautomount_file(void) {
|
||||
static bool ensure_kstuff_noautomount_file(void) {
|
||||
if (path_exists(KSTUFF_NOAUTOMOUNT_FILE))
|
||||
return;
|
||||
return true;
|
||||
|
||||
int fd = open(KSTUFF_NOAUTOMOUNT_FILE, O_RDONLY | O_CREAT, 0666);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
printf("[KSTUFF] Created startup sentinel: %s\n",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return;
|
||||
int fd = open(KSTUFF_NOAUTOMOUNT_FILE, O_WRONLY | O_CREAT, 0666);
|
||||
if (fd < 0) {
|
||||
printf("[KSTUFF] Failed to create %s: %s\n", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
(void)close(fd);
|
||||
|
||||
printf("[KSTUFF] Failed to create %s: %s\n", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
printf("[KSTUFF] Created automount sentinel: %s\n",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_buffer_to_fd(int fd, const unsigned char *buf, size_t size) {
|
||||
@@ -381,7 +424,19 @@ static void ensure_runtime_config_file(void) {
|
||||
printf("[CFG] Created default config from template: %s\n", CONFIG_FILE);
|
||||
}
|
||||
|
||||
static void cleanup_kstuff_noautomount_files(void) {
|
||||
static void cleanup_kstuff_noautomount_file(void) {
|
||||
pid_t replacement_pid = find_pid_by_name(PAYLOAD_NAME, true);
|
||||
if (replacement_pid > 0) {
|
||||
log_debug("[KSTUFF] replacement instance pid=%ld; keeping %s",
|
||||
(long)replacement_pid, KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return;
|
||||
}
|
||||
if (replacement_pid < 0) {
|
||||
log_debug("[KSTUFF] process enumeration unavailable; keeping %s",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unlink(KSTUFF_NOAUTOMOUNT_FILE) == 0) {
|
||||
log_debug("[KSTUFF] removed shutdown sentinel: %s",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
@@ -428,21 +483,31 @@ int main(void) {
|
||||
|
||||
mkdir(LOG_DIR, 0777);
|
||||
ensure_runtime_config_file();
|
||||
ensure_kstuff_noautomount_file();
|
||||
existing_pid = find_pid_by_name(PAYLOAD_NAME, true);
|
||||
if (existing_pid < 0) {
|
||||
printf("[RESTART] Failed to enumerate running processes.\n");
|
||||
sceUserServiceTerminate();
|
||||
return 1;
|
||||
}
|
||||
if (!ensure_kstuff_noautomount_file()) {
|
||||
sceUserServiceTerminate();
|
||||
return 1;
|
||||
}
|
||||
if (existing_pid > 0) {
|
||||
printf("[RESTART] Another instance is already running.\n");
|
||||
if (!wait_for_existing_instance_exit(existing_pid)) {
|
||||
printf("[KSTUFF] Keeping automount sentinel after failed handoff.\n");
|
||||
sceUserServiceTerminate();
|
||||
return 0;
|
||||
}
|
||||
restarted_previous_instance = true;
|
||||
}
|
||||
// Older builds removed the sentinel unconditionally during shutdown.
|
||||
// Recreate it after handoff before starting any mount work.
|
||||
if (!ensure_kstuff_noautomount_file()) {
|
||||
sceUserServiceTerminate();
|
||||
return 1;
|
||||
}
|
||||
syscall(SYS_thr_set_name, -1, PAYLOAD_NAME);
|
||||
|
||||
if (remove(KILL_FILE) == 0) {
|
||||
@@ -461,20 +526,30 @@ int main(void) {
|
||||
log_debug(
|
||||
"ShadowMount+ v%s exFAT/UFS/PFS/LVD/MD. "
|
||||
"FW: %s. "
|
||||
"Build: %s %s. "
|
||||
"Build: %s. "
|
||||
"Thx to VoidWhisper/Gezine/Earthonion/EchoStretch/Drakmor",
|
||||
SHADOWMOUNT_VERSION, firmware_version, __DATE__, __TIME__);
|
||||
SHADOWMOUNT_VERSION, firmware_version, SHADOWMOUNT_BUILD_TIME);
|
||||
if (restarted_previous_instance)
|
||||
log_debug("[RESTART] Previous instance stopped, continuing startup");
|
||||
load_runtime_config();
|
||||
sm_notifications_init();
|
||||
stop_conflicting_backpork();
|
||||
if (!sm_shellcore_flags_start())
|
||||
log_debug(" [SHELLFLAG] monitor unavailable");
|
||||
sm_mdbg_init();
|
||||
sm_kstuff_init();
|
||||
if (!sm_shellcore_service_start())
|
||||
log_debug(" [SHELLCORE] Unix socket service unavailable: %s",
|
||||
strerror(errno));
|
||||
else if (!sm_shellcore_hooks_start()) {
|
||||
log_debug(" [SHELLCORE] lifecycle hooks unavailable; stock behavior kept");
|
||||
notify_system_l10n(SM_L10N_SHELLCORE_HOOKS_FAILED);
|
||||
}
|
||||
if (!refresh_game_lifecycle_watcher())
|
||||
log_debug(" [GAME] lifecycle watcher unavailable");
|
||||
// Publish the initial AppFocus only after its lifecycle/kstuff consumers.
|
||||
if (!sm_shellcore_flags_start())
|
||||
log_debug(" [SHELLFLAG] monitor unavailable");
|
||||
if (!sm_ampr_updater_start())
|
||||
log_debug(" [AMPR] update service unavailable: %s", strerror(errno));
|
||||
|
||||
if (mkdir("/system_ex/app", 0777) != 0 && errno != EEXIST) {
|
||||
log_debug(" [MOUNT] failed to create /system_ex/app: %s", strerror(errno));
|
||||
@@ -483,14 +558,14 @@ int main(void) {
|
||||
log_debug(" [MOUNT] remount_system_ex failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
notify_system("ShadowMount+ v%s exFAT/UFS/PFS", SHADOWMOUNT_VERSION);
|
||||
notify_system_l10n(SM_L10N_STARTUP, SHADOWMOUNT_VERSION);
|
||||
log_non_empty_scan_paths();
|
||||
|
||||
if (runtime_config()->legacy_recursive_scan_forced) {
|
||||
notify_system_info("ShadowMount+: recursive_scan=1 deprecated, using scan_depth=2.");
|
||||
notify_system_info_l10n(SM_L10N_RECURSIVE_SCAN_DEPRECATED);
|
||||
} else if (runtime_config()->scan_depth > 1u) {
|
||||
notify_system_info("ShadowMount+: scan depth %u enabled.",
|
||||
runtime_config()->scan_depth);
|
||||
notify_system_info_l10n(SM_L10N_SCAN_DEPTH_ENABLED,
|
||||
runtime_config()->scan_depth);
|
||||
}
|
||||
|
||||
cleanup_mount_dirs();
|
||||
@@ -500,9 +575,11 @@ int main(void) {
|
||||
}
|
||||
|
||||
log_debug("[STARTUP] cleanup_staged_mount_links begin");
|
||||
runtime_mount_state_lock();
|
||||
cleanup_staged_mount_links();
|
||||
log_debug("[STARTUP] cleanup_duplicate_title_mounts begin");
|
||||
cleanup_duplicate_title_mounts();
|
||||
runtime_mount_state_unlock();
|
||||
if (!app_db_run_startup_maintenance())
|
||||
log_debug(" [DB] startup snd0info maintenance unavailable");
|
||||
log_debug("[STARTUP] scanner startup sync begin");
|
||||
@@ -511,19 +588,36 @@ int main(void) {
|
||||
goto shutdown;
|
||||
}
|
||||
log_debug("[STARTUP] scanner startup sync done");
|
||||
if (!sm_api_service_start())
|
||||
log_debug(" [API] HTTP/JSON service unavailable: %s", strerror(errno));
|
||||
sm_scanner_run_loop();
|
||||
|
||||
shutdown:
|
||||
sm_ampr_updater_stop();
|
||||
sm_api_service_stop();
|
||||
// Stop ShellCore producers before their lifecycle and mount-state owners.
|
||||
sm_shellcore_flags_stop();
|
||||
sm_shellcore_hooks_stop();
|
||||
stop_game_lifecycle_watcher();
|
||||
sm_shellcore_service_stop();
|
||||
sm_scanner_shutdown();
|
||||
sm_kstuff_shutdown();
|
||||
sm_mdbg_shutdown();
|
||||
cleanup_kstuff_noautomount_files();
|
||||
shutdown_title_mounts();
|
||||
if (!shutdown_image_mounts()) {
|
||||
bool title_mounts_released = shutdown_title_mounts();
|
||||
bool image_mounts_released = false;
|
||||
if (title_mounts_released) {
|
||||
image_mounts_released = shutdown_image_mounts();
|
||||
} else {
|
||||
log_debug("[SHUTDOWN] image teardown skipped while title layers remain");
|
||||
}
|
||||
if (!image_mounts_released) {
|
||||
log_debug("[SHUTDOWN] some image mounts or devices were not fully released");
|
||||
}
|
||||
if (title_mounts_released && image_mounts_released) {
|
||||
cleanup_kstuff_noautomount_file();
|
||||
} else {
|
||||
log_debug("[KSTUFF] keeping automount disabled after incomplete teardown");
|
||||
}
|
||||
shutdown_app_db();
|
||||
|
||||
if (atomic_load_explicit(&g_shutdown_on_going_stop_requested,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* PEM trust anchors for the certificate chains used by GitHub releases. */
|
||||
static char g_ampr_ca_sectigo_e46_usertrust[] =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDRjCCAsugAwIBAgIQGp6v7G3o4ZtcGTFBto2Q3TAKBggqhkjOPQQDAzCBiDEL\n"
|
||||
"MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\n"
|
||||
"eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\n"
|
||||
"JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjEwMzIy\n"
|
||||
"MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMP\n"
|
||||
"U2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIg\n"
|
||||
"QXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR2\n"
|
||||
"+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0\n"
|
||||
"NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6Oj\n"
|
||||
"ggEgMIIBHDAfBgNVHSMEGDAWgBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAdBgNVHQ4E\n"
|
||||
"FgQU0SLaTFnxS18mOKqd1u7rDcP7qWEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB\n"
|
||||
"/wQFMAMBAf8wHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBEGA1UdIAQK\n"
|
||||
"MAgwBgYEVR0gADBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVz\n"
|
||||
"dC5jb20vVVNFUlRydXN0RUNDQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwNQYI\n"
|
||||
"KwYBBQUHAQEEKTAnMCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3Qu\n"
|
||||
"Y29tMAoGCCqGSM49BAMDA2kAMGYCMQCMCyBit99vX2ba6xEkDe+YO7vC0twjbkv9\n"
|
||||
"PKpqGGuZ61JZryjFsp+DFpEclCVy4noCMQCwvZDXD/m2Ko1HA5Bkmz7YQOFAiNDD\n"
|
||||
"49IWa2wdT7R3DtODaSXH/BiXv8fwB9su4tU=\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
static char g_ampr_ca_isrg_root_yr_by_x1[] =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIF9DCCA9ygAwIBAgIRAPJLbRf52a18scn+p4eCaZ8wDQYJKoZIhvcNAQELBQAw\n"
|
||||
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
|
||||
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjYwNTEzMDAwMDAw\n"
|
||||
"WhcNMzIwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQ\n"
|
||||
"MA4GA1UEAxMHUm9vdCBZUjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\n"
|
||||
"ANvGJnN78CTJdWL3+eGfsLN5TrNBJs+VH9hRXqRbwxu9sGNiB0BD1fcOxbSUQCJI\n"
|
||||
"M1xE13Db+5Cw1w0s0EBYsvuIP/6joF0w8cuImbgR1OGgYbSQ4OpzI+DG8SGuTlcE\n"
|
||||
"873OCS+kh3srlo6vl43M5OJg4Aeo1sfHp6kTJDoIiFBNJAY+OKfX/FUvYKuhjT+n\n"
|
||||
"o49lmqmupSBI5PkBQiqrEGtWU5uxU/cQWHGu8jSjFBznZqvbNPLMXMLFxCb3WTfr\n"
|
||||
"JBXXjqvWG+v4bjzxjjeAtOlU7qarRDvNOyAuQYLln904M+faKx8hnLCpJ15ZqaEg\n"
|
||||
"cNlY+9MMWcC5yvL2A2j3l9+2buggZX+dOE91zYmIdawTvSZuVvlbRrAlLxIB6pwM\n"
|
||||
"BjneXCjYQ8+3BCCjssbSNpZU3hTcBDdhfAlEDlYr6pEatnMdmDT5BqnKC92bd0Eh\n"
|
||||
"M1fbLHioLccLCuievT8ZkPhZrq7Mii7gNXAcUEAR8+lzYal+9zTg7C5DALyVOeG/\n"
|
||||
"CqfRAMn1KSHCR0NSA6P8tn/mGRlnCct5rtVCLnVySVpU6H1qGg3DgTOuskf8eahT\n"
|
||||
"MiYbI5ezPJmO5ertalskQ1utp74+eDy92PI4ftHKTbq9IWhH4YZKh3WnJEIt+oQv\n"
|
||||
"lYZbY8tpEroKrFB6PFGzrJIDRyts4HqvuH52RFj2zv/BAgMBAAGjgeswgegwDgYD\n"
|
||||
"VR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMB\n"
|
||||
"Af8wHQYDVR0OBBYEFN7nW2DQIm1AKH0/DQH+pLVStFGUMB8GA1UdIwQYMBaAFHm0\n"
|
||||
"WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcwAoYW\n"
|
||||
"aHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAEDDAKMAgGBmeBDAECATAnBgNV\n"
|
||||
"HR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5jci5vcmcvMA0GCSqGSIb3DQEB\n"
|
||||
"CwUAA4ICAQA8spSI95KKfn2W6GMmDpHBJSPaLbsS3W93cijJCRCYAc1fsJgL1FIL\n"
|
||||
"7C0C9ecPOdcwB2fi0Dk2p94j9iTJCxmt5CFSKLRWwnXT2MMSXexVxqoVB79BdWPx\n"
|
||||
"VXETkVme/qYSAuKVHh5Ps+5BixgmwS1JkjSAc+MfrUbNssVEEnH0aEiAh+rotXAV\n"
|
||||
"JSP/Ye7LJPEwD9DWG72vVWbhAcuOf5OLjz57Ctk7MgQHynZ7+PlHJtajroCaIbtC\n"
|
||||
"r6tcZZaAwUQm+jQyeWdV+2hv9deOYFmKeQyjjcSrN5Nadrw+L9DZJLbA1HqeNvLh\n"
|
||||
"BgqpP0fvJq2N6EtD574N6eMI7uMsJTnji2UDz9el5XLSv9fqJMuDQtYVb2oTNoKp\n"
|
||||
"oUqhxPVC0aq4eG5MESaIdn8b5ZGSSeAJLMHXljEdlNza+ncfkviXk1POLnnFdvx8\n"
|
||||
"/gk6M374WbLWFXw8N141B/Rl/tINGfl1TxOIiqtiMYkL02RSGb1kq34BL9NPP27z\n"
|
||||
"RGMuHGnzS3hFIrRTfKxrzUZ9RzQWzEG3K6fJ3r2nqSltkeytis9DIBoFY9VmVyjL\n"
|
||||
"M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID\n"
|
||||
"yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
static char g_ampr_ca_usertrust_ecc[] =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\n"
|
||||
"MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\n"
|
||||
"eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\n"
|
||||
"JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\n"
|
||||
"MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\n"
|
||||
"Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\n"
|
||||
"VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\n"
|
||||
"aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\n"
|
||||
"I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\n"
|
||||
"o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\n"
|
||||
"A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\n"
|
||||
"VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\n"
|
||||
"zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\n"
|
||||
"RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
static char g_ampr_ca_isrg_root_x1[] =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
|
||||
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
|
||||
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
|
||||
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
|
||||
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
|
||||
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
|
||||
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
|
||||
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
|
||||
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
|
||||
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
|
||||
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
|
||||
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
|
||||
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
|
||||
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
|
||||
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
|
||||
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
|
||||
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
|
||||
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
|
||||
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
|
||||
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
|
||||
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
|
||||
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
|
||||
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
|
||||
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
|
||||
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
|
||||
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
|
||||
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
|
||||
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
|
||||
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
@@ -0,0 +1,962 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include "sm_ampr_updater.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
|
||||
#define AMPR_FILE_NAME "libSceAmpr.sprx"
|
||||
#define AMPR_TEMP_FILE_NAME ".libSceAmpr.sprx.download"
|
||||
#define AMPR_INITIAL_CHECK_DELAY_SECONDS 30u
|
||||
#define AMPR_UPDATE_INTERVAL_SECONDS (4u * 60u * 60u)
|
||||
#define AMPR_BUSY_RETRY_SECONDS (10u * 60u)
|
||||
#define AMPR_NET_POOL_SIZE (32u * 1024u)
|
||||
#define AMPR_PRIVATE_CA_COUNT 4u
|
||||
#define AMPR_SSL_POOL_SIZE \
|
||||
((256u + 4u * AMPR_PRIVATE_CA_COUNT) * 1024u)
|
||||
#define AMPR_HTTP_POOL_SIZE (256u * 1024u)
|
||||
#define AMPR_IO_BUFFER_SIZE (64u * 1024u)
|
||||
#define AMPR_MAX_DOWNLOAD_SIZE (16u * 1024u * 1024u)
|
||||
#define AMPR_HTTP_TIMEOUT_US (30u * 1000u * 1000u)
|
||||
#define AMPR_HTTP_VERSION_1_1 2
|
||||
|
||||
int sceNetInit(void);
|
||||
int sceNetPoolCreate(const char *name, int size, int flags);
|
||||
int sceNetPoolDestroy(int pool_id);
|
||||
int sceSslInit(size_t pool_size);
|
||||
int sceSslTerm(int ssl_id);
|
||||
typedef struct {
|
||||
char *ptr;
|
||||
size_t size;
|
||||
} SceSslData;
|
||||
int sceSslLoadCert(int ssl_id, int ca_count, const SceSslData **ca_list,
|
||||
const SceSslData *cert, const SceSslData *private_key);
|
||||
int sceSslUnloadCert(int ssl_id);
|
||||
int sceHttp2Init(int net_pool_id, int ssl_id, size_t pool_size,
|
||||
int max_concurrent_requests);
|
||||
int sceHttp2Term(int http_id);
|
||||
int sceHttp2CreateTemplate(int http_id, const char *user_agent,
|
||||
int http_version, int auto_proxy_config);
|
||||
int sceHttp2DeleteTemplate(int template_id);
|
||||
int sceHttp2CreateRequestWithURL(int template_id, const char *method,
|
||||
const char *url, uint64_t content_length);
|
||||
int sceHttp2DeleteRequest(int request_id);
|
||||
int sceHttp2SendRequest(int request_id, const void *data, size_t size);
|
||||
int sceHttp2GetStatusCode(int request_id, int *status_code);
|
||||
int sceHttp2GetAllResponseHeaders(int request_id, char **headers,
|
||||
size_t *headers_size);
|
||||
int sceHttp2ReadData(int request_id, void *buffer, size_t size);
|
||||
int sceHttp2AbortRequest(int request_id);
|
||||
int sceHttp2SetAutoRedirect(int id, int enabled);
|
||||
int sceHttp2SetTimeOut(int id, uint32_t timeout_us);
|
||||
int sceHttp2SetResolveTimeOut(int id, uint32_t timeout_us);
|
||||
int sceHttp2SetConnectTimeOut(int id, uint32_t timeout_us);
|
||||
int sceHttp2SetSendTimeOut(int id, uint32_t timeout_us);
|
||||
int sceHttp2SetRecvTimeOut(int id, uint32_t timeout_us);
|
||||
|
||||
#include "sm_ampr_ca.inc"
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
uint64_t generation;
|
||||
char url[MAX_PATH];
|
||||
char emulators_path[MAX_PATH];
|
||||
} ampr_update_config_t;
|
||||
|
||||
typedef enum {
|
||||
AMPR_UPDATE_FAILED = 0,
|
||||
AMPR_UPDATE_UNCHANGED,
|
||||
AMPR_UPDATE_REPLACED,
|
||||
AMPR_UPDATE_CANCELLED,
|
||||
} ampr_update_result_t;
|
||||
|
||||
typedef struct {
|
||||
pthread_t thread;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
bool started;
|
||||
bool stop_requested;
|
||||
bool wake_requested;
|
||||
bool net_initialized;
|
||||
uint64_t config_generation;
|
||||
int active_request_id;
|
||||
} ampr_updater_state_t;
|
||||
|
||||
static ampr_updater_state_t g_ampr_updater = {
|
||||
.mutex = PTHREAD_MUTEX_INITIALIZER,
|
||||
.cond = PTHREAD_COND_INITIALIZER,
|
||||
.active_request_id = -1,
|
||||
};
|
||||
|
||||
static void log_sce_failure(const char *operation, int error) {
|
||||
log_debug(" [AMPR] %s failed: 0x%08x", operation, (unsigned int)error);
|
||||
}
|
||||
|
||||
static bool ampr_update_busy(void) {
|
||||
return runtime_sleep_mode_active() || sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
}
|
||||
|
||||
static void snapshot_update_config(ampr_update_config_t *snapshot) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
snapshot->enabled = cfg->auto_update_ampr_enabled;
|
||||
snapshot->generation = g_ampr_updater.config_generation;
|
||||
(void)strlcpy(snapshot->url, cfg->ampr_update_url, sizeof(snapshot->url));
|
||||
(void)strlcpy(snapshot->emulators_path, cfg->emulators_path,
|
||||
sizeof(snapshot->emulators_path));
|
||||
}
|
||||
|
||||
static bool
|
||||
update_snapshot_is_current_locked(const ampr_update_config_t *snapshot) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
return !g_ampr_updater.stop_requested &&
|
||||
snapshot->generation == g_ampr_updater.config_generation &&
|
||||
cfg->auto_update_ampr_enabled &&
|
||||
strcmp(snapshot->url, cfg->ampr_update_url) == 0 &&
|
||||
strcmp(snapshot->emulators_path, cfg->emulators_path) == 0;
|
||||
}
|
||||
|
||||
static bool update_snapshot_is_current(const ampr_update_config_t *snapshot) {
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
bool current = update_snapshot_is_current_locked(snapshot);
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return current;
|
||||
}
|
||||
|
||||
static bool wait_for_update_event_locked(uint32_t seconds,
|
||||
bool wake_early) {
|
||||
struct timespec deadline;
|
||||
if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) {
|
||||
log_debug(" [AMPR] cannot create update deadline: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
deadline.tv_sec += (time_t)seconds;
|
||||
|
||||
bool wait_failed = false;
|
||||
while (!g_ampr_updater.stop_requested) {
|
||||
if (wake_early && g_ampr_updater.wake_requested)
|
||||
break;
|
||||
g_ampr_updater.wake_requested = false;
|
||||
int rc = pthread_cond_timedwait(&g_ampr_updater.cond,
|
||||
&g_ampr_updater.mutex, &deadline);
|
||||
if (rc == ETIMEDOUT)
|
||||
break;
|
||||
if (rc != 0) {
|
||||
log_debug(" [AMPR] update wait failed: %s", strerror(rc));
|
||||
wait_failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool keep_running = !g_ampr_updater.stop_requested;
|
||||
g_ampr_updater.wake_requested = false;
|
||||
return keep_running && !wait_failed;
|
||||
}
|
||||
|
||||
static bool publish_active_request(int request_id,
|
||||
const ampr_update_config_t *snapshot) {
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
bool current = update_snapshot_is_current_locked(snapshot);
|
||||
if (current)
|
||||
g_ampr_updater.active_request_id = request_id;
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return current;
|
||||
}
|
||||
|
||||
static void clear_active_request(int request_id) {
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
if (g_ampr_updater.active_request_id == request_id)
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
}
|
||||
|
||||
static bool ensure_net_initialized(void) {
|
||||
if (g_ampr_updater.net_initialized)
|
||||
return true;
|
||||
|
||||
int rc = sceNetInit();
|
||||
if (rc != 0) {
|
||||
log_sce_failure("sceNetInit", rc);
|
||||
return false;
|
||||
}
|
||||
g_ampr_updater.net_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ensure_emulators_directory(const char *path) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode))
|
||||
return true;
|
||||
errno = ENOTDIR;
|
||||
return false;
|
||||
}
|
||||
if (errno != ENOENT)
|
||||
return false;
|
||||
if (mkdir(path, 0777) == 0)
|
||||
return true;
|
||||
if (errno != EEXIST || stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool load_ampr_ca_certificates(int ssl_id) {
|
||||
SceSslData certificates[AMPR_PRIVATE_CA_COUNT] = {
|
||||
{
|
||||
.ptr = g_ampr_ca_sectigo_e46_usertrust,
|
||||
.size = sizeof(g_ampr_ca_sectigo_e46_usertrust) - 1u,
|
||||
},
|
||||
{
|
||||
.ptr = g_ampr_ca_isrg_root_yr_by_x1,
|
||||
.size = sizeof(g_ampr_ca_isrg_root_yr_by_x1) - 1u,
|
||||
},
|
||||
{
|
||||
.ptr = g_ampr_ca_usertrust_ecc,
|
||||
.size = sizeof(g_ampr_ca_usertrust_ecc) - 1u,
|
||||
},
|
||||
{
|
||||
.ptr = g_ampr_ca_isrg_root_x1,
|
||||
.size = sizeof(g_ampr_ca_isrg_root_x1) - 1u,
|
||||
},
|
||||
};
|
||||
const SceSslData *ca_list[AMPR_PRIVATE_CA_COUNT];
|
||||
for (unsigned i = 0; i < AMPR_PRIVATE_CA_COUNT; ++i)
|
||||
ca_list[i] = &certificates[i];
|
||||
int rc = sceSslLoadCert(ssl_id, (int)AMPR_PRIVATE_CA_COUNT, ca_list, NULL,
|
||||
NULL);
|
||||
if (rc != 0) {
|
||||
log_sce_failure("sceSslLoadCert", rc);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_all(int fd, const void *data, size_t size) {
|
||||
const unsigned char *cursor = data;
|
||||
while (size > 0) {
|
||||
ssize_t written = write(fd, cursor, size);
|
||||
if (written < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (written <= 0)
|
||||
return false;
|
||||
cursor += (size_t)written;
|
||||
size -= (size_t)written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool downloaded_file_is_sprx(const char *path) {
|
||||
static const unsigned char self_magic[4] = {0x4f, 0x15, 0x3d, 0x1d};
|
||||
static const unsigned char elf_magic[4] = {0x7f, 'E', 'L', 'F'};
|
||||
unsigned char magic[sizeof(self_magic)];
|
||||
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
ssize_t length;
|
||||
do {
|
||||
length = read(fd, magic, sizeof(magic));
|
||||
} while (length < 0 && errno == EINTR);
|
||||
(void)close(fd);
|
||||
return length == (ssize_t)sizeof(magic) &&
|
||||
(memcmp(magic, self_magic, sizeof(magic)) == 0 ||
|
||||
memcmp(magic, elf_magic, sizeof(magic)) == 0);
|
||||
}
|
||||
|
||||
static bool find_http_header(const char *headers, size_t headers_size,
|
||||
const char *name, char *value,
|
||||
size_t value_size) {
|
||||
if (!headers || !name || !value || value_size == 0)
|
||||
return false;
|
||||
|
||||
size_t name_size = strlen(name);
|
||||
size_t offset = 0;
|
||||
while (offset < headers_size) {
|
||||
size_t line_end = offset;
|
||||
while (line_end < headers_size && headers[line_end] != '\0' &&
|
||||
headers[line_end] != '\r' && headers[line_end] != '\n') {
|
||||
line_end++;
|
||||
}
|
||||
|
||||
size_t colon = offset;
|
||||
while (colon < line_end && headers[colon] != ':')
|
||||
colon++;
|
||||
if (colon - offset == name_size && colon < line_end &&
|
||||
strncasecmp(headers + offset, name, name_size) == 0) {
|
||||
size_t begin = colon + 1u;
|
||||
while (begin < line_end &&
|
||||
(headers[begin] == ' ' || headers[begin] == '\t')) {
|
||||
begin++;
|
||||
}
|
||||
size_t length = line_end - begin;
|
||||
while (length > 0 &&
|
||||
(headers[begin + length - 1u] == ' ' ||
|
||||
headers[begin + length - 1u] == '\t')) {
|
||||
length--;
|
||||
}
|
||||
if (length == 0 || length >= value_size)
|
||||
return false;
|
||||
memcpy(value, headers + begin, length);
|
||||
value[length] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line_end < headers_size && headers[line_end] == '\0')
|
||||
break;
|
||||
offset = line_end;
|
||||
while (offset < headers_size &&
|
||||
(headers[offset] == '\r' || headers[offset] == '\n')) {
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_leap_year(unsigned year) {
|
||||
return year % 4u == 0 && (year % 100u != 0 || year % 400u == 0);
|
||||
}
|
||||
|
||||
static int month_number(const char *name) {
|
||||
static const char *const months[] = {
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
};
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
if (strcasecmp(name, months[i]) == 0)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int64_t days_from_civil(int year, unsigned month, unsigned day) {
|
||||
year -= month <= 2u;
|
||||
int era = (year >= 0 ? year : year - 399) / 400;
|
||||
unsigned year_of_era = (unsigned)(year - era * 400);
|
||||
unsigned adjusted_month =
|
||||
(unsigned)((int)month + (month > 2u ? -3 : 9));
|
||||
unsigned day_of_year =
|
||||
(153u * adjusted_month + 2u) / 5u + day - 1u;
|
||||
unsigned day_of_era = year_of_era * 365u + year_of_era / 4u -
|
||||
year_of_era / 100u + day_of_year;
|
||||
return (int64_t)era * 146097ll + (int64_t)day_of_era - 719468ll;
|
||||
}
|
||||
|
||||
static bool parse_http_modified_time(const char *value, time_t *time_out) {
|
||||
if (!value || !time_out)
|
||||
return false;
|
||||
|
||||
char weekday[5] = {0};
|
||||
char month_name[4] = {0};
|
||||
char zone[4] = {0};
|
||||
char extra;
|
||||
unsigned day = 0;
|
||||
unsigned year = 0;
|
||||
unsigned hour = 0;
|
||||
unsigned minute = 0;
|
||||
unsigned second = 0;
|
||||
int fields = sscanf(value, "%4s %u %3s %u %u:%u:%u %3s %c", weekday,
|
||||
&day, month_name, &year, &hour, &minute, &second, zone,
|
||||
&extra);
|
||||
int month = month_number(month_name);
|
||||
static const unsigned days_per_month[] = {31u, 28u, 31u, 30u, 31u, 30u,
|
||||
31u, 31u, 30u, 31u, 30u, 31u};
|
||||
if (fields != 8 || strlen(weekday) != 4u || weekday[3] != ',' ||
|
||||
strcmp(zone, "GMT") != 0 || year < 1970u || year > 9999u ||
|
||||
month == 0 || hour > 23u || minute > 59u || second > 59u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned max_day = days_per_month[month - 1];
|
||||
if (month == 2 && is_leap_year(year))
|
||||
max_day++;
|
||||
if (day == 0 || day > max_day)
|
||||
return false;
|
||||
|
||||
int64_t timestamp =
|
||||
days_from_civil((int)year, (unsigned)month, day) * 86400ll +
|
||||
(int64_t)hour * 3600ll + (int64_t)minute * 60ll + (int64_t)second;
|
||||
time_t converted = (time_t)timestamp;
|
||||
if ((int64_t)converted != timestamp)
|
||||
return false;
|
||||
*time_out = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool try_read_remote_modified_time(int request_id, time_t *time_out) {
|
||||
char *headers = NULL;
|
||||
size_t headers_size = 0;
|
||||
int rc =
|
||||
sceHttp2GetAllResponseHeaders(request_id, &headers, &headers_size);
|
||||
if (rc != 0) {
|
||||
log_sce_failure("sceHttp2GetAllResponseHeaders", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
char value[64];
|
||||
return find_http_header(headers, headers_size, "Last-Modified", value,
|
||||
sizeof(value)) &&
|
||||
parse_http_modified_time(value, time_out);
|
||||
}
|
||||
|
||||
static bool inspect_local_emulator(const char *path, bool *missing_out,
|
||||
time_t *modified_time_out) {
|
||||
*missing_out = false;
|
||||
*modified_time_out = 0;
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) {
|
||||
if (errno == ENOENT) {
|
||||
*missing_out = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
*modified_time_out = st.st_mtime;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool set_file_modified_time(const char *path, time_t modified_time) {
|
||||
struct timeval times[2] = {
|
||||
{.tv_sec = modified_time, .tv_usec = 0},
|
||||
{.tv_sec = modified_time, .tv_usec = 0},
|
||||
};
|
||||
return utimes(path, times) == 0;
|
||||
}
|
||||
|
||||
static bool regular_files_equal(const char *first, const char *second,
|
||||
bool *equal_out) {
|
||||
*equal_out = false;
|
||||
|
||||
struct stat first_st;
|
||||
if (lstat(first, &first_st) != 0) {
|
||||
if (errno == ENOENT)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if (!S_ISREG(first_st.st_mode)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat second_st;
|
||||
if (lstat(second, &second_st) != 0)
|
||||
return false;
|
||||
if (!S_ISREG(second_st.st_mode)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
if (first_st.st_size != second_st.st_size)
|
||||
return true;
|
||||
|
||||
int first_fd = open(first, O_RDONLY);
|
||||
if (first_fd < 0)
|
||||
return false;
|
||||
int second_fd = open(second, O_RDONLY);
|
||||
if (second_fd < 0) {
|
||||
int saved_errno = errno;
|
||||
(void)close(first_fd);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned char *buffers = malloc(AMPR_IO_BUFFER_SIZE * 2u);
|
||||
if (!buffers) {
|
||||
(void)close(second_fd);
|
||||
(void)close(first_fd);
|
||||
errno = ENOMEM;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool io_ok = true;
|
||||
bool equal = true;
|
||||
while (io_ok && equal) {
|
||||
ssize_t first_size;
|
||||
do {
|
||||
first_size = read(first_fd, buffers, AMPR_IO_BUFFER_SIZE);
|
||||
} while (first_size < 0 && errno == EINTR);
|
||||
if (first_size < 0) {
|
||||
io_ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t received = 0;
|
||||
while (received < (size_t)first_size) {
|
||||
ssize_t second_size =
|
||||
read(second_fd, buffers + AMPR_IO_BUFFER_SIZE + received,
|
||||
(size_t)first_size - received);
|
||||
if (second_size < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (second_size <= 0) {
|
||||
if (second_size == 0)
|
||||
errno = EIO;
|
||||
io_ok = false;
|
||||
break;
|
||||
}
|
||||
received += (size_t)second_size;
|
||||
}
|
||||
if (!io_ok ||
|
||||
memcmp(buffers, buffers + AMPR_IO_BUFFER_SIZE,
|
||||
(size_t)first_size) != 0) {
|
||||
equal = false;
|
||||
break;
|
||||
}
|
||||
if (first_size == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
int saved_errno = io_ok ? 0 : errno;
|
||||
free(buffers);
|
||||
(void)close(second_fd);
|
||||
(void)close(first_fd);
|
||||
if (!io_ok)
|
||||
errno = saved_errno;
|
||||
*equal_out = equal;
|
||||
return io_ok;
|
||||
}
|
||||
|
||||
static bool configure_http_template(int template_id) {
|
||||
int rc = sceHttp2SetAutoRedirect(template_id, 1);
|
||||
if (rc != 0) {
|
||||
log_sce_failure("sceHttp2SetAutoRedirect", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
int timeout_error = 0;
|
||||
rc = sceHttp2SetTimeOut(template_id, AMPR_HTTP_TIMEOUT_US);
|
||||
if (rc != 0)
|
||||
timeout_error = rc;
|
||||
rc = sceHttp2SetResolveTimeOut(template_id, AMPR_HTTP_TIMEOUT_US);
|
||||
if (timeout_error == 0 && rc != 0)
|
||||
timeout_error = rc;
|
||||
rc = sceHttp2SetConnectTimeOut(template_id, AMPR_HTTP_TIMEOUT_US);
|
||||
if (timeout_error == 0 && rc != 0)
|
||||
timeout_error = rc;
|
||||
rc = sceHttp2SetSendTimeOut(template_id, AMPR_HTTP_TIMEOUT_US);
|
||||
if (timeout_error == 0 && rc != 0)
|
||||
timeout_error = rc;
|
||||
rc = sceHttp2SetRecvTimeOut(template_id, AMPR_HTTP_TIMEOUT_US);
|
||||
if (timeout_error == 0 && rc != 0)
|
||||
timeout_error = rc;
|
||||
if (timeout_error != 0) {
|
||||
log_sce_failure("HTTP timeout configuration", timeout_error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool create_and_send_request(
|
||||
int template_id, const char *method, const ampr_update_config_t *snapshot,
|
||||
int *request_id_out, int *status_code_out,
|
||||
ampr_update_result_t *result_out) {
|
||||
int request_id = sceHttp2CreateRequestWithURL(template_id, method,
|
||||
snapshot->url, 0);
|
||||
if (request_id < 0) {
|
||||
log_sce_failure("sceHttp2CreateRequestWithURL", request_id);
|
||||
return false;
|
||||
}
|
||||
*request_id_out = request_id;
|
||||
|
||||
if (!publish_active_request(request_id, snapshot)) {
|
||||
*result_out = AMPR_UPDATE_CANCELLED;
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = sceHttp2SendRequest(request_id, NULL, 0);
|
||||
if (rc != 0) {
|
||||
if (update_snapshot_is_current(snapshot))
|
||||
log_sce_failure("sceHttp2SendRequest", rc);
|
||||
else
|
||||
*result_out = AMPR_UPDATE_CANCELLED;
|
||||
return false;
|
||||
}
|
||||
|
||||
rc = sceHttp2GetStatusCode(request_id, status_code_out);
|
||||
if (rc != 0) {
|
||||
if (update_snapshot_is_current(snapshot))
|
||||
log_sce_failure("sceHttp2GetStatusCode", rc);
|
||||
else
|
||||
*result_out = AMPR_UPDATE_CANCELLED;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static ampr_update_result_t
|
||||
check_and_update_ampr(const ampr_update_config_t *snapshot) {
|
||||
int net_pool_id = -1;
|
||||
int ssl_id = -1;
|
||||
int http_id = -1;
|
||||
int template_id = -1;
|
||||
int request_id = -1;
|
||||
int output_fd = -1;
|
||||
unsigned char *buffer = NULL;
|
||||
bool temp_exists = false;
|
||||
bool ca_certificates_loaded = false;
|
||||
bool destination_missing = false;
|
||||
time_t local_modified_time = 0;
|
||||
time_t remote_modified_time = 0;
|
||||
ampr_update_result_t result = AMPR_UPDATE_FAILED;
|
||||
char destination[MAX_PATH];
|
||||
char temp_path[MAX_PATH];
|
||||
|
||||
int written = snprintf(destination, sizeof(destination), "%s/%s",
|
||||
snapshot->emulators_path, AMPR_FILE_NAME);
|
||||
int temp_written = snprintf(temp_path, sizeof(temp_path), "%s/%s",
|
||||
snapshot->emulators_path, AMPR_TEMP_FILE_NAME);
|
||||
if (written <= 0 || (size_t)written >= sizeof(destination) ||
|
||||
temp_written <= 0 || (size_t)temp_written >= sizeof(temp_path)) {
|
||||
log_debug(" [AMPR] emulator destination path is too long: %s",
|
||||
snapshot->emulators_path);
|
||||
return AMPR_UPDATE_FAILED;
|
||||
}
|
||||
|
||||
if (!inspect_local_emulator(destination, &destination_missing,
|
||||
&local_modified_time)) {
|
||||
log_debug(" [AMPR] cannot inspect %s: %s", destination,
|
||||
strerror(errno));
|
||||
return AMPR_UPDATE_FAILED;
|
||||
}
|
||||
|
||||
if (!ensure_net_initialized())
|
||||
return AMPR_UPDATE_FAILED;
|
||||
|
||||
net_pool_id = sceNetPoolCreate("sm_ampr", AMPR_NET_POOL_SIZE, 0);
|
||||
if (net_pool_id < 0) {
|
||||
log_sce_failure("sceNetPoolCreate", net_pool_id);
|
||||
goto cleanup;
|
||||
}
|
||||
ssl_id = sceSslInit(AMPR_SSL_POOL_SIZE);
|
||||
if (ssl_id < 0) {
|
||||
log_sce_failure("sceSslInit", ssl_id);
|
||||
goto cleanup;
|
||||
}
|
||||
if (strncasecmp(snapshot->url, "https://", 8u) == 0) {
|
||||
if (!load_ampr_ca_certificates(ssl_id))
|
||||
goto cleanup;
|
||||
ca_certificates_loaded = true;
|
||||
}
|
||||
http_id = sceHttp2Init(net_pool_id, ssl_id, AMPR_HTTP_POOL_SIZE, 1);
|
||||
if (http_id < 0) {
|
||||
log_sce_failure("sceHttp2Init", http_id);
|
||||
goto cleanup;
|
||||
}
|
||||
template_id = sceHttp2CreateTemplate(http_id, "ShadowMountPlus/1.0",
|
||||
AMPR_HTTP_VERSION_1_1, 1);
|
||||
if (template_id < 0) {
|
||||
log_sce_failure("sceHttp2CreateTemplate", template_id);
|
||||
goto cleanup;
|
||||
}
|
||||
if (!configure_http_template(template_id))
|
||||
goto cleanup;
|
||||
|
||||
int status_code = 0;
|
||||
if (!destination_missing) {
|
||||
if (!create_and_send_request(template_id, "HEAD", snapshot, &request_id,
|
||||
&status_code, &result)) {
|
||||
goto cleanup;
|
||||
}
|
||||
if (status_code != 200) {
|
||||
log_debug(" [AMPR] metadata check returned HTTP %d: %s", status_code,
|
||||
snapshot->url);
|
||||
goto cleanup;
|
||||
}
|
||||
if (!try_read_remote_modified_time(request_id, &remote_modified_time)) {
|
||||
log_debug(" [AMPR] response has no valid Last-Modified header");
|
||||
goto cleanup;
|
||||
}
|
||||
if (remote_modified_time <= local_modified_time) {
|
||||
result = AMPR_UPDATE_UNCHANGED;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
clear_active_request(request_id);
|
||||
(void)sceHttp2DeleteRequest(request_id);
|
||||
request_id = -1;
|
||||
} else {
|
||||
log_debug(" [AMPR] emulator is missing; downloading: %s", destination);
|
||||
}
|
||||
|
||||
if (!update_snapshot_is_current(snapshot)) {
|
||||
result = AMPR_UPDATE_CANCELLED;
|
||||
goto cleanup;
|
||||
}
|
||||
if (!ensure_emulators_directory(snapshot->emulators_path)) {
|
||||
log_debug(" [AMPR] cannot use emulator folder %s: %s",
|
||||
snapshot->emulators_path, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
status_code = 0;
|
||||
if (!create_and_send_request(template_id, "GET", snapshot, &request_id,
|
||||
&status_code, &result)) {
|
||||
goto cleanup;
|
||||
}
|
||||
if (status_code != 200) {
|
||||
log_debug(" [AMPR] update download returned HTTP %d: %s", status_code,
|
||||
snapshot->url);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (destination_missing &&
|
||||
!try_read_remote_modified_time(request_id, &remote_modified_time)) {
|
||||
remote_modified_time = time(NULL);
|
||||
if (remote_modified_time == (time_t)-1) {
|
||||
log_debug(" [AMPR] cannot determine download modification time");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
(void)unlink(temp_path);
|
||||
output_fd = open(temp_path, O_WRONLY | O_CREAT | O_EXCL, 0777);
|
||||
if (output_fd < 0) {
|
||||
log_debug(" [AMPR] cannot create temporary download %s: %s", temp_path,
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
temp_exists = true;
|
||||
|
||||
buffer = malloc(AMPR_IO_BUFFER_SIZE);
|
||||
if (!buffer) {
|
||||
log_debug(" [AMPR] download buffer allocation failed");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
size_t total_size = 0;
|
||||
for (;;) {
|
||||
if (!update_snapshot_is_current(snapshot)) {
|
||||
result = AMPR_UPDATE_CANCELLED;
|
||||
goto cleanup;
|
||||
}
|
||||
int received = sceHttp2ReadData(request_id, buffer, AMPR_IO_BUFFER_SIZE);
|
||||
if (received < 0) {
|
||||
if (update_snapshot_is_current(snapshot))
|
||||
log_sce_failure("sceHttp2ReadData", received);
|
||||
else
|
||||
result = AMPR_UPDATE_CANCELLED;
|
||||
goto cleanup;
|
||||
}
|
||||
if (received == 0)
|
||||
break;
|
||||
if ((size_t)received > AMPR_MAX_DOWNLOAD_SIZE - total_size) {
|
||||
log_debug(" [AMPR] update exceeds the %u-byte size limit",
|
||||
(unsigned)AMPR_MAX_DOWNLOAD_SIZE);
|
||||
goto cleanup;
|
||||
}
|
||||
if (!write_all(output_fd, buffer, (size_t)received)) {
|
||||
log_debug(" [AMPR] write failed for %s: %s", temp_path,
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
total_size += (size_t)received;
|
||||
}
|
||||
|
||||
if (total_size < 4u || fsync(output_fd) != 0) {
|
||||
log_debug(" [AMPR] incomplete download for %s: %s", temp_path,
|
||||
total_size < 4u ? "file is empty" : strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
if (close(output_fd) != 0) {
|
||||
output_fd = -1;
|
||||
log_debug(" [AMPR] close failed for %s: %s", temp_path, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
output_fd = -1;
|
||||
free(buffer);
|
||||
buffer = NULL;
|
||||
|
||||
if (!set_file_modified_time(temp_path, remote_modified_time)) {
|
||||
log_debug(" [AMPR] cannot preserve server modification time for %s: %s",
|
||||
temp_path, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!downloaded_file_is_sprx(temp_path)) {
|
||||
log_debug(" [AMPR] downloaded file has an invalid SELF/ELF header");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
bool unchanged = false;
|
||||
if (!regular_files_equal(destination, temp_path, &unchanged)) {
|
||||
log_debug(" [AMPR] cannot compare %s: %s", destination, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
if (!update_snapshot_is_current(snapshot) || ampr_update_busy()) {
|
||||
result = AMPR_UPDATE_CANCELLED;
|
||||
goto cleanup;
|
||||
}
|
||||
if (unchanged) {
|
||||
if (!set_file_modified_time(destination, remote_modified_time)) {
|
||||
log_debug(" [AMPR] cannot update modification time for %s: %s",
|
||||
destination, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
result = AMPR_UPDATE_UNCHANGED;
|
||||
goto cleanup;
|
||||
}
|
||||
if (rename(temp_path, destination) != 0) {
|
||||
log_debug(" [AMPR] cannot replace %s: %s", destination, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
temp_exists = false;
|
||||
result = AMPR_UPDATE_REPLACED;
|
||||
|
||||
cleanup:
|
||||
free(buffer);
|
||||
if (output_fd >= 0)
|
||||
(void)close(output_fd);
|
||||
if (request_id >= 0) {
|
||||
clear_active_request(request_id);
|
||||
(void)sceHttp2DeleteRequest(request_id);
|
||||
}
|
||||
if (template_id >= 0)
|
||||
(void)sceHttp2DeleteTemplate(template_id);
|
||||
if (http_id >= 0)
|
||||
(void)sceHttp2Term(http_id);
|
||||
if (ca_certificates_loaded)
|
||||
(void)sceSslUnloadCert(ssl_id);
|
||||
if (ssl_id >= 0)
|
||||
(void)sceSslTerm(ssl_id);
|
||||
if (net_pool_id >= 0)
|
||||
(void)sceNetPoolDestroy(net_pool_id);
|
||||
if (temp_exists)
|
||||
(void)unlink(temp_path);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void *ampr_updater_thread_main(void *unused) {
|
||||
(void)unused;
|
||||
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
g_ampr_updater.wake_requested = false;
|
||||
if (!wait_for_update_event_locked(AMPR_INITIAL_CHECK_DELAY_SECONDS, false)) {
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
while (!g_ampr_updater.stop_requested) {
|
||||
ampr_update_config_t snapshot;
|
||||
snapshot_update_config(&snapshot);
|
||||
g_ampr_updater.wake_requested = false;
|
||||
|
||||
if (!snapshot.enabled) {
|
||||
while (!g_ampr_updater.stop_requested &&
|
||||
!g_ampr_updater.wake_requested) {
|
||||
int rc =
|
||||
pthread_cond_wait(&g_ampr_updater.cond, &g_ampr_updater.mutex);
|
||||
if (rc != 0) {
|
||||
log_debug(" [AMPR] disabled wait failed: %s", strerror(rc));
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ampr_update_busy()) {
|
||||
if (!wait_for_update_event_locked(AMPR_BUSY_RETRY_SECONDS, true))
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
log_debug(" [AMPR] checking emulator modification time: %s",
|
||||
snapshot.url);
|
||||
ampr_update_result_t result = check_and_update_ampr(&snapshot);
|
||||
if (result == AMPR_UPDATE_REPLACED) {
|
||||
log_debug(" [AMPR] emulator updated: %s/%s", snapshot.emulators_path,
|
||||
AMPR_FILE_NAME);
|
||||
notify_system_l10n(SM_L10N_AMPR_EMULATOR_UPDATED);
|
||||
} else if (result == AMPR_UPDATE_UNCHANGED) {
|
||||
log_debug(" [AMPR] emulator is already current");
|
||||
}
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
|
||||
if (!g_ampr_updater.stop_requested &&
|
||||
!g_ampr_updater.wake_requested &&
|
||||
!wait_for_update_event_locked(AMPR_UPDATE_INTERVAL_SECONDS, true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_ampr_updater_start(void) {
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
if (g_ampr_updater.started) {
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
g_ampr_updater.stop_requested = false;
|
||||
g_ampr_updater.wake_requested = true;
|
||||
g_ampr_updater.config_generation++;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
int rc = pthread_create(&g_ampr_updater.thread, NULL,
|
||||
ampr_updater_thread_main, NULL);
|
||||
if (rc != 0) {
|
||||
g_ampr_updater.wake_requested = false;
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
errno = rc;
|
||||
return false;
|
||||
}
|
||||
g_ampr_updater.started = true;
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_ampr_updater_stop(void) {
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
if (!g_ampr_updater.started) {
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return;
|
||||
}
|
||||
g_ampr_updater.stop_requested = true;
|
||||
g_ampr_updater.wake_requested = true;
|
||||
g_ampr_updater.config_generation++;
|
||||
int active_request_id = g_ampr_updater.active_request_id;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
pthread_cond_broadcast(&g_ampr_updater.cond);
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
|
||||
if (active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(active_request_id);
|
||||
(void)pthread_join(g_ampr_updater.thread, NULL);
|
||||
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
g_ampr_updater.started = false;
|
||||
g_ampr_updater.stop_requested = false;
|
||||
g_ampr_updater.wake_requested = false;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
}
|
||||
|
||||
void sm_ampr_updater_on_config_reload(const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg) {
|
||||
if (!old_cfg || !new_cfg ||
|
||||
(old_cfg->auto_update_ampr_enabled ==
|
||||
new_cfg->auto_update_ampr_enabled &&
|
||||
strcmp(old_cfg->ampr_update_url, new_cfg->ampr_update_url) == 0 &&
|
||||
strcmp(old_cfg->emulators_path, new_cfg->emulators_path) == 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
g_ampr_updater.config_generation++;
|
||||
g_ampr_updater.wake_requested = true;
|
||||
int active_request_id = g_ampr_updater.active_request_id;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
pthread_cond_broadcast(&g_ampr_updater.cond);
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
|
||||
if (active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(active_request_id);
|
||||
}
|
||||
@@ -0,0 +1,934 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <json-c/json.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sm_api_protocol.h"
|
||||
#include "sm_api_service.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_socket_io.h"
|
||||
|
||||
#ifndef SHADOWMOUNT_VERSION
|
||||
#define SHADOWMOUNT_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
#define SM_API_ROUTE_SIZE 128u
|
||||
#define SM_API_RETRY_INITIAL_MS 250u
|
||||
#define SM_API_RETRY_MAX_MS 5000u
|
||||
#define SM_API_CORS_HEADERS \
|
||||
"Access-Control-Allow-Origin: *\r\n" \
|
||||
"Access-Control-Allow-Methods: POST, OPTIONS\r\n" \
|
||||
"Access-Control-Allow-Headers: Content-Type\r\n" \
|
||||
"Access-Control-Allow-Private-Network: true\r\n"
|
||||
|
||||
typedef struct {
|
||||
pthread_t thread;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t retry_cond;
|
||||
bool started;
|
||||
bool stop_requested;
|
||||
int listen_fd;
|
||||
int client_fd;
|
||||
char bind_address[MAX_API_BIND_ADDRESS];
|
||||
uint16_t port;
|
||||
} sm_api_service_state_t;
|
||||
|
||||
typedef struct {
|
||||
char route[SM_API_ROUTE_SIZE];
|
||||
char *storage;
|
||||
char *body;
|
||||
size_t body_size;
|
||||
bool preflight;
|
||||
} sm_http_request_t;
|
||||
|
||||
static sm_api_service_state_t g_api = {
|
||||
.mutex = PTHREAD_MUTEX_INITIALIZER,
|
||||
.retry_cond = PTHREAD_COND_INITIALIZER,
|
||||
.listen_fd = -1,
|
||||
.client_fd = -1,
|
||||
};
|
||||
|
||||
static int open_http_listener(const char *bind_address, uint16_t port) {
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
int reuse = 1;
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
|
||||
struct sockaddr_in address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_len = sizeof(address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, bind_address, &address.sin_addr) != 1) {
|
||||
close(fd);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||
listen(fd, 4) != 0) {
|
||||
int saved_errno = errno;
|
||||
close(fd);
|
||||
errno = saved_errno;
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static uint32_t next_retry_delay_ms(uint32_t current) {
|
||||
if (current >= SM_API_RETRY_MAX_MS)
|
||||
return SM_API_RETRY_MAX_MS;
|
||||
if (current > SM_API_RETRY_MAX_MS / 2u)
|
||||
return SM_API_RETRY_MAX_MS;
|
||||
return current * 2u;
|
||||
}
|
||||
|
||||
static bool wait_before_listener_retry(uint32_t delay_ms) {
|
||||
struct timespec deadline;
|
||||
if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) {
|
||||
sceKernelUsleep(SM_API_RETRY_INITIAL_MS * 1000u);
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
bool keep_running = !g_api.stop_requested;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return keep_running;
|
||||
}
|
||||
|
||||
deadline.tv_sec += (time_t)(delay_ms / 1000u);
|
||||
deadline.tv_nsec += (long)(delay_ms % 1000u) * 1000000l;
|
||||
if (deadline.tv_nsec >= 1000000000l) {
|
||||
deadline.tv_sec++;
|
||||
deadline.tv_nsec -= 1000000000l;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
int wait_status = 0;
|
||||
while (!g_api.stop_requested) {
|
||||
wait_status =
|
||||
pthread_cond_timedwait(&g_api.retry_cond, &g_api.mutex, &deadline);
|
||||
if (wait_status != 0)
|
||||
break;
|
||||
}
|
||||
bool keep_running = !g_api.stop_requested;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
if (keep_running && wait_status != 0 && wait_status != ETIMEDOUT) {
|
||||
sceKernelUsleep(SM_API_RETRY_INITIAL_MS * 1000u);
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
keep_running = !g_api.stop_requested;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
}
|
||||
return keep_running;
|
||||
}
|
||||
|
||||
static bool accept_error_needs_wait(int error) {
|
||||
return error == EAGAIN || error == EWOULDBLOCK || error == EMFILE ||
|
||||
error == ENFILE || error == ENOBUFS || error == ENOMEM;
|
||||
}
|
||||
|
||||
static const char *http_reason_phrase(int status) {
|
||||
switch (status) {
|
||||
case 200:
|
||||
return "OK";
|
||||
case 400:
|
||||
return "Bad Request";
|
||||
case 404:
|
||||
return "Not Found";
|
||||
case 405:
|
||||
return "Method Not Allowed";
|
||||
case 409:
|
||||
return "Conflict";
|
||||
case 411:
|
||||
return "Length Required";
|
||||
case 413:
|
||||
return "Payload Too Large";
|
||||
case 415:
|
||||
return "Unsupported Media Type";
|
||||
case 431:
|
||||
return "Request Header Fields Too Large";
|
||||
case 500:
|
||||
return "Internal Server Error";
|
||||
case 501:
|
||||
return "Not Implemented";
|
||||
default:
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
|
||||
static bool send_json_text(int fd, int http_status, const char *body,
|
||||
size_t body_size) {
|
||||
char header[512];
|
||||
int count = snprintf(header, sizeof(header),
|
||||
"HTTP/1.1 %d %s\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
SM_API_CORS_HEADERS
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Cache-Control: no-store\r\n\r\n",
|
||||
http_status, http_reason_phrase(http_status), body_size);
|
||||
if (count < 0 || (size_t)count >= sizeof(header))
|
||||
return false;
|
||||
return sm_socket_write_full(fd, header, (size_t)count) &&
|
||||
sm_socket_write_full(fd, body, body_size);
|
||||
}
|
||||
|
||||
static void send_preflight_response(int fd) {
|
||||
static const char response[] =
|
||||
"HTTP/1.1 204 No Content\r\n"
|
||||
SM_API_CORS_HEADERS
|
||||
"Access-Control-Max-Age: 86400\r\n"
|
||||
"Content-Length: 0\r\n"
|
||||
"Connection: close\r\n\r\n";
|
||||
(void)sm_socket_write_full(fd, response, sizeof(response) - 1u);
|
||||
}
|
||||
|
||||
static bool send_json_object(int fd, int http_status,
|
||||
struct json_object *response) {
|
||||
const char *body =
|
||||
json_object_to_json_string_ext(response, JSON_C_TO_STRING_PLAIN);
|
||||
if (!body)
|
||||
return false;
|
||||
return send_json_text(fd, http_status, body, strlen(body));
|
||||
}
|
||||
|
||||
static bool add_json_value(struct json_object *object, const char *key,
|
||||
struct json_object *value) {
|
||||
if (!value)
|
||||
return false;
|
||||
if (json_object_object_add_ex(object, key, value,
|
||||
JSON_C_OBJECT_ADD_CONSTANT_KEY) == 0) {
|
||||
return true;
|
||||
}
|
||||
json_object_put(value);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool add_json_int(struct json_object *object, const char *key,
|
||||
int64_t value) {
|
||||
return add_json_value(object, key, json_object_new_int64(value));
|
||||
}
|
||||
|
||||
static bool add_json_bool(struct json_object *object, const char *key,
|
||||
bool value) {
|
||||
return add_json_value(object, key, json_object_new_boolean(value));
|
||||
}
|
||||
|
||||
static bool add_json_string(struct json_object *object, const char *key,
|
||||
const char *value) {
|
||||
return add_json_value(object, key, json_object_new_string(value));
|
||||
}
|
||||
|
||||
static bool append_json_string(struct json_object *array, const char *value) {
|
||||
struct json_object *item = json_object_new_string(value);
|
||||
if (!item)
|
||||
return false;
|
||||
if (json_object_array_add(array, item) == 0)
|
||||
return true;
|
||||
json_object_put(item);
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct json_object *new_status_response(int status) {
|
||||
struct json_object *response = json_object_new_object();
|
||||
if (response && !add_json_int(response, "status", status)) {
|
||||
json_object_put(response);
|
||||
return NULL;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static void send_out_of_memory_response(int fd) {
|
||||
static const char body[] =
|
||||
"{\"status\":12,\"error\":\"out of memory\"}";
|
||||
(void)send_json_text(fd, 500, body, sizeof(body) - 1u);
|
||||
}
|
||||
|
||||
static void send_error_response(int fd, int http_status, int status,
|
||||
const char *message) {
|
||||
struct json_object *response = new_status_response(status);
|
||||
if (!response) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_string(response, "error", message)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, http_status, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static char *find_http_header_end(char *buffer, size_t size) {
|
||||
if (size < 4u)
|
||||
return NULL;
|
||||
for (size_t i = 0; i + 3u < size; ++i) {
|
||||
if (buffer[i] == '\r' && buffer[i + 1u] == '\n' &&
|
||||
buffer[i + 2u] == '\r' && buffer[i + 3u] == '\n') {
|
||||
return &buffer[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *trim_http_space(char *value) {
|
||||
while (*value == ' ' || *value == '\t')
|
||||
value++;
|
||||
size_t size = strlen(value);
|
||||
while (size > 0u && (value[size - 1u] == ' ' || value[size - 1u] == '\t'))
|
||||
value[--size] = '\0';
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool is_json_content_type(const char *value) {
|
||||
static const char media_type[] = "application/json";
|
||||
size_t size = sizeof(media_type) - 1u;
|
||||
if (strncasecmp(value, media_type, size) != 0)
|
||||
return false;
|
||||
value += size;
|
||||
while (*value == ' ' || *value == '\t')
|
||||
value++;
|
||||
return *value == '\0' || *value == ';';
|
||||
}
|
||||
|
||||
static bool parse_content_length(const char *value, size_t *size_out) {
|
||||
if (!value || !size_out || value[0] == '\0' || value[0] == '-')
|
||||
return false;
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
unsigned long long parsed = strtoull(value, &end, 10);
|
||||
if (errno != 0 || end == value || *end != '\0' || parsed > SIZE_MAX)
|
||||
return false;
|
||||
*size_out = (size_t)parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool reject_http_request(char *buffer, int http_status,
|
||||
const char *error, int *http_status_out,
|
||||
const char **error_out) {
|
||||
free(buffer);
|
||||
*http_status_out = http_status;
|
||||
*error_out = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool receive_http_request(int fd, sm_http_request_t *request,
|
||||
int *http_status_out,
|
||||
const char **error_out) {
|
||||
size_t capacity = SM_API_MAX_HTTP_HEADER_SIZE + SM_API_MAX_JSON_BODY_SIZE + 1u;
|
||||
char *buffer = malloc(capacity);
|
||||
if (!buffer)
|
||||
return reject_http_request(NULL, 500, "out of memory", http_status_out,
|
||||
error_out);
|
||||
|
||||
size_t received = 0;
|
||||
char *header_end = NULL;
|
||||
while (!header_end) {
|
||||
if (received >= SM_API_MAX_HTTP_HEADER_SIZE)
|
||||
return reject_http_request(buffer, 431, "HTTP headers are too large",
|
||||
http_status_out, error_out);
|
||||
ssize_t count = recv(fd, buffer + received,
|
||||
SM_API_MAX_HTTP_HEADER_SIZE - received, 0);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
return reject_http_request(buffer, 400, "incomplete HTTP request",
|
||||
http_status_out, error_out);
|
||||
received += (size_t)count;
|
||||
header_end = find_http_header_end(buffer, received);
|
||||
}
|
||||
|
||||
size_t header_size = (size_t)(header_end - buffer) + 4u;
|
||||
*header_end = '\0';
|
||||
char *line_end = strstr(buffer, "\r\n");
|
||||
if (!line_end)
|
||||
return reject_http_request(buffer, 400, "invalid HTTP request line",
|
||||
http_status_out, error_out);
|
||||
*line_end = '\0';
|
||||
|
||||
char method[8];
|
||||
char version[16];
|
||||
char extra;
|
||||
if (sscanf(buffer, "%7s %127s %15s %c", method, request->route, version,
|
||||
&extra) != 3 ||
|
||||
(strcmp(version, "HTTP/1.1") != 0 && strcmp(version, "HTTP/1.0") != 0))
|
||||
return reject_http_request(buffer, 400, "invalid HTTP request line",
|
||||
http_status_out, error_out);
|
||||
if (strcmp(method, "POST") == 0) {
|
||||
request->preflight = false;
|
||||
} else if (strcmp(method, "OPTIONS") == 0) {
|
||||
request->preflight = true;
|
||||
} else {
|
||||
return reject_http_request(buffer, 405,
|
||||
"only POST and OPTIONS requests are supported",
|
||||
http_status_out, error_out);
|
||||
}
|
||||
|
||||
bool has_content_length = false;
|
||||
bool has_json_content_type = false;
|
||||
bool has_transfer_encoding = false;
|
||||
size_t body_size = 0;
|
||||
char *line = line_end + 2;
|
||||
while (line < header_end && line[0] != '\0') {
|
||||
char *next = strstr(line, "\r\n");
|
||||
if (!next)
|
||||
next = header_end;
|
||||
*next = '\0';
|
||||
char *colon = strchr(line, ':');
|
||||
if (!colon)
|
||||
return reject_http_request(buffer, 400, "invalid HTTP header",
|
||||
http_status_out, error_out);
|
||||
*colon = '\0';
|
||||
char *name = trim_http_space(line);
|
||||
char *value = trim_http_space(colon + 1);
|
||||
if (strcasecmp(name, "Content-Length") == 0) {
|
||||
if (has_content_length || !parse_content_length(value, &body_size))
|
||||
return reject_http_request(buffer, 400,
|
||||
"invalid Content-Length header",
|
||||
http_status_out, error_out);
|
||||
has_content_length = true;
|
||||
} else if (strcasecmp(name, "Content-Type") == 0) {
|
||||
has_json_content_type = is_json_content_type(value);
|
||||
} else if (strcasecmp(name, "Transfer-Encoding") == 0) {
|
||||
has_transfer_encoding = true;
|
||||
}
|
||||
line = next + 2;
|
||||
}
|
||||
|
||||
if (has_transfer_encoding)
|
||||
return reject_http_request(buffer, 501,
|
||||
"Transfer-Encoding is not supported",
|
||||
http_status_out, error_out);
|
||||
if (request->preflight) {
|
||||
request->storage = buffer;
|
||||
return true;
|
||||
}
|
||||
if (!has_content_length)
|
||||
return reject_http_request(buffer, 411, "Content-Length is required",
|
||||
http_status_out, error_out);
|
||||
if (!has_json_content_type)
|
||||
return reject_http_request(buffer, 415,
|
||||
"Content-Type must be application/json",
|
||||
http_status_out, error_out);
|
||||
if (body_size > SM_API_MAX_JSON_BODY_SIZE)
|
||||
return reject_http_request(buffer, 413, "JSON request body is too large",
|
||||
http_status_out, error_out);
|
||||
|
||||
size_t total_size = header_size + body_size;
|
||||
while (received < total_size) {
|
||||
ssize_t count = recv(fd, buffer + received, total_size - received, 0);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
return reject_http_request(buffer, 400,
|
||||
"incomplete JSON request body",
|
||||
http_status_out, error_out);
|
||||
received += (size_t)count;
|
||||
}
|
||||
buffer[total_size] = '\0';
|
||||
request->storage = buffer;
|
||||
request->body = buffer + header_size;
|
||||
request->body_size = body_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct json_object *parse_request_json(const sm_http_request_t *request) {
|
||||
struct json_tokener *tokener = json_tokener_new_ex(16);
|
||||
if (!tokener)
|
||||
return NULL;
|
||||
json_tokener_set_flags(tokener, JSON_TOKENER_STRICT | JSON_TOKENER_VALIDATE_UTF8);
|
||||
struct json_object *root =
|
||||
json_tokener_parse_ex(tokener, request->body, (int)request->body_size);
|
||||
enum json_tokener_error error = json_tokener_get_error(tokener);
|
||||
size_t parsed_size = json_tokener_get_parse_end(tokener);
|
||||
while (parsed_size < request->body_size &&
|
||||
isspace((unsigned char)request->body[parsed_size])) {
|
||||
parsed_size++;
|
||||
}
|
||||
bool valid = error == json_tokener_success && root != NULL &&
|
||||
parsed_size == request->body_size &&
|
||||
json_object_is_type(root, json_type_object);
|
||||
json_tokener_free(tokener);
|
||||
if (!valid) {
|
||||
if (root)
|
||||
json_object_put(root);
|
||||
return NULL;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
static const char *get_title_id(struct json_object *request) {
|
||||
struct json_object *value = NULL;
|
||||
if (!json_object_object_get_ex(request, "title_id", &value) ||
|
||||
!json_object_is_type(value, json_type_string)) {
|
||||
return NULL;
|
||||
}
|
||||
const char *title_id = json_object_get_string(value);
|
||||
return is_supported_game_title_id(title_id) ? title_id : NULL;
|
||||
}
|
||||
|
||||
static void handle_version(int fd) {
|
||||
struct json_object *response = new_status_response(0);
|
||||
struct json_object *capabilities = json_object_new_array();
|
||||
if (!response || !capabilities) {
|
||||
if (response)
|
||||
json_object_put(response);
|
||||
if (capabilities)
|
||||
json_object_put(capabilities);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_int(response, "api_version", SM_API_VERSION) ||
|
||||
!add_json_string(response, "shadowmount_version", SHADOWMOUNT_VERSION) ||
|
||||
!append_json_string(capabilities, "list_images") ||
|
||||
!append_json_string(capabilities, "list_games") ||
|
||||
!append_json_string(capabilities, "mount_game") ||
|
||||
!append_json_string(capabilities, "unmount_game")) {
|
||||
json_object_put(capabilities);
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_value(response, "capabilities", capabilities)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, 200, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static struct json_object *image_to_json(const void *entry) {
|
||||
const sm_image_index_snapshot_entry_t *source = entry;
|
||||
struct json_object *item = json_object_new_object();
|
||||
if (!item)
|
||||
return NULL;
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(source->path, mount_point);
|
||||
bool source_available = path_exists(source->path);
|
||||
bool mapped = false;
|
||||
bool mounted = false;
|
||||
int unit_id = -1;
|
||||
attach_backend_t backend = ATTACH_BACKEND_NONE;
|
||||
|
||||
image_cache_entry_t cached;
|
||||
if (find_image_cache_entry(source->path, &cached, NULL)) {
|
||||
mapped = true;
|
||||
(void)strlcpy(mount_point, cached.mount_point, sizeof(mount_point));
|
||||
unit_id = cached.unit_id;
|
||||
backend = cached.backend;
|
||||
mounted = cached.unit_id >= 0 && cached.backend != ATTACH_BACKEND_NONE &&
|
||||
is_active_image_mount_point(cached.mount_point);
|
||||
}
|
||||
|
||||
if (!add_json_string(item, "path", source->path) ||
|
||||
!add_json_string(item, "mount_point", mount_point) ||
|
||||
!add_json_int(item, "size", source->size) ||
|
||||
!add_json_int(item, "mtime_sec", source->mtime_sec) ||
|
||||
!add_json_int(item, "mtime_nsec", source->mtime_nsec) ||
|
||||
!add_json_int(item, "unit_id", unit_id) ||
|
||||
!add_json_string(item, "backend", attach_backend_name(backend)) ||
|
||||
!add_json_bool(item, "complete", source->complete) ||
|
||||
!add_json_bool(item, "source_available", source_available) ||
|
||||
!add_json_bool(item, "mapped", mapped) ||
|
||||
!add_json_bool(item, "mounted", mounted)) {
|
||||
json_object_put(item);
|
||||
return NULL;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
static struct json_object *game_to_json(const void *entry) {
|
||||
const sm_game_cache_snapshot_entry_t *source = entry;
|
||||
struct json_object *item = json_object_new_object();
|
||||
if (!item)
|
||||
return NULL;
|
||||
|
||||
char managed_path[MAX_PATH];
|
||||
char image_path[MAX_PATH] = {0};
|
||||
bool installed = is_installed(source->title_id);
|
||||
bool mounted = is_data_mounted(source->title_id);
|
||||
bool managed =
|
||||
read_mount_link(source->title_id, managed_path, sizeof(managed_path));
|
||||
bool image_backed = read_mount_image_link(source->title_id, image_path,
|
||||
sizeof(image_path));
|
||||
bool source_available = path_exists(source->path) ||
|
||||
(image_path[0] != '\0' && path_exists(image_path));
|
||||
|
||||
if (!add_json_string(item, "path", source->path) ||
|
||||
!add_json_string(item, "image_path", image_path) ||
|
||||
!add_json_string(item, "title_id", source->title_id) ||
|
||||
!add_json_string(item, "title_name", source->title_name) ||
|
||||
!add_json_bool(item, "installed", installed) ||
|
||||
!add_json_bool(item, "managed", managed) ||
|
||||
!add_json_bool(item, "mounted", mounted) ||
|
||||
!add_json_bool(item, "image_backed", image_backed) ||
|
||||
!add_json_bool(item, "source_available", source_available)) {
|
||||
json_object_put(item);
|
||||
return NULL;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
typedef struct json_object *(*snapshot_to_json_fn)(const void *entry);
|
||||
|
||||
static void send_snapshot_response(int fd, void *snapshot, size_t count,
|
||||
size_t entry_size, const char *array_key,
|
||||
snapshot_to_json_fn to_json) {
|
||||
struct json_object *response = new_status_response(0);
|
||||
struct json_object *items = json_object_new_array_ext((int)count);
|
||||
if (!response || !items) {
|
||||
if (response)
|
||||
json_object_put(response);
|
||||
if (items)
|
||||
json_object_put(items);
|
||||
free(snapshot);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned char *entries = snapshot;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
struct json_object *item = to_json(entries + i * entry_size);
|
||||
if (!item || json_object_array_add(items, item) != 0) {
|
||||
if (item)
|
||||
json_object_put(item);
|
||||
json_object_put(items);
|
||||
json_object_put(response);
|
||||
free(snapshot);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
free(snapshot);
|
||||
|
||||
if (!add_json_int(response, "count", (int64_t)count)) {
|
||||
json_object_put(items);
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_value(response, array_key, items)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, 200, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static void handle_images(int fd) {
|
||||
sm_image_index_snapshot_entry_t *snapshot = NULL;
|
||||
size_t count = 0;
|
||||
if (!sm_image_index_snapshot(&snapshot, &count)) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
send_snapshot_response(fd, snapshot, count, sizeof(*snapshot), "images",
|
||||
image_to_json);
|
||||
}
|
||||
|
||||
static void handle_games(int fd) {
|
||||
sm_game_cache_snapshot_entry_t *snapshot = NULL;
|
||||
size_t count = 0;
|
||||
if (!sm_game_cache_snapshot(&snapshot, &count)) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
send_snapshot_response(fd, snapshot, count, sizeof(*snapshot), "games",
|
||||
game_to_json);
|
||||
}
|
||||
|
||||
static int operation_http_status(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 200;
|
||||
case EINVAL:
|
||||
return 400;
|
||||
case ENOENT:
|
||||
return 404;
|
||||
case EBUSY:
|
||||
return 409;
|
||||
case ENOTSUP:
|
||||
return 501;
|
||||
default:
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_mount_operation(int fd, struct json_object *request,
|
||||
bool mount) {
|
||||
const char *title_id = get_title_id(request);
|
||||
if (!title_id) {
|
||||
send_error_response(fd, 400, EINVAL,
|
||||
"title_id must be a valid PS4 or PS5 title ID");
|
||||
return;
|
||||
}
|
||||
|
||||
int status = mount ? sm_shellcore_mount_title_runtime(title_id)
|
||||
: sm_shellcore_unmount_title_runtime(title_id);
|
||||
log_debug(" [API] HTTP %s game: title=%s status=%d",
|
||||
mount ? "mount" : "unmount", title_id, status);
|
||||
if (status != 0) {
|
||||
send_error_response(fd, operation_http_status(status), status,
|
||||
strerror(status));
|
||||
return;
|
||||
}
|
||||
|
||||
struct json_object *response = new_status_response(0);
|
||||
if (!response) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_string(response, "title_id", title_id) ||
|
||||
!add_json_bool(response, "mounted", mount)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, 200, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static void handle_client(int fd) {
|
||||
sm_http_request_t request;
|
||||
memset(&request, 0, sizeof(request));
|
||||
int http_status = 400;
|
||||
const char *error = "invalid HTTP request";
|
||||
if (!receive_http_request(fd, &request, &http_status, &error)) {
|
||||
send_error_response(fd, http_status,
|
||||
http_status == 500 ? ENOMEM : EPROTO, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.preflight) {
|
||||
send_preflight_response(fd);
|
||||
free(request.storage);
|
||||
return;
|
||||
}
|
||||
|
||||
struct json_object *json = parse_request_json(&request);
|
||||
if (!json) {
|
||||
send_error_response(fd, 400, EINVAL,
|
||||
"request body must be one valid JSON object");
|
||||
free(request.storage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(request.route, SM_API_ROUTE_VERSION) == 0) {
|
||||
handle_version(fd);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_IMAGES) == 0) {
|
||||
handle_images(fd);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_GAMES) == 0) {
|
||||
handle_games(fd);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_MOUNT) == 0) {
|
||||
handle_mount_operation(fd, json, true);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_UNMOUNT) == 0) {
|
||||
handle_mount_operation(fd, json, false);
|
||||
} else {
|
||||
send_error_response(fd, 404, ENOENT, "unknown API route");
|
||||
}
|
||||
|
||||
json_object_put(json);
|
||||
free(request.storage);
|
||||
}
|
||||
|
||||
static void *service_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
char bind_address[MAX_API_BIND_ADDRESS];
|
||||
(void)strlcpy(bind_address, g_api.bind_address, sizeof(bind_address));
|
||||
uint16_t port = g_api.port;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
int listen_fd = -1;
|
||||
uint32_t retry_delay_ms = SM_API_RETRY_INITIAL_MS;
|
||||
bool outage_logged = false;
|
||||
while (true) {
|
||||
if (listen_fd < 0) {
|
||||
listen_fd = open_http_listener(bind_address, port);
|
||||
if (listen_fd < 0) {
|
||||
int open_error = errno;
|
||||
if (!outage_logged) {
|
||||
log_debug(" [API] HTTP listener unavailable at %s:%u: %s; "
|
||||
"recovery scheduled",
|
||||
bind_address, (unsigned)port, strerror(open_error));
|
||||
outage_logged = true;
|
||||
}
|
||||
if (!wait_before_listener_retry(retry_delay_ms))
|
||||
break;
|
||||
retry_delay_ms = next_retry_delay_ms(retry_delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.stop_requested) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
close(listen_fd);
|
||||
break;
|
||||
}
|
||||
g_api.listen_fd = listen_fd;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
if (outage_logged) {
|
||||
log_debug(" [API] HTTP listener recovered: http://%s:%u/api/v1",
|
||||
bind_address, (unsigned)port);
|
||||
} else {
|
||||
log_debug(" [API] HTTP/JSON ready: http://%s:%u/api/v1 (v%u)",
|
||||
bind_address, (unsigned)port, SM_API_VERSION);
|
||||
}
|
||||
outage_logged = false;
|
||||
retry_delay_ms = SM_API_RETRY_INITIAL_MS;
|
||||
}
|
||||
|
||||
int client = accept(listen_fd, NULL, NULL);
|
||||
if (client < 0) {
|
||||
int accept_error = errno;
|
||||
if (accept_error == EINTR || accept_error == ECONNABORTED ||
|
||||
accept_error == EPROTO) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.stop_requested) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (accept_error_needs_wait(accept_error)) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
if (!outage_logged) {
|
||||
log_debug(" [API] HTTP accept paused: %s; recovery scheduled",
|
||||
strerror(accept_error));
|
||||
outage_logged = true;
|
||||
}
|
||||
if (!wait_before_listener_retry(retry_delay_ms))
|
||||
break;
|
||||
retry_delay_ms = next_retry_delay_ms(retry_delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool owns_listener = g_api.listen_fd == listen_fd;
|
||||
if (owns_listener)
|
||||
g_api.listen_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
if (owns_listener)
|
||||
close(listen_fd);
|
||||
listen_fd = -1;
|
||||
if (!outage_logged) {
|
||||
log_debug(" [API] HTTP listener lost: %s; recovery scheduled",
|
||||
strerror(accept_error));
|
||||
outage_logged = true;
|
||||
}
|
||||
if (!wait_before_listener_retry(retry_delay_ms))
|
||||
break;
|
||||
retry_delay_ms = next_retry_delay_ms(retry_delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (outage_logged) {
|
||||
log_debug(" [API] HTTP accept recovered");
|
||||
outage_logged = false;
|
||||
}
|
||||
retry_delay_ms = SM_API_RETRY_INITIAL_MS;
|
||||
|
||||
struct timeval timeout = {.tv_sec = 5, .tv_usec = 0};
|
||||
(void)setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
(void)setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.stop_requested) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
close(client);
|
||||
break;
|
||||
}
|
||||
g_api.client_fd = client;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
handle_client(client);
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.client_fd == client)
|
||||
g_api.client_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
close(client);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_api_service_start(void) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
char bind_address[MAX_API_BIND_ADDRESS];
|
||||
(void)strlcpy(bind_address, cfg->api_bind_address, sizeof(bind_address));
|
||||
uint16_t port = (uint16_t)cfg->api_port;
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.started) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return true;
|
||||
}
|
||||
g_api.listen_fd = -1;
|
||||
g_api.client_fd = -1;
|
||||
g_api.stop_requested = false;
|
||||
(void)strlcpy(g_api.bind_address, bind_address,
|
||||
sizeof(g_api.bind_address));
|
||||
g_api.port = port;
|
||||
int rc = pthread_create(&g_api.thread, NULL, service_thread_main, NULL);
|
||||
if (rc != 0) {
|
||||
g_api.listen_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
errno = rc;
|
||||
return false;
|
||||
}
|
||||
g_api.started = true;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_api_service_stop(void) {
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (!g_api.started) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return;
|
||||
}
|
||||
g_api.stop_requested = true;
|
||||
int fd = g_api.listen_fd;
|
||||
int client_fd = g_api.client_fd;
|
||||
g_api.listen_fd = -1;
|
||||
pthread_cond_broadcast(&g_api.retry_cond);
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
if (client_fd >= 0)
|
||||
(void)shutdown(client_fd, SHUT_RDWR);
|
||||
if (fd >= 0) {
|
||||
(void)shutdown(fd, SHUT_RDWR);
|
||||
close(fd);
|
||||
}
|
||||
pthread_join(g_api.thread, NULL);
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
g_api.started = false;
|
||||
g_api.stop_requested = false;
|
||||
g_api.client_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
}
|
||||
|
||||
bool sm_api_service_reconfigure(void) {
|
||||
sm_api_service_stop();
|
||||
return sm_api_service_start();
|
||||
}
|
||||
+226
-210
@@ -11,32 +11,39 @@
|
||||
#include "sm_paths.h"
|
||||
|
||||
static sqlite3 *g_app_db;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0;
|
||||
static sqlite3_stmt *g_app_db_stmt_normalize_snd0;
|
||||
static struct AppDbTitleList g_app_db_title_cache;
|
||||
static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache;
|
||||
static bool g_app_db_title_cache_ready = false;
|
||||
static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
static time_t g_app_db_title_cache_mtime = 0;
|
||||
static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
struct AppDbTitleCache {
|
||||
struct AppDbTitleList list;
|
||||
uint64_t revision;
|
||||
time_t mtime;
|
||||
bool ready;
|
||||
};
|
||||
|
||||
static struct AppDbTitleCache g_app_db_title_cache;
|
||||
static struct AppDbTitleCache g_app_db_auto_remove_cache;
|
||||
static struct AppDbTitleCache g_app_db_blocked_uninstall_ppsa_cache;
|
||||
static pthread_mutex_t g_app_db_write_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_app_db_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
|
||||
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
|
||||
|
||||
static void close_app_db(void) {
|
||||
if (g_app_db_stmt_update_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_update_snd0);
|
||||
g_app_db_stmt_update_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db_stmt_normalize_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
|
||||
g_app_db_stmt_normalize_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db) {
|
||||
sqlite3_close(g_app_db);
|
||||
g_app_db = NULL;
|
||||
static bool close_sqlite_handle(sqlite3 **db, const char *label) {
|
||||
if (!*db)
|
||||
return true;
|
||||
|
||||
int rc = sqlite3_close_v2(*db);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_debug(" [DB] close failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(*db));
|
||||
return false;
|
||||
}
|
||||
*db = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool close_app_db(void) {
|
||||
return close_sqlite_handle(&g_app_db, "write connection");
|
||||
}
|
||||
|
||||
void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
@@ -46,16 +53,27 @@ void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
list->capacity = 0;
|
||||
}
|
||||
|
||||
static void invalidate_app_db_title_cache_entry(
|
||||
struct AppDbTitleCache *cache) {
|
||||
cache->ready = false;
|
||||
cache->mtime = 0;
|
||||
++cache->revision;
|
||||
}
|
||||
|
||||
void shutdown_app_db(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
close_app_db();
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
(void)close_app_db();
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
free_app_db_title_list(&g_app_db_title_cache.list);
|
||||
free_app_db_title_list(&g_app_db_auto_remove_cache.list);
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache.list);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_title_cache);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_auto_remove_cache);
|
||||
invalidate_app_db_title_cache_entry(
|
||||
&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
}
|
||||
|
||||
static bool ensure_app_db_open(void) {
|
||||
@@ -175,7 +193,7 @@ static int app_db_query_int(const char *sql, const char *label) {
|
||||
|
||||
bool app_db_run_startup_maintenance(void) {
|
||||
bool ok = false;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
|
||||
if (!ensure_app_db_open())
|
||||
goto out;
|
||||
@@ -215,110 +233,72 @@ bool app_db_run_startup_maintenance(void) {
|
||||
ok = true;
|
||||
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
static int app_db_update_title(const char *title_id, const char *sql,
|
||||
const char *label) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(
|
||||
sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES, label);
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
|
||||
if (!g_app_db_stmt_update_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
|
||||
"WHERE titleId = ?1;";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_update_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info update");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_update_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; ++attempt) {
|
||||
(void)sqlite3_reset(stmt);
|
||||
(void)sqlite3_clear_bindings(stmt);
|
||||
if (sqlite3_bind_text(stmt, 1, title_id, -1, SQLITE_TRANSIENT) !=
|
||||
SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for %s: %s", label,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
result = sqlite3_changes(g_app_db);
|
||||
break;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
log_debug(" [DB] step failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
(void)sqlite3_finalize(stmt);
|
||||
(void)close_app_db();
|
||||
return result;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
|
||||
"WHERE titleId = ?1;";
|
||||
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
int result = app_db_update_title(title_id, sql, "snd0info update");
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
int normalize_snd0info_for_title(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
|
||||
if (!g_app_db_stmt_normalize_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info normalize");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_normalize_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info normalize: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
int result = app_db_update_title(title_id, sql, "snd0info normalize");
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -354,87 +334,115 @@ bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
compare_title_id_str) != NULL;
|
||||
}
|
||||
|
||||
static bool step_app_db_title_query(struct AppDbTitleList *list,
|
||||
sqlite3_stmt *stmt,
|
||||
const char *label) {
|
||||
static bool step_app_db_title_query(struct AppDbTitleList *list, sqlite3 *db,
|
||||
sqlite3_stmt *stmt, const char *label) {
|
||||
int busy_attempts = 0;
|
||||
while (!should_stop_requested()) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (!append_app_db_title(list, title_id)) {
|
||||
log_debug(" [DB] %s allocation failed", label);
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
if (append_app_db_title(list, title_id))
|
||||
continue;
|
||||
log_debug(" [DB] %s allocation failed", label);
|
||||
return false;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
if (rc == SQLITE_DONE)
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
|
||||
busy_attempts++;
|
||||
++busy_attempts;
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] %s query failed: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
sqlite3_errmsg(db));
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
static bool load_app_db_title_query(struct AppDbTitleList *list,
|
||||
const char *sql, const char *label) {
|
||||
free_app_db_title_list(list);
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
int rc = sqlite3_open_v2(APP_DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_debug(" [DB] open failed for %s: rc=%d err=%s", label, rc,
|
||||
db ? sqlite3_errmsg(db) : APP_DB_PATH);
|
||||
if (db)
|
||||
(void)close_sqlite_handle(&db, label);
|
||||
return false;
|
||||
}
|
||||
(void)sqlite3_busy_timeout(db, APP_DB_BUSY_TIMEOUT_MS);
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
for (int attempt = 0; attempt < APP_DB_PREPARE_BUSY_RETRIES; ++attempt) {
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK)
|
||||
break;
|
||||
if ((rc != SQLITE_BUSY && rc != SQLITE_LOCKED) ||
|
||||
attempt + 1 >= APP_DB_PREPARE_BUSY_RETRIES ||
|
||||
should_stop_requested()) {
|
||||
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(db));
|
||||
break;
|
||||
}
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
}
|
||||
|
||||
bool loaded = rc == SQLITE_OK &&
|
||||
step_app_db_title_query(list, db, stmt, label);
|
||||
|
||||
if (stmt)
|
||||
(void)sqlite3_finalize(stmt);
|
||||
if (!close_sqlite_handle(&db, label))
|
||||
loaded = false;
|
||||
if (!loaded)
|
||||
free_app_db_title_list(list);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "title list");
|
||||
bool loaded = load_app_db_title_query(list, sql, "title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
static bool load_app_db_auto_remove_title_list(struct AppDbTitleList *list) {
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE metaDataPath GLOB '/user/app/PPSA*/sce_sys' "
|
||||
"ORDER BY titleId;";
|
||||
bool loaded =
|
||||
load_app_db_title_query(list, sql, "auto-remove title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db auto-remove title list: %d entries",
|
||||
list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId LIKE 'PPSA%' "
|
||||
"AND uninstallable = 0 "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc =
|
||||
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"blocked PPSA uninstallable query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
|
||||
bool loaded =
|
||||
load_app_db_title_query(list, sql, "blocked PPSA uninstall");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
|
||||
list->count);
|
||||
@@ -442,12 +450,12 @@ static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list)
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_title_cache);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_auto_remove_cache);
|
||||
invalidate_app_db_title_cache_entry(
|
||||
&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
}
|
||||
|
||||
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
@@ -470,69 +478,77 @@ static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
typedef bool (*load_app_db_title_list_fn)(struct AppDbTitleList *list);
|
||||
|
||||
static bool get_cached_app_db_title_list(
|
||||
struct AppDbTitleList *list_out, struct AppDbTitleCache *cache,
|
||||
load_app_db_title_list_fn load_fn) {
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
|
||||
if (!g_app_db_title_cache_ready ||
|
||||
(app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
g_app_db_title_cache = fresh;
|
||||
g_app_db_title_cache_mtime = st.st_mtime;
|
||||
g_app_db_title_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_title_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ShellCore can keep app.db busy for several five-second timeout windows
|
||||
// immediately after WORKING. The pre-suspend snapshot is sufficient for
|
||||
// mounting already registered games and keeps resume scans non-blocking.
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
bool cache_is_current =
|
||||
cache->ready &&
|
||||
(runtime_resume_grace_active() || app_db_stat_rc != 0 ||
|
||||
cache->mtime == st.st_mtime);
|
||||
if (cache_is_current) {
|
||||
bool copied = copy_app_db_title_list(list_out, &cache->list);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
return copied;
|
||||
}
|
||||
uint64_t revision = cache->revision;
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
|
||||
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
struct AppDbTitleList fresh = {0};
|
||||
bool loaded = app_db_stat_rc == 0 && load_fn(&fresh);
|
||||
struct stat verified_st;
|
||||
bool source_unchanged =
|
||||
loaded && stat(APP_DB_PATH, &verified_st) == 0 &&
|
||||
verified_st.st_mtime == st.st_mtime;
|
||||
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
if (source_unchanged && cache->revision == revision) {
|
||||
free_app_db_title_list(&cache->list);
|
||||
cache->list = fresh;
|
||||
memset(&fresh, 0, sizeof(fresh));
|
||||
cache->mtime = st.st_mtime;
|
||||
cache->ready = true;
|
||||
++cache->revision;
|
||||
}
|
||||
bool copied = cache->ready &&
|
||||
copy_app_db_title_list(list_out, &cache->list);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
free_app_db_title_list(&fresh);
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
return get_cached_app_db_title_list(list_out, &g_app_db_title_cache,
|
||||
load_app_db_title_list);
|
||||
}
|
||||
|
||||
bool get_app_db_auto_remove_title_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
return get_cached_app_db_title_list(
|
||||
list_out, &g_app_db_auto_remove_cache,
|
||||
load_app_db_auto_remove_title_list);
|
||||
}
|
||||
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
|
||||
if (!g_app_db_blocked_uninstall_ppsa_cache_ready ||
|
||||
(app_db_stat_rc == 0 &&
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 &&
|
||||
load_app_db_blocked_uninstall_ppsa_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_blocked_uninstall_ppsa_cache = fresh;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_blocked_uninstall_ppsa_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool loaded =
|
||||
copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return loaded;
|
||||
return get_cached_app_db_title_list(
|
||||
list_out, &g_app_db_blocked_uninstall_ppsa_cache,
|
||||
load_app_db_blocked_uninstall_ppsa_list);
|
||||
}
|
||||
+312
-72
@@ -1,7 +1,14 @@
|
||||
#include "sm_platform.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sm_api_protocol.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_l10n.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_defs.h"
|
||||
#include "sm_mount_device.h"
|
||||
@@ -61,6 +68,7 @@ static runtime_config_state_t
|
||||
static _Atomic int g_runtime_state_active_index = 0;
|
||||
static atomic_bool g_runtime_cfg_ready = false;
|
||||
static config_file_stamp_t g_config_file_stamp;
|
||||
static pthread_mutex_t g_autotune_file_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static char *trim_ascii(char *s);
|
||||
static bool parse_ini_line(char *line, char **key_out, char **value_out);
|
||||
@@ -75,6 +83,9 @@ static bool add_global_fakelib_exclude_rule(runtime_config_state_t *state,
|
||||
const char *value);
|
||||
static bool normalize_image_filename_value(const char *value,
|
||||
char out[MAX_PATH]);
|
||||
static bool normalize_absolute_path_value(const char *value,
|
||||
char out[MAX_PATH]);
|
||||
static bool normalize_http_url_value(const char *value, char out[MAX_PATH]);
|
||||
static bool set_image_sector_rule(runtime_config_state_t *state,
|
||||
const char *value);
|
||||
static bool parse_image_sector_rule_value(const char *value,
|
||||
@@ -98,7 +109,6 @@ static bool lookup_kstuff_delay_override_in_file(const char *path,
|
||||
static bool upsert_kstuff_delay_override_in_file(const char *path,
|
||||
const char *title_id,
|
||||
uint32_t delay_seconds);
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state);
|
||||
|
||||
static char *trim_ascii(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
@@ -152,19 +162,6 @@ static bool parse_ini_line(char *line, char **key_out, char **value_out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state) {
|
||||
if (!state)
|
||||
return;
|
||||
|
||||
if (sm_firmware_major_version() >= 12u) {
|
||||
state->cfg.app_install_all_enabled = true;
|
||||
state->cfg.app_install_all_forced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state->cfg.app_install_all_forced = false;
|
||||
}
|
||||
|
||||
static const runtime_config_state_t *active_runtime_state(void) {
|
||||
return &g_runtime_state_slots[atomic_load_explicit(&g_runtime_state_active_index,
|
||||
memory_order_acquire)];
|
||||
@@ -249,36 +246,53 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
state->cfg.quiet_mode = false;
|
||||
state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
state->cfg.force_mount = false;
|
||||
state->cfg.persistent_image_mounts = false;
|
||||
state->cfg.app_install_all_enabled = false;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
state->cfg.auto_remove_missing_games = false;
|
||||
state->cfg.auto_remove_games_with_dlc = false;
|
||||
state->cfg.backport_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_mount_first = true;
|
||||
state->cfg.global_fakelib_game_priority = true;
|
||||
state->cfg.update_emulators_enabled = true;
|
||||
state->cfg.auto_update_ampr_enabled = false;
|
||||
state->cfg.kstuff_game_auto_toggle = true;
|
||||
state->cfg.kstuff_crash_detection_enabled = true;
|
||||
state->cfg.legacy_recursive_scan_forced = false;
|
||||
state->cfg.watch_subdirs = false;
|
||||
state->cfg.watch_image_files = true;
|
||||
(void)strlcpy(state->cfg.api_bind_address, SM_API_DEFAULT_BIND_ADDRESS,
|
||||
sizeof(state->cfg.api_bind_address));
|
||||
state->cfg.api_port = SM_API_DEFAULT_PORT;
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, DEFAULT_GLOBAL_FAKELIB_PATH,
|
||||
sizeof(state->cfg.global_fakelib_path));
|
||||
(void)strlcpy(state->cfg.emulators_path, DEFAULT_EMULATORS_PATH,
|
||||
sizeof(state->cfg.emulators_path));
|
||||
(void)strlcpy(state->cfg.ampr_update_url, DEFAULT_AMPR_UPDATE_URL,
|
||||
sizeof(state->cfg.ampr_update_url));
|
||||
state->cfg.scan_depth = DEFAULT_SCAN_DEPTH;
|
||||
state->cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US;
|
||||
state->cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS;
|
||||
state->cfg.auto_remove_missing_delay_seconds =
|
||||
DEFAULT_AUTO_REMOVE_MISSING_DELAY_SECONDS;
|
||||
state->cfg.kstuff_pause_delay_image_seconds =
|
||||
DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS;
|
||||
state->cfg.kstuff_pause_delay_direct_seconds =
|
||||
DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS;
|
||||
state->cfg.fan_target_temperature_c = FAN_TARGET_TEMPERATURE_SYSTEM;
|
||||
state->cfg.language_id = SM_LANGUAGE_AUTO;
|
||||
state->cfg.exfat_backend = default_exfat_backend();
|
||||
state->cfg.ufs_backend = default_ufs_backend();
|
||||
state->cfg.legacy_mount_ufs = true;
|
||||
state->cfg.legacy_mount_exfat = true;
|
||||
state->cfg.legacy_mount_pfs = true;
|
||||
state->cfg.legacy_mount_pfsc = true;
|
||||
state->cfg.nested_pfs_index_cache_enabled = false;
|
||||
state->cfg.lvd_sector_exfat = LVD_SECTOR_SIZE_EXFAT;
|
||||
state->cfg.lvd_sector_ufs = LVD_SECTOR_SIZE_UFS;
|
||||
state->cfg.lvd_sector_pfs = LVD_SECTOR_SIZE_PFS;
|
||||
state->cfg.md_sector_exfat = MD_SECTOR_SIZE_EXFAT;
|
||||
state->cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS;
|
||||
state->cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS_LEGACY;
|
||||
memset(state->image_mode_rules, 0, sizeof(state->image_mode_rules));
|
||||
clear_kstuff_title_rules(state);
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
apply_firmware_runtime_overrides(state);
|
||||
}
|
||||
|
||||
static config_file_stamp_t read_config_file_stamp(void) {
|
||||
@@ -399,8 +413,11 @@ bool get_image_sector_size_override(const char *filename,
|
||||
if (!filename || !sector_size_out)
|
||||
return false;
|
||||
|
||||
if (lookup_image_sector_override_in_file(AUTOTUNE_FILE, filename,
|
||||
sector_size_out)) {
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool autotuned = lookup_image_sector_override_in_file(
|
||||
AUTOTUNE_FILE, filename, sector_size_out);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
if (autotuned) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -480,8 +497,11 @@ bool get_kstuff_pause_delay_override_for_title(const char *title_id,
|
||||
|
||||
bool get_kstuff_autotune_pause_delay_for_title(const char *title_id,
|
||||
uint32_t *delay_seconds_out) {
|
||||
return lookup_kstuff_delay_override_in_file(AUTOTUNE_FILE, title_id,
|
||||
delay_seconds_out);
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool found = lookup_kstuff_delay_override_in_file(
|
||||
AUTOTUNE_FILE, title_id, delay_seconds_out);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool upsert_kstuff_autotune_pause_delay(const char *title_id,
|
||||
@@ -499,8 +519,11 @@ bool upsert_kstuff_autotune_pause_delay(const char *title_id,
|
||||
tuned_delay_seconds = 1;
|
||||
if (tuned_delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS)
|
||||
tuned_delay_seconds = MAX_KSTUFF_PAUSE_DELAY_SECONDS;
|
||||
if (!upsert_kstuff_delay_override_in_file(AUTOTUNE_FILE, normalized,
|
||||
(uint32_t)tuned_delay_seconds)) {
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool updated = upsert_kstuff_delay_override_in_file(
|
||||
AUTOTUNE_FILE, normalized, (uint32_t)tuned_delay_seconds);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
if (delay_seconds_out)
|
||||
@@ -520,8 +543,11 @@ bool upsert_image_sector_size_autotune(const char *filename,
|
||||
if (!normalize_image_filename_value(filename, normalized_filename))
|
||||
return false;
|
||||
|
||||
if (!upsert_image_sector_override_in_file(AUTOTUNE_FILE, normalized_filename,
|
||||
sector_size)) {
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool updated = upsert_image_sector_override_in_file(
|
||||
AUTOTUNE_FILE, normalized_filename, sector_size);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -572,6 +598,62 @@ static bool normalize_image_filename_value(const char *value,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool normalize_absolute_path_value(const char *value,
|
||||
char out[MAX_PATH]) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
|
||||
char local[MAX_PATH];
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local))
|
||||
return false;
|
||||
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_PATH || trimmed[0] != '/')
|
||||
return false;
|
||||
|
||||
while (len > 1 && trimmed[len - 1] == '/') {
|
||||
trimmed[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
(void)strlcpy(out, trimmed, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool normalize_http_url_value(const char *value, char out[MAX_PATH]) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
|
||||
char local[MAX_PATH];
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local))
|
||||
return false;
|
||||
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
const char *authority = NULL;
|
||||
if (strncasecmp(trimmed, "https://", 8) == 0)
|
||||
authority = trimmed + 8;
|
||||
else if (strncasecmp(trimmed, "http://", 7) == 0)
|
||||
authority = trimmed + 7;
|
||||
else
|
||||
return false;
|
||||
if (*authority == '\0' || *authority == '/' || *authority == '?' ||
|
||||
*authority == '#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (iscntrl((unsigned char)trimmed[i]) ||
|
||||
isspace((unsigned char)trimmed[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(void)strlcpy(out, trimmed, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_bool_ini(const char *value, bool *out) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
@@ -1137,6 +1219,7 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
int line_no = 0;
|
||||
bool has_custom_scanpaths = false;
|
||||
bool legacy_recursive_scan_requested = false;
|
||||
bool md_ufs_sector_configured = false;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
line_no++;
|
||||
char *key = NULL;
|
||||
@@ -1173,6 +1256,42 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "language") == 0 || strcasecmp(key, "lang") == 0 ||
|
||||
strcasecmp(key, "locale") == 0) {
|
||||
int32_t language_id = SM_LANGUAGE_AUTO;
|
||||
if (!sm_l10n_parse_language_id(value, &language_id)) {
|
||||
log_debug(" [CFG] invalid language at line %d: %s=%s "
|
||||
"(use auto or a supported locale like en-US, ru-RU)",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.language_id = language_id;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "api_bind_address") == 0) {
|
||||
struct in_addr address;
|
||||
if (strlen(value) >= sizeof(state->cfg.api_bind_address) ||
|
||||
inet_pton(AF_INET, value, &address) != 1) {
|
||||
log_debug(" [CFG] invalid API IPv4 address at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
(void)strlcpy(state->cfg.api_bind_address, value,
|
||||
sizeof(state->cfg.api_bind_address));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "api_port") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 == 0 || u32 > 65535u) {
|
||||
log_debug(" [CFG] invalid API port at line %d: %s=%s (range: 1..65535)",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.api_port = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "mount_read_only") == 0 ||
|
||||
strcasecmp(key, "read_only") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
@@ -1192,13 +1311,39 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "persistent_image_mounts") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.persistent_image_mounts = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "app_install_all") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.app_install_all_enabled = bval;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_remove_missing_games") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_remove_missing_games = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_remove_games_with_dlc") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_remove_games_with_dlc = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1243,24 +1388,6 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "watch_subdirs") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.watch_subdirs = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "watch_image_files") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.watch_image_files = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "backport_fakelib") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
@@ -1281,34 +1408,22 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib_path") == 0) {
|
||||
char local[MAX_PATH];
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local)) {
|
||||
char path[MAX_PATH];
|
||||
if (!normalize_absolute_path_value(value, path)) {
|
||||
log_debug(" [CFG] invalid global fakelib path at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_PATH || trimmed[0] != '/') {
|
||||
log_debug(" [CFG] invalid global fakelib path at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
while (len > 1 && trimmed[len - 1] == '/') {
|
||||
trimmed[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, trimmed,
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, path,
|
||||
sizeof(state->cfg.global_fakelib_path));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib_priority") == 0) {
|
||||
// Lower-priority overlay must be mounted first.
|
||||
if (strcasecmp(value, "game") == 0) {
|
||||
state->cfg.global_fakelib_mount_first = true;
|
||||
state->cfg.global_fakelib_game_priority = true;
|
||||
} else if (strcasecmp(value, "global") == 0) {
|
||||
state->cfg.global_fakelib_mount_first = false;
|
||||
state->cfg.global_fakelib_game_priority = false;
|
||||
} else {
|
||||
log_debug(" [CFG] invalid global fakelib priority at line %d: %s=%s "
|
||||
"(use game or global)",
|
||||
@@ -1346,6 +1461,68 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "update_emulators") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.update_emulators_enabled = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "emulators_path") == 0) {
|
||||
char path[MAX_PATH];
|
||||
if (!normalize_absolute_path_value(value, path)) {
|
||||
log_debug(" [CFG] invalid emulator path at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
(void)strlcpy(state->cfg.emulators_path, path,
|
||||
sizeof(state->cfg.emulators_path));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_update_ampr") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_update_ampr_enabled = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "ampr_update_url") == 0) {
|
||||
char url[MAX_PATH];
|
||||
if (!normalize_http_url_value(value, url)) {
|
||||
log_debug(" [CFG] invalid AMPR update URL at line %d: %s=%s "
|
||||
"(HTTP or HTTPS required)",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
(void)strlcpy(state->cfg.ampr_update_url, url,
|
||||
sizeof(state->cfg.ampr_update_url));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "fan_target_temperature") == 0) {
|
||||
if (strcasecmp(value, "system") == 0 ||
|
||||
strcasecmp(value, "auto") == 0) {
|
||||
state->cfg.fan_target_temperature_c = FAN_TARGET_TEMPERATURE_SYSTEM;
|
||||
} else if (!parse_u32_ini(value, &u32) ||
|
||||
u32 < MIN_FAN_TARGET_TEMPERATURE_C ||
|
||||
u32 > MAX_FAN_TARGET_TEMPERATURE_C) {
|
||||
log_debug(" [CFG] invalid fan target at line %d: %s=%s "
|
||||
"(use system or %u..%u C)",
|
||||
line_no, key, value,
|
||||
(unsigned)MIN_FAN_TARGET_TEMPERATURE_C,
|
||||
(unsigned)MAX_FAN_TARGET_TEMPERATURE_C);
|
||||
continue;
|
||||
} else {
|
||||
state->cfg.fan_target_temperature_c = u32;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_no_pause") == 0) {
|
||||
if (!add_kstuff_no_pause_title_rule(state, value)) {
|
||||
log_debug(" [CFG] invalid kstuff no-pause title rule at line %d: "
|
||||
@@ -1388,6 +1565,22 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_remove_missing_delay_seconds") == 0 ||
|
||||
strcasecmp(key, "auto_remove_missing_delay_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) ||
|
||||
u32 < MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS ||
|
||||
u32 > MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS) {
|
||||
log_debug(" [CFG] invalid auto-remove delay at line %d: %s=%s "
|
||||
"(range: %u..%u)",
|
||||
line_no, key, value,
|
||||
(unsigned)MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS,
|
||||
(unsigned)MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_remove_missing_delay_seconds = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_pause_delay_image_seconds") == 0 ||
|
||||
strcasecmp(key, "kstuff_pause_delay_image_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
|
||||
@@ -1432,6 +1625,29 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
bool *bool_option = NULL;
|
||||
if (strcasecmp(key, "legacy_mount_ufs") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_ufs;
|
||||
} else if (strcasecmp(key, "legacy_mount_exfat") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_exfat;
|
||||
} else if (strcasecmp(key, "legacy_mount_pfs") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_pfs;
|
||||
} else if (strcasecmp(key, "legacy_mount_pfsc") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_pfsc;
|
||||
} else if (strcasecmp(key, "nested_pfs_index_cache") == 0 ||
|
||||
strcasecmp(key, "legacy_gddr5_cache") == 0) {
|
||||
bool_option = &state->cfg.nested_pfs_index_cache_enabled;
|
||||
}
|
||||
if (bool_option) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
continue;
|
||||
}
|
||||
*bool_option = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "scanpath") == 0) {
|
||||
if (!has_custom_scanpaths) {
|
||||
clear_runtime_scan_paths(state);
|
||||
@@ -1472,6 +1688,7 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
state->cfg.md_sector_exfat = u32;
|
||||
} else if (strcasecmp(key, "md_ufs_sector_size") == 0) {
|
||||
state->cfg.md_sector_ufs = u32;
|
||||
md_ufs_sector_configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1483,13 +1700,15 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
}
|
||||
add_runtime_managed_scan_paths(state);
|
||||
|
||||
if (!md_ufs_sector_configured && !state->cfg.legacy_mount_ufs)
|
||||
state->cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS_OPTIMIZED;
|
||||
|
||||
if (legacy_recursive_scan_requested) {
|
||||
state->cfg.scan_depth = 2u;
|
||||
state->cfg.legacy_recursive_scan_forced = true;
|
||||
log_debug(" [CFG] recursive_scan=1 is deprecated; forcing scan_depth=2");
|
||||
}
|
||||
|
||||
apply_firmware_runtime_overrides(state);
|
||||
|
||||
int image_rule_count = 0;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
@@ -1503,37 +1722,58 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
kstuff_delay_rule_count++;
|
||||
}
|
||||
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d "
|
||||
"app_install_all=%d app_install_all_forced=%d scan_depth=%u "
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d language=%s ro=%d force=%d "
|
||||
"persistent_image_mounts=%d app_install_all=%d "
|
||||
"auto_remove_missing_games=%d "
|
||||
"auto_remove_games_with_dlc=%d auto_remove_missing_delay_s=%u "
|
||||
"api=%s:%u scan_depth=%u "
|
||||
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
|
||||
"global_fakelib=%d global_fakelib_priority=%s "
|
||||
"global_fakelib_path=%s global_fakelib_exclude=%u "
|
||||
"watch_subdirs=%d watch_image_files=%d "
|
||||
"update_emulators=%d emulators_path=%s auto_update_ampr=%d "
|
||||
"ampr_update_url=%s "
|
||||
"kstuff_game_auto_toggle=%d kstuff_crash_detection=%d "
|
||||
"fan_target_temperature=%u (0=system) "
|
||||
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
|
||||
"exfat_backend=%s ufs_backend=%s "
|
||||
"legacy_mount(ufs=%d exfat=%d pfs=%d pfsc=%d) "
|
||||
"nested_pfs_index_cache=%d "
|
||||
"lvd_sec(exfat=%u ufs=%u pfs=%u) md_sec(exfat=%u ufs=%u) "
|
||||
"scan_interval_s=%u stability_wait_s=%u scan_paths=%d image_rules=%d "
|
||||
"kstuff_no_pause=%d kstuff_delay_rules=%d",
|
||||
state->cfg.debug_enabled ? 1 : 0, state->cfg.quiet_mode ? 1 : 0,
|
||||
sm_l10n_language_name(state->cfg.language_id),
|
||||
state->cfg.mount_read_only ? 1 : 0,
|
||||
state->cfg.force_mount ? 1 : 0,
|
||||
state->cfg.persistent_image_mounts ? 1 : 0,
|
||||
state->cfg.app_install_all_enabled ? 1 : 0,
|
||||
state->cfg.app_install_all_forced ? 1 : 0, state->cfg.scan_depth,
|
||||
state->cfg.auto_remove_missing_games ? 1 : 0,
|
||||
state->cfg.auto_remove_games_with_dlc ? 1 : 0,
|
||||
state->cfg.auto_remove_missing_delay_seconds,
|
||||
state->cfg.api_bind_address, state->cfg.api_port,
|
||||
state->cfg.scan_depth,
|
||||
state->cfg.legacy_recursive_scan_forced ? 1 : 0,
|
||||
state->cfg.backport_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_mount_first ? "game" : "global",
|
||||
state->cfg.global_fakelib_game_priority ? "game" : "global",
|
||||
state->cfg.global_fakelib_path,
|
||||
state->cfg.global_fakelib_exclude_title_count,
|
||||
state->cfg.watch_subdirs ? 1 : 0,
|
||||
state->cfg.watch_image_files ? 1 : 0,
|
||||
state->cfg.update_emulators_enabled ? 1 : 0,
|
||||
state->cfg.emulators_path,
|
||||
state->cfg.auto_update_ampr_enabled ? 1 : 0,
|
||||
state->cfg.ampr_update_url,
|
||||
state->cfg.kstuff_game_auto_toggle ? 1 : 0,
|
||||
state->cfg.kstuff_crash_detection_enabled ? 1 : 0,
|
||||
state->cfg.fan_target_temperature_c,
|
||||
state->cfg.kstuff_pause_delay_image_seconds,
|
||||
state->cfg.kstuff_pause_delay_direct_seconds,
|
||||
attach_backend_name(state->cfg.exfat_backend),
|
||||
attach_backend_name(state->cfg.ufs_backend),
|
||||
state->cfg.legacy_mount_ufs ? 1 : 0,
|
||||
state->cfg.legacy_mount_exfat ? 1 : 0,
|
||||
state->cfg.legacy_mount_pfs ? 1 : 0,
|
||||
state->cfg.legacy_mount_pfsc ? 1 : 0,
|
||||
state->cfg.nested_pfs_index_cache_enabled ? 1 : 0,
|
||||
state->cfg.lvd_sector_exfat, state->cfg.lvd_sector_ufs,
|
||||
state->cfg.lvd_sector_pfs, state->cfg.md_sector_exfat,
|
||||
state->cfg.md_sector_ufs, state->cfg.scan_interval_us / 1000000u,
|
||||
|
||||
+1009
-63
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,86 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_fan.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#define FAN_DEVICE "/dev/icc_fan"
|
||||
#define FAN_GET_AUTOSERVO 0xC01C8F08UL
|
||||
#define FAN_SET_AUTOSERVO 0xC01C8F07UL
|
||||
#define FAN_CONFIG_SIZE 28u
|
||||
#define FAN_SELECTOR 0u
|
||||
#define FAN_TARGET_OFFSET 5u
|
||||
|
||||
static int get_fan_config(int fd, uint8_t config[FAN_CONFIG_SIZE]) {
|
||||
memset(config, 0, FAN_CONFIG_SIZE);
|
||||
config[2] = FAN_SELECTOR;
|
||||
return ioctl(fd, FAN_GET_AUTOSERVO, config);
|
||||
}
|
||||
|
||||
static int set_fan_target(int fd, uint8_t target_c,
|
||||
uint8_t *previous_target_out) {
|
||||
uint8_t current[FAN_CONFIG_SIZE];
|
||||
uint8_t verified[FAN_CONFIG_SIZE];
|
||||
|
||||
if (get_fan_config(fd, current) != 0)
|
||||
return -1;
|
||||
|
||||
*previous_target_out = current[FAN_TARGET_OFFSET];
|
||||
if (*previous_target_out == target_c)
|
||||
return 0;
|
||||
|
||||
current[FAN_TARGET_OFFSET] = target_c;
|
||||
if (ioctl(fd, FAN_SET_AUTOSERVO, current) != 0)
|
||||
return -1;
|
||||
if (get_fan_config(fd, verified) != 0)
|
||||
return -1;
|
||||
if (verified[FAN_TARGET_OFFSET] == target_c)
|
||||
return 0;
|
||||
|
||||
errno = EIO;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int apply_fan_target(uint8_t target_c,
|
||||
uint8_t *previous_target_out) {
|
||||
int fd = open(FAN_DEVICE, O_RDWR | O_DIRECT);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
int result = set_fan_target(fd, target_c, previous_target_out);
|
||||
int saved_errno = errno;
|
||||
close(fd);
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
|
||||
void sm_fan_apply_game_target(const char *title_id) {
|
||||
static bool target_notification_sent = false;
|
||||
|
||||
uint32_t target_c = runtime_config()->fan_target_temperature_c;
|
||||
if (target_c == FAN_TARGET_TEMPERATURE_SYSTEM)
|
||||
return;
|
||||
|
||||
uint8_t previous_target = 0;
|
||||
if (apply_fan_target((uint8_t)target_c, &previous_target) != 0) {
|
||||
log_debug(" [FAN] target apply failed for %s: %s", title_id,
|
||||
strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
if (previous_target == target_c) {
|
||||
log_debug(" [FAN] target already %u C for %s", (unsigned)target_c,
|
||||
title_id);
|
||||
} else {
|
||||
log_debug(" [FAN] target applied for %s: %u C -> %u C", title_id,
|
||||
(unsigned)previous_target, (unsigned)target_c);
|
||||
}
|
||||
|
||||
if (!target_notification_sent) {
|
||||
target_notification_sent = true;
|
||||
notify_system_info_l10n(SM_L10N_TEMPERATURE_TRANSITION,
|
||||
(unsigned)previous_target, (unsigned)target_c);
|
||||
}
|
||||
}
|
||||
+492
-89
@@ -4,11 +4,16 @@
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_diag.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path);
|
||||
static bool unmount_controlled_mount_stack_impl(const char *path,
|
||||
bool diagnose_busy);
|
||||
|
||||
// --- FILESYSTEM ---
|
||||
bool is_installed(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
@@ -87,7 +92,7 @@ typedef bool (*title_app_dir_iter_fn)(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx);
|
||||
|
||||
static void for_each_title_app_dir(const char *open_error_context,
|
||||
static bool for_each_title_app_dir(const char *open_error_context,
|
||||
bool stop_on_signal,
|
||||
title_app_dir_iter_fn fn, void *ctx) {
|
||||
DIR *d = opendir(APP_BASE);
|
||||
@@ -95,23 +100,30 @@ static void for_each_title_app_dir(const char *open_error_context,
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [LINK] open %s failed%s: %s", APP_BASE,
|
||||
open_error_context ? open_error_context : "", strerror(errno));
|
||||
return;
|
||||
return errno == ENOENT;
|
||||
}
|
||||
|
||||
bool completed = true;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (stop_on_signal && should_stop_requested())
|
||||
if (stop_on_signal && should_stop_requested()) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
if (!resolve_title_app_dir(entry, NULL, 0))
|
||||
continue;
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(entry->d_name, &paths);
|
||||
if (!fn(entry->d_name, &paths, ctx))
|
||||
if (!fn(entry->d_name, &paths, ctx)) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
if (closedir(d) != 0)
|
||||
completed = false;
|
||||
return completed;
|
||||
}
|
||||
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
@@ -124,6 +136,131 @@ bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
return read_mount_link_file(paths.mount_link, out, out_size);
|
||||
}
|
||||
|
||||
static bool write_link_lines_atomic(const char *path,
|
||||
const char *const *lines,
|
||||
size_t count) {
|
||||
char temp_path[MAX_PATH];
|
||||
int written = snprintf(temp_path, sizeof(temp_path), "%s.tmp", path);
|
||||
if (written < 0 || (size_t)written >= sizeof(temp_path))
|
||||
return false;
|
||||
|
||||
FILE *file = fopen(temp_path, "w");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
bool ok = true;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (!lines[i] || lines[i][0] == '\0' ||
|
||||
fprintf(file, "%s%s", lines[i], i + 1u < count ? "\n" : "") < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok && fflush(file) != 0)
|
||||
ok = false;
|
||||
if (ok && fsync(fileno(file)) != 0)
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (ok && rename(temp_path, path) == 0)
|
||||
return true;
|
||||
|
||||
int saved_errno = errno != 0 ? errno : EIO;
|
||||
(void)unlink(temp_path);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool write_mount_link(const char *title_id, const char *source_path) {
|
||||
if (!title_id || title_id[0] == '\0' || !source_path ||
|
||||
source_path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(title_id, &paths);
|
||||
const char *lines[] = {source_path};
|
||||
return write_link_lines_atomic(paths.mount_link, lines, 1);
|
||||
}
|
||||
|
||||
bool read_mount_image_link(const char *title_id, char *out, size_t out_size) {
|
||||
if (!title_id || !out || out_size == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(title_id, &paths);
|
||||
return read_mount_link_file(paths.mount_image_link, out, out_size);
|
||||
}
|
||||
|
||||
bool write_mount_image_link(const char *title_id, const char *image_path) {
|
||||
if (!title_id || title_id[0] == '\0' || !image_path ||
|
||||
image_path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH] = {{0}};
|
||||
(void)strlcpy(chain[0], image_path, sizeof(chain[0]));
|
||||
return write_mount_image_chain(title_id, chain, 1);
|
||||
}
|
||||
|
||||
bool read_mount_image_chain(
|
||||
const char *title_id,
|
||||
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out) {
|
||||
if (!title_id || !paths || !count_out)
|
||||
return false;
|
||||
*count_out = 0;
|
||||
|
||||
title_link_paths_t link_paths;
|
||||
build_title_link_paths(title_id, &link_paths);
|
||||
FILE *file = fopen(link_paths.mount_image_link, "r");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
bool ok = true;
|
||||
while (*count_out < MAX_IMAGE_CHAIN_DEPTH &&
|
||||
fgets(paths[*count_out], MAX_PATH, file)) {
|
||||
size_t len = strcspn(paths[*count_out], "\r\n");
|
||||
if (paths[*count_out][len] == '\0' && len + 1u >= MAX_PATH) {
|
||||
int next = fgetc(file);
|
||||
if (next != EOF && next != '\n' && next != '\r') {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
paths[*count_out][len] = '\0';
|
||||
if (len == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
(*count_out)++;
|
||||
}
|
||||
if (ok && *count_out == MAX_IMAGE_CHAIN_DEPTH && fgetc(file) != EOF)
|
||||
ok = false;
|
||||
if (ferror(file))
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (!ok)
|
||||
*count_out = 0;
|
||||
return ok && *count_out > 0;
|
||||
}
|
||||
|
||||
bool write_mount_image_chain(
|
||||
const char *title_id,
|
||||
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count) {
|
||||
if (!title_id || title_id[0] == '\0' || !paths || count == 0 ||
|
||||
count > MAX_IMAGE_CHAIN_DEPTH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
title_link_paths_t link_paths;
|
||||
build_title_link_paths(title_id, &link_paths);
|
||||
const char *lines[MAX_IMAGE_CHAIN_DEPTH];
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
lines[i] = paths[i];
|
||||
return write_link_lines_atomic(link_paths.mount_image_link, lines, count);
|
||||
}
|
||||
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size) {
|
||||
if (!entry)
|
||||
@@ -159,14 +296,18 @@ bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool source_path_needs_cleanup(const char *source_path,
|
||||
bool *tried_image_recovery) {
|
||||
static bool source_path_needs_cleanup(const char *source_path) {
|
||||
if (is_under_image_mount_base(source_path)) {
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = resolve_image_source_from_mount_cache(
|
||||
source_path, image_source_path, sizeof(image_source_path));
|
||||
if (has_image_source && !path_exists(image_source_path))
|
||||
return true;
|
||||
if (has_image_source) {
|
||||
if (!path_exists(image_source_path))
|
||||
return true;
|
||||
// The image mount point is intentionally absent outside install/launch.
|
||||
if (!path_exists(source_path))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path_exists(source_path))
|
||||
@@ -182,11 +323,6 @@ static bool source_path_needs_cleanup(const char *source_path,
|
||||
if (path_exists(eboot_path))
|
||||
return false;
|
||||
|
||||
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
|
||||
cleanup_stale_image_mounts();
|
||||
*tried_image_recovery = true;
|
||||
}
|
||||
|
||||
return !path_exists(eboot_path);
|
||||
}
|
||||
|
||||
@@ -459,7 +595,8 @@ static bool inspect_title_stack(const char *title_id, const char *source_path,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_top_controlled_layer(const char *path) {
|
||||
static bool unmount_top_controlled_layer_impl(const char *path,
|
||||
bool diagnose_busy) {
|
||||
struct statfs mount_st;
|
||||
if (!get_top_mount(path, &mount_st))
|
||||
return true;
|
||||
@@ -476,13 +613,22 @@ static bool unmount_top_controlled_layer(const char *path) {
|
||||
|
||||
if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL)
|
||||
return true;
|
||||
if (unmount(path, MNT_FORCE) == 0 || errno == ENOENT || errno == EINVAL)
|
||||
return true;
|
||||
int unmount_errno = errno;
|
||||
if (unmount_errno == EBUSY && diagnose_busy)
|
||||
sm_mount_diag_log_busy(path);
|
||||
|
||||
log_debug(" [LINK] unmount failed for %s: %s", path, strerror(errno));
|
||||
if (unmount_errno != EBUSY || diagnose_busy) {
|
||||
log_debug(" [LINK] unmount deferred for %s: %s", path,
|
||||
strerror(unmount_errno));
|
||||
}
|
||||
errno = unmount_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_top_controlled_layer(const char *path) {
|
||||
return unmount_top_controlled_layer_impl(path, true);
|
||||
}
|
||||
|
||||
static bool title_stack_top_is_managed(const title_mount_state_t *state) {
|
||||
return state->top_is_our_nullfs || state->top_kind == TITLE_STACK_TOP_BACKPORT;
|
||||
}
|
||||
@@ -501,6 +647,60 @@ bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_title_runtime_layers_impl(const char *title_id,
|
||||
bool diagnose_busy) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
char path[MAX_PATH];
|
||||
int written = snprintf(path, sizeof(path), "/system_ex/app/%s", title_id);
|
||||
if (written < 0 || (size_t)written >= sizeof(path))
|
||||
return false;
|
||||
|
||||
if (!unmount_controlled_mount_stack_impl(path, diagnose_busy))
|
||||
return false;
|
||||
log_debug(" [LINK] runtime layers released: %s", title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unmount_title_runtime_layers(const char *title_id) {
|
||||
return unmount_title_runtime_layers_impl(title_id, true);
|
||||
}
|
||||
|
||||
bool unmount_title_runtime_layers_quiet(const char *title_id) {
|
||||
return unmount_title_runtime_layers_impl(title_id, false);
|
||||
}
|
||||
|
||||
static bool unmount_managed_title_entry(
|
||||
const char *title_id, const title_link_paths_t *paths, void *ctx) {
|
||||
bool *all_released = (bool *)ctx;
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path,
|
||||
sizeof(source_path))) {
|
||||
return true;
|
||||
}
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state)) {
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
if (!state.has_our_nullfs) {
|
||||
return true;
|
||||
}
|
||||
if (!unmount_title_runtime_layers(title_id))
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unmount_all_title_runtime_layers(void) {
|
||||
bool all_released = true;
|
||||
if (!for_each_title_app_dir(" during runtime mount cleanup", false,
|
||||
unmount_managed_title_entry, &all_released)) {
|
||||
all_released = false;
|
||||
}
|
||||
return all_released;
|
||||
}
|
||||
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out) {
|
||||
@@ -541,6 +741,88 @@ bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
return backport_present;
|
||||
}
|
||||
|
||||
static void log_backport_overlay_path(const char *label, const char *path,
|
||||
const struct stat *known_stat,
|
||||
const struct statfs *known_statfs) {
|
||||
struct stat path_stat;
|
||||
const struct stat *st = known_stat;
|
||||
if (!st) {
|
||||
if (stat(path, &path_stat) != 0) {
|
||||
int stat_errno = errno;
|
||||
log_debug(" [IMG][UNIONFS] %s stat failed: path=%s error=%d (%s)",
|
||||
label, path, stat_errno, strerror(stat_errno));
|
||||
} else {
|
||||
st = &path_stat;
|
||||
}
|
||||
}
|
||||
if (st) {
|
||||
log_debug(" [IMG][UNIONFS] %s stat: path=%s mode=0%o uid=%lu gid=%lu "
|
||||
"dev=0x%llX ino=0x%llX",
|
||||
label, path, (unsigned)st->st_mode,
|
||||
(unsigned long)st->st_uid, (unsigned long)st->st_gid,
|
||||
(unsigned long long)st->st_dev,
|
||||
(unsigned long long)st->st_ino);
|
||||
}
|
||||
|
||||
struct statfs path_statfs;
|
||||
const struct statfs *sfs = known_statfs;
|
||||
if (!sfs) {
|
||||
if (statfs(path, &path_statfs) != 0) {
|
||||
int statfs_errno = errno;
|
||||
log_debug(" [IMG][UNIONFS] %s statfs failed: path=%s error=%d (%s)",
|
||||
label, path, statfs_errno, strerror(statfs_errno));
|
||||
} else {
|
||||
sfs = &path_statfs;
|
||||
}
|
||||
}
|
||||
if (sfs) {
|
||||
log_debug(" [IMG][UNIONFS] %s fs: type=%s from=%s on=%s "
|
||||
"flags=0x%lX bsize=%lu iosize=%ld",
|
||||
label, sfs->f_fstypename, sfs->f_mntfromname,
|
||||
sfs->f_mntonname, (unsigned long)sfs->f_flags,
|
||||
(unsigned long)sfs->f_bsize, (long)sfs->f_iosize);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_backport_overlay_failure(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const struct stat *backport_stat,
|
||||
const struct statfs *mounted_sfs,
|
||||
int overlay_flags,
|
||||
int overlay_errno) {
|
||||
log_debug(" [IMG][UNIONFS] nmount failed: error=%d (%s) "
|
||||
"mount_flags=0x%X read_only=%d euid=%lu egid=%lu "
|
||||
"options=copymode:transparent,noatime,fnodup",
|
||||
overlay_errno, strerror(overlay_errno), overlay_flags,
|
||||
(overlay_flags & MNT_RDONLY) != 0 ? 1 : 0,
|
||||
(unsigned long)geteuid(), (unsigned long)getegid());
|
||||
log_backport_overlay_path("upper", backport_path, backport_stat, NULL);
|
||||
log_backport_overlay_path("covered", mount_point, NULL, mounted_sfs);
|
||||
|
||||
struct statfs *mounts = NULL;
|
||||
int mount_count = getmntinfo(&mounts, MNT_NOWAIT);
|
||||
if (mount_count <= 0 || !mounts) {
|
||||
int table_errno = errno;
|
||||
log_debug(" [IMG][UNIONFS] mount table unavailable: error=%d (%s)",
|
||||
table_errno, strerror(table_errno));
|
||||
return;
|
||||
}
|
||||
int exact_count = 0;
|
||||
for (int i = 0; i < mount_count; ++i) {
|
||||
if (strcmp(mounts[i].f_mntonname, mount_point) != 0)
|
||||
continue;
|
||||
exact_count++;
|
||||
log_debug(" [IMG][UNIONFS] covered layer[%d]: type=%s from=%s "
|
||||
"flags=0x%lX",
|
||||
exact_count - 1, mounts[i].f_fstypename,
|
||||
mounts[i].f_mntfromname, (unsigned long)mounts[i].f_flags);
|
||||
}
|
||||
if (exact_count == 0) {
|
||||
log_debug(" [IMG][UNIONFS] no exact mount-table entry for %s",
|
||||
mount_point);
|
||||
}
|
||||
}
|
||||
|
||||
bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const char *title_id) {
|
||||
@@ -576,17 +858,21 @@ bool mount_backport_overlay(const char *mount_point,
|
||||
int overlay_err = errno;
|
||||
log_debug(" [IMG] backport overlay failed: %s -> %s (%s)", backport_path,
|
||||
mount_point, strerror(overlay_err));
|
||||
notify_system("Backport overlay failed: %s\n%s\n0x%08X", title_id,
|
||||
backport_path, (uint32_t)overlay_err);
|
||||
log_backport_overlay_failure(mount_point, backport_path, &backport_st,
|
||||
&mounted_sfs, overlay_flags, overlay_err);
|
||||
notify_system_l10n(SM_L10N_BACKPORT_OVERLAY_FAILED, title_id, backport_path,
|
||||
(uint32_t)overlay_err);
|
||||
errno = overlay_err;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
static bool unmount_controlled_mount_stack_impl(const char *path,
|
||||
bool diagnose_busy) {
|
||||
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) {
|
||||
struct statfs mount_st;
|
||||
if (!get_top_mount(path, &mount_st))
|
||||
return true;
|
||||
if (!unmount_top_controlled_layer(path))
|
||||
if (!unmount_top_controlled_layer_impl(path, diagnose_busy))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -594,27 +880,14 @@ static bool unmount_controlled_mount_stack(const char *path) {
|
||||
return !get_top_mount(path, &mount_st);
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
return unmount_controlled_mount_stack_impl(path, true);
|
||||
}
|
||||
|
||||
static void unmount_mount_point_for_recovery(const char *path) {
|
||||
if (!unmount_controlled_mount_stack(path)) {
|
||||
log_debug(" [LINK] failed to reset mount stack for recovery: %s", path);
|
||||
}
|
||||
|
||||
if (unmount(path, 0) == 0) {
|
||||
log_debug(" [LINK] extra unmount for recovery: %s", path);
|
||||
return;
|
||||
}
|
||||
if (errno == ENOENT || errno == EINVAL)
|
||||
return;
|
||||
|
||||
if (unmount(path, MNT_FORCE) == 0) {
|
||||
log_debug(" [LINK] extra forced unmount for recovery: %s", path);
|
||||
return;
|
||||
}
|
||||
if (errno == ENOENT || errno == EINVAL)
|
||||
return;
|
||||
|
||||
log_debug(" [LINK] extra unmount for recovery did not complete for %s: %s",
|
||||
path, strerror(errno));
|
||||
}
|
||||
|
||||
static bool recover_mount_directory_after_stat_failure(
|
||||
@@ -686,7 +959,26 @@ void cleanup_duplicate_title_mounts(void) {
|
||||
}
|
||||
|
||||
// --- Copy Helpers for Install Action ---
|
||||
static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
static FILE *open_copy_destination(const char *path, bool set_mode,
|
||||
mode_t mode) {
|
||||
if (!set_mode)
|
||||
return fopen(path, "wb");
|
||||
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
FILE *file = fdopen(fd, "wb");
|
||||
if (!file) {
|
||||
int saved_errno = errno;
|
||||
(void)close(fd);
|
||||
(void)unlink(path);
|
||||
errno = saved_errno;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static int copy_param_json_rewrite(const char *src, const char *dst,
|
||||
bool set_mode, mode_t mode) {
|
||||
FILE *fs = fopen(src, "rb");
|
||||
if (!fs)
|
||||
return -1;
|
||||
@@ -729,15 +1021,13 @@ static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
len -= 2u;
|
||||
}
|
||||
|
||||
FILE *fd = fopen(dst, "wb");
|
||||
FILE *fd = open_copy_destination(dst, set_mode, mode);
|
||||
if (!fd) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = 0;
|
||||
if (len > 0 && fwrite(buf, 1, len, fd) != len)
|
||||
ret = -1;
|
||||
int ret = len > 0 && fwrite(buf, 1, len, fd) != len ? -1 : 0;
|
||||
if (fclose(fd) != 0)
|
||||
ret = -1;
|
||||
|
||||
@@ -750,28 +1040,31 @@ static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
int copy_file(const char *src, const char *dst) {
|
||||
static int copy_file_buffered(const char *src, const char *dst, bool set_mode,
|
||||
mode_t mode, char *buffer,
|
||||
size_t buffer_size) {
|
||||
if (strstr(src, "/sce_sys/param.json")) {
|
||||
return copy_param_json_rewrite(src, dst);
|
||||
return copy_param_json_rewrite(src, dst, set_mode, mode);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
FILE *fs = fopen(src, "rb");
|
||||
if (!fs)
|
||||
return -1;
|
||||
FILE *fd = fopen(dst, "wb");
|
||||
FILE *fd = open_copy_destination(dst, set_mode, mode);
|
||||
if (!fd) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
int ret = 0;
|
||||
while (true) {
|
||||
size_t n = fread(buf, 1, sizeof(buf), fs);
|
||||
if (n > 0 && fwrite(buf, 1, n, fd) != n) {
|
||||
if (ret != 0)
|
||||
break;
|
||||
size_t n = fread(buffer, 1, buffer_size, fs);
|
||||
if (n > 0 && fwrite(buffer, 1, n, fd) != n) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (n < sizeof(buf)) {
|
||||
if (n < buffer_size) {
|
||||
if (ferror(fs))
|
||||
ret = -1;
|
||||
break;
|
||||
@@ -788,9 +1081,61 @@ int copy_file(const char *src, const char *dst) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
int copy_dir(const char *src, const char *dst) {
|
||||
if (mkdir(dst, 0777) != 0 && errno != EEXIST)
|
||||
static int copy_file_impl(const char *src, const char *dst, bool set_mode,
|
||||
mode_t mode) {
|
||||
const size_t buffer_size = 64u * 1024u;
|
||||
char *buffer = malloc(buffer_size);
|
||||
if (!buffer)
|
||||
return -1;
|
||||
int ret = copy_file_buffered(src, dst, set_mode, mode, buffer, buffer_size);
|
||||
free(buffer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int remove_copy_path(const char *path) {
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0)
|
||||
return errno == ENOENT ? 0 : -1;
|
||||
if (!S_ISDIR(st.st_mode))
|
||||
return unlink(path);
|
||||
|
||||
DIR *d = opendir(path);
|
||||
if (!d)
|
||||
return -1;
|
||||
int ret = 0;
|
||||
struct dirent *entry;
|
||||
while (ret == 0 && (entry = readdir(d)) != NULL) {
|
||||
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
|
||||
continue;
|
||||
char child[MAX_PATH];
|
||||
int written = snprintf(child, sizeof(child), "%s/%s", path,
|
||||
entry->d_name);
|
||||
if (written < 0 || (size_t)written >= sizeof(child) ||
|
||||
remove_copy_path(child) != 0) {
|
||||
ret = -1;
|
||||
}
|
||||
}
|
||||
if (closedir(d) != 0)
|
||||
ret = -1;
|
||||
if (ret == 0 && rmdir(path) != 0)
|
||||
ret = -1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int copy_dir_impl(const char *src, const char *dst, bool set_mode,
|
||||
mode_t mode, bool overlay, char *buffer,
|
||||
size_t buffer_size) {
|
||||
if (mkdir(dst, set_mode ? mode : 0777) != 0) {
|
||||
if (errno != EEXIST)
|
||||
return -1;
|
||||
struct stat dst_st;
|
||||
if (lstat(dst, &dst_st) != 0)
|
||||
return -1;
|
||||
if (!S_ISDIR(dst_st.st_mode)) {
|
||||
errno = ENOTDIR;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
DIR *d = opendir(src);
|
||||
if (!d)
|
||||
return -1;
|
||||
@@ -826,13 +1171,34 @@ int copy_dir(const char *src, const char *dst) {
|
||||
} else {
|
||||
st = lst;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
|
||||
errno = EINVAL;
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (overlay) {
|
||||
struct stat dst_st;
|
||||
if (lstat(dd, &dst_st) == 0) {
|
||||
if (!S_ISDIR(st.st_mode) || !S_ISDIR(dst_st.st_mode)) {
|
||||
if (remove_copy_path(dd) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (errno != ENOENT) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (copy_dir(ss, dd) != 0) {
|
||||
if (copy_dir_impl(ss, dd, set_mode, mode, overlay, buffer,
|
||||
buffer_size) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (copy_file(ss, dd) != 0) {
|
||||
if (copy_file_buffered(ss, dd, set_mode, mode, buffer, buffer_size) !=
|
||||
0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
@@ -843,6 +1209,34 @@ int copy_dir(const char *src, const char *dst) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int copy_dir_with_options(const char *src, const char *dst,
|
||||
bool set_mode, mode_t mode, bool overlay) {
|
||||
const size_t buffer_size = 64u * 1024u;
|
||||
char *buffer = malloc(buffer_size);
|
||||
if (!buffer)
|
||||
return -1;
|
||||
int ret = copy_dir_impl(src, dst, set_mode, mode, overlay, buffer,
|
||||
buffer_size);
|
||||
free(buffer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int copy_file(const char *src, const char *dst) {
|
||||
return copy_file_impl(src, dst, false, 0);
|
||||
}
|
||||
|
||||
int copy_file_with_mode(const char *src, const char *dst, mode_t mode) {
|
||||
return copy_file_impl(src, dst, true, mode);
|
||||
}
|
||||
|
||||
int copy_dir(const char *src, const char *dst) {
|
||||
return copy_dir_with_options(src, dst, false, 0, false);
|
||||
}
|
||||
|
||||
int copy_dir_with_mode(const char *src, const char *dst, mode_t mode) {
|
||||
return copy_dir_with_options(src, dst, true, mode, true);
|
||||
}
|
||||
|
||||
int remount_system_ex(void) {
|
||||
struct iovec iov[] = {
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY("/dev/ssd0.system_ex"),
|
||||
@@ -880,8 +1274,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
return false;
|
||||
}
|
||||
if (!path_exists(src_eboot)) {
|
||||
bool tried_image_recovery = false;
|
||||
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
|
||||
if (source_path_needs_cleanup(src_path)) {
|
||||
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
|
||||
src_eboot);
|
||||
return false;
|
||||
@@ -945,20 +1338,16 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL)
|
||||
(void)unmount(dst, MNT_FORCE);
|
||||
if (!unmount_top_controlled_layer(dst))
|
||||
log_debug(" [LINK] nullfs rollback deferred during sleep: %s", dst);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path_exists(dst_eboot)) {
|
||||
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
|
||||
dst_eboot);
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
if (unmount(dst, MNT_FORCE) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
log_debug(" [LINK] failed to rollback empty nullfs mount %s: %s", dst,
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
if (!unmount_top_controlled_layer(dst))
|
||||
log_debug(" [LINK] empty nullfs rollback deferred: %s", dst);
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst);
|
||||
@@ -968,9 +1357,9 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
typedef struct {
|
||||
const char *removed_source_root;
|
||||
bool unmount_system_ex_bind;
|
||||
bool tried_image_recovery;
|
||||
bool force_remove_matching_source;
|
||||
bool match_usb_sources;
|
||||
bool preserve_mount_links;
|
||||
} cleanup_mount_links_ctx_t;
|
||||
|
||||
static bool cleanup_mount_links_entry(const char *title_id,
|
||||
@@ -1014,12 +1403,10 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
return true;
|
||||
should_remove = ctx->force_remove_matching_source ||
|
||||
(has_image_source && !path_exists(image_source_path)) ||
|
||||
source_path_needs_cleanup(source_path,
|
||||
&ctx->tried_image_recovery);
|
||||
source_path_needs_cleanup(source_path);
|
||||
} else {
|
||||
should_remove = (has_image_source && !path_exists(image_source_path)) ||
|
||||
source_path_needs_cleanup(source_path,
|
||||
&ctx->tried_image_recovery);
|
||||
source_path_needs_cleanup(source_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,17 +1429,19 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
stack_matches_link = state.has_nullfs_from_root;
|
||||
}
|
||||
|
||||
if (inspected && stack_matches_link && !top_is_ours) {
|
||||
if (stack_matches_link && !top_is_ours) {
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] keeping mount link for %s: stack still active but "
|
||||
"top layer is not ours", state.system_ex_path);
|
||||
} else if (inspected && top_is_ours && stack_matches_link) {
|
||||
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
|
||||
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
return true;
|
||||
} else if (top_is_ours && stack_matches_link) {
|
||||
if (!ctx->preserve_mount_links) {
|
||||
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
|
||||
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
return true;
|
||||
}
|
||||
mount_link_staged = true;
|
||||
}
|
||||
mount_link_staged = true;
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] failed to unmount mount stack for %s",
|
||||
@@ -1066,6 +1455,9 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx->preserve_mount_links)
|
||||
keep_mount_link = true;
|
||||
|
||||
if (!keep_mount_link) {
|
||||
int unlink_res = mount_link_staged ? unlink(paths->staged_mount_link)
|
||||
: unlink(paths->mount_link);
|
||||
@@ -1084,8 +1476,7 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
|
||||
if (mount_link_staged && rename(paths->staged_mount_link, paths->mount_link) == 0) {
|
||||
log_debug(" [LINK] restored mount link after failed cleanup: %s",
|
||||
paths->mount_link);
|
||||
log_debug(" [LINK] restored staged mount link: %s", paths->mount_link);
|
||||
} else if (mount_link_staged) {
|
||||
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
@@ -1098,9 +1489,9 @@ void cleanup_mount_links(const char *removed_source_root,
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = removed_source_root,
|
||||
.unmount_system_ex_bind = unmount_system_ex_bind,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = false,
|
||||
.match_usb_sources = false,
|
||||
.preserve_mount_links = false,
|
||||
};
|
||||
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
|
||||
&ctx);
|
||||
@@ -1113,9 +1504,9 @@ void cleanup_mount_links_for_source_unmount(const char *source_root) {
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = source_root,
|
||||
.unmount_system_ex_bind = true,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = true,
|
||||
.match_usb_sources = false,
|
||||
.preserve_mount_links = false,
|
||||
};
|
||||
for_each_title_app_dir(" during source unmount cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
@@ -1125,9 +1516,9 @@ void cleanup_usb_mount_links_for_suspend(void) {
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = NULL,
|
||||
.unmount_system_ex_bind = true,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = false,
|
||||
.match_usb_sources = true,
|
||||
.preserve_mount_links = true,
|
||||
};
|
||||
for_each_title_app_dir(" during USB suspend cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
@@ -1136,7 +1527,7 @@ void cleanup_usb_mount_links_for_suspend(void) {
|
||||
static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
(void)ctx;
|
||||
bool *all_released = (bool *)ctx;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
|
||||
@@ -1144,20 +1535,32 @@ static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
}
|
||||
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state) ||
|
||||
!state.has_our_nullfs ||
|
||||
!title_stack_top_is_managed(&state)) {
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state)) {
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
if (!state.has_our_nullfs)
|
||||
return true;
|
||||
if (!title_stack_top_is_managed(&state)) {
|
||||
log_debug(" [LINK] shutdown stack blocked by unmanaged top for %s",
|
||||
state.system_ex_path);
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
log_debug(" [LINK] failed to unmount shutdown stack for %s",
|
||||
state.system_ex_path);
|
||||
*all_released = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown_title_mounts(void) {
|
||||
for_each_title_app_dir(" during shutdown", false,
|
||||
shutdown_title_mounts_entry, NULL);
|
||||
bool shutdown_title_mounts(void) {
|
||||
bool all_released = true;
|
||||
if (!for_each_title_app_dir(" during shutdown", false,
|
||||
shutdown_title_mounts_entry, &all_released)) {
|
||||
all_released = false;
|
||||
}
|
||||
return all_released;
|
||||
}
|
||||
+317
-23
@@ -1,11 +1,16 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
struct GameCache {
|
||||
@@ -13,10 +18,34 @@ struct GameCache {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char owning_scan_root[MAX_PATH];
|
||||
uint64_t missing_since_us;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct GameCache g_game_cache[MAX_PENDING];
|
||||
static pthread_mutex_t g_game_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static bool game_cache_source_exists(const struct GameCache *entry) {
|
||||
return entry->path[0] != '\0' && path_exists(entry->path);
|
||||
}
|
||||
|
||||
static bool title_is_protected_by_dlc(const char *title_id,
|
||||
bool remove_games_with_dlc) {
|
||||
if (remove_games_with_dlc)
|
||||
return false;
|
||||
|
||||
char addcont_path[MAX_PATH];
|
||||
int written = snprintf(addcont_path, sizeof(addcont_path),
|
||||
"/user/addcont/%s", title_id);
|
||||
if (written <= 0 || (size_t)written >= sizeof(addcont_path))
|
||||
return true;
|
||||
|
||||
struct stat st;
|
||||
if (stat(addcont_path, &st) == 0)
|
||||
return S_ISDIR(st.st_mode);
|
||||
|
||||
return errno != ENOENT && errno != ENOTDIR;
|
||||
}
|
||||
|
||||
static bool resolve_game_cache_owning_scan_root(const char *path,
|
||||
char owning_scan_root[MAX_PATH]) {
|
||||
@@ -56,6 +85,7 @@ static void write_game_cache_slot(struct GameCache *entry, const char *path,
|
||||
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
|
||||
(void)strlcpy(entry->owning_scan_root, owning_scan_root,
|
||||
sizeof(entry->owning_scan_root));
|
||||
entry->missing_since_us = 0;
|
||||
entry->valid = true;
|
||||
}
|
||||
|
||||
@@ -66,6 +96,27 @@ static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
|
||||
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
|
||||
}
|
||||
|
||||
static void start_missing_timer(struct GameCache *entry, uint64_t now_us) {
|
||||
if (entry->missing_since_us != 0 || now_us == 0)
|
||||
return;
|
||||
|
||||
entry->missing_since_us = now_us;
|
||||
if (entry->path[0] != '\0') {
|
||||
log_debug(" [CACHE] source missing, auto-remove timer started: %s (%s)",
|
||||
entry->title_id, entry->path);
|
||||
} else {
|
||||
log_debug(" [CACHE] source missing, auto-remove timer started: %s",
|
||||
entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static bool missing_timer_expired(const struct GameCache *entry,
|
||||
uint64_t now_us, uint64_t delay_us) {
|
||||
return entry->valid && entry->missing_since_us != 0 &&
|
||||
now_us >= entry->missing_since_us &&
|
||||
now_us - entry->missing_since_us >= delay_us;
|
||||
}
|
||||
|
||||
static void clear_game_cache_slot(int index, const char *reason) {
|
||||
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
|
||||
return;
|
||||
@@ -89,6 +140,7 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
@@ -98,6 +150,7 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
}
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,40 +158,218 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
if (!g_game_cache[k].valid) {
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
void prune_game_cache(void) {
|
||||
static void prune_game_cache_entries(const char *root) {
|
||||
const bool auto_remove = runtime_config()->auto_remove_missing_games;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
if (root) {
|
||||
const char *entry_root =
|
||||
ensure_game_cache_owning_scan_root(&g_game_cache[k])
|
||||
? g_game_cache[k].owning_scan_root
|
||||
: g_game_cache[k].path;
|
||||
if (!path_matches_root_or_child(entry_root, root))
|
||||
continue;
|
||||
}
|
||||
|
||||
struct GameCache *entry = &g_game_cache[k];
|
||||
if (game_cache_source_exists(entry)) {
|
||||
if (entry->missing_since_us != 0) {
|
||||
entry->missing_since_us = 0;
|
||||
log_debug(" [CACHE] source restored, auto-remove cancelled: %s (%s)",
|
||||
entry->title_id, entry->path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (auto_remove)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
void prune_game_cache(void) { prune_game_cache_entries(NULL); }
|
||||
|
||||
void prune_game_cache_for_root(const char *root) {
|
||||
if (!root || root[0] == '\0') {
|
||||
prune_game_cache();
|
||||
return;
|
||||
}
|
||||
|
||||
prune_game_cache_entries(root);
|
||||
}
|
||||
|
||||
void note_game_cache_source_seen(const char *path, const char *title_id,
|
||||
const char *title_name) {
|
||||
if (!path || !title_id || title_id[0] == '\0')
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us == 0)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].title_id, title_id) != 0)
|
||||
continue;
|
||||
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id,
|
||||
title_name ? title_name : "", owning_scan_root);
|
||||
log_debug(" [CACHE] source restored, auto-remove cancelled: %s (%s)",
|
||||
title_id, path);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
static void reconcile_missing_game_cache(
|
||||
const struct AppDbTitleList *auto_remove_titles, uint64_t now_us,
|
||||
bool remove_games_with_dlc) {
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
struct GameCache *entry = &g_game_cache[k];
|
||||
if (!entry->valid || game_cache_source_exists(entry) ||
|
||||
app_db_title_list_contains(auto_remove_titles, entry->title_id)) {
|
||||
continue;
|
||||
}
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
|
||||
bool cache_full_logged = false;
|
||||
for (int i = 0; i < auto_remove_titles->count; ++i) {
|
||||
const char *title_id = auto_remove_titles->ids[i];
|
||||
bool protected_by_dlc =
|
||||
title_is_protected_by_dlc(title_id, remove_games_with_dlc);
|
||||
bool image_source_exists =
|
||||
!protected_by_dlc && sm_image_index_has_source_for_title(title_id);
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
int entry_index = -1;
|
||||
int free_index = -1;
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
if (!g_game_cache[k].valid) {
|
||||
if (free_index < 0)
|
||||
free_index = k;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(g_game_cache[k].title_id, title_id) == 0) {
|
||||
entry_index = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (protected_by_dlc) {
|
||||
if (entry_index >= 0) {
|
||||
struct GameCache *entry = &g_game_cache[entry_index];
|
||||
if (game_cache_source_exists(entry))
|
||||
entry->missing_since_us = 0;
|
||||
else
|
||||
clear_game_cache_slot(entry_index, NULL);
|
||||
}
|
||||
} else if (entry_index >= 0) {
|
||||
struct GameCache *entry = &g_game_cache[entry_index];
|
||||
if (game_cache_source_exists(entry) || image_source_exists) {
|
||||
if (entry->missing_since_us != 0) {
|
||||
log_debug(" [CACHE] source restored, auto-remove cancelled: %s",
|
||||
title_id);
|
||||
}
|
||||
if (entry->path[0] == '\0')
|
||||
clear_game_cache_slot(entry_index, NULL);
|
||||
else
|
||||
entry->missing_since_us = 0;
|
||||
} else {
|
||||
start_missing_timer(entry, now_us);
|
||||
}
|
||||
} else if (!image_source_exists && free_index >= 0) {
|
||||
struct GameCache *entry = &g_game_cache[free_index];
|
||||
write_game_cache_slot(entry, "", title_id, "", "");
|
||||
start_missing_timer(entry, now_us);
|
||||
} else if (!image_source_exists && !cache_full_logged) {
|
||||
log_debug(" [CACHE] auto-remove tracking full; remaining titles deferred");
|
||||
cache_full_logged = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void reconcile_missing_app_db_games(void) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->auto_remove_missing_games)
|
||||
return;
|
||||
|
||||
struct AppDbTitleList auto_remove_titles = {0};
|
||||
if (!get_app_db_auto_remove_title_list(&auto_remove_titles)) {
|
||||
log_debug(" [REG] app.db auto-remove title list unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t now_us = monotonic_time_us();
|
||||
const uint64_t delay_us =
|
||||
(uint64_t)cfg->auto_remove_missing_delay_seconds * 1000000ull;
|
||||
reconcile_missing_game_cache(&auto_remove_titles, now_us,
|
||||
cfg->auto_remove_games_with_dlc);
|
||||
|
||||
for (;;) {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
title_id[0] = '\0';
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
const struct GameCache *entry = &g_game_cache[k];
|
||||
if (!missing_timer_expired(entry, now_us, delay_us))
|
||||
continue;
|
||||
|
||||
(void)strlcpy(title_id, entry->title_id, sizeof(title_id));
|
||||
clear_game_cache_slot(k, NULL);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
|
||||
if (title_id[0] == '\0')
|
||||
break;
|
||||
|
||||
if (!app_db_title_list_contains(&auto_remove_titles, title_id))
|
||||
continue;
|
||||
if (title_is_protected_by_dlc(title_id,
|
||||
cfg->auto_remove_games_with_dlc))
|
||||
continue;
|
||||
|
||||
int res = sceAppInstUtilAppUnInstall(title_id);
|
||||
if (res == 0) {
|
||||
sm_install_forget_pending_title(title_id);
|
||||
invalidate_app_db_title_cache();
|
||||
log_debug(" [REG] auto-remove requested: %s", title_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [REG] auto-remove failed (not retried): %s code=0x%08X",
|
||||
title_id, (uint32_t)res);
|
||||
}
|
||||
|
||||
free_app_db_title_list(&auto_remove_titles);
|
||||
}
|
||||
|
||||
void reset_missing_game_cache_timers(void) {
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
const char *entry_root =
|
||||
ensure_game_cache_owning_scan_root(&g_game_cache[k])
|
||||
? g_game_cache[k].owning_scan_root
|
||||
: g_game_cache[k].path;
|
||||
if (!path_matches_root_or_child(entry_root, root))
|
||||
continue;
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
if (g_game_cache[k].path[0] == '\0')
|
||||
clear_game_cache_slot(k, NULL);
|
||||
else
|
||||
g_game_cache[k].missing_since_us = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
|
||||
@@ -147,52 +378,115 @@ void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
|
||||
return;
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
bool has_owning_scan_root = ensure_game_cache_owning_scan_root(&g_game_cache[k]);
|
||||
const char *entry_root =
|
||||
has_owning_scan_root ? g_game_cache[k].owning_scan_root : g_game_cache[k].path;
|
||||
if (root && root[0] != '\0' && strcmp(entry_root, root) != 0) {
|
||||
continue;
|
||||
struct GameCache entry;
|
||||
bool valid = false;
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
if (g_game_cache[k].valid && g_game_cache[k].missing_since_us == 0) {
|
||||
bool has_owning_scan_root =
|
||||
ensure_game_cache_owning_scan_root(&g_game_cache[k]);
|
||||
const char *entry_root = has_owning_scan_root
|
||||
? g_game_cache[k].owning_scan_root
|
||||
: g_game_cache[k].path;
|
||||
if (!root || root[0] == '\0' || strcmp(entry_root, root) == 0) {
|
||||
(void)strlcpy(entry.path, g_game_cache[k].path, sizeof(entry.path));
|
||||
(void)strlcpy(entry.title_id, g_game_cache[k].title_id,
|
||||
sizeof(entry.title_id));
|
||||
(void)strlcpy(entry.title_name, g_game_cache[k].title_name,
|
||||
sizeof(entry.title_name));
|
||||
(void)strlcpy(entry.owning_scan_root,
|
||||
has_owning_scan_root ? g_game_cache[k].owning_scan_root
|
||||
: "",
|
||||
sizeof(entry.owning_scan_root));
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
if (!fn(g_game_cache[k].path, g_game_cache[k].title_id,
|
||||
g_game_cache[k].title_name,
|
||||
has_owning_scan_root ? g_game_cache[k].owning_scan_root : NULL, ctx)) {
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
if (!valid)
|
||||
continue;
|
||||
if (!fn(entry.path, entry.title_id, entry.title_name,
|
||||
entry.owning_scan_root[0] != '\0' ? entry.owning_scan_root : NULL,
|
||||
ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool sm_game_cache_snapshot(sm_game_cache_snapshot_entry_t **entries_out,
|
||||
size_t *count_out) {
|
||||
if (!entries_out || !count_out)
|
||||
return false;
|
||||
*entries_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
size_t count = 0;
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
if (g_game_cache[k].valid && g_game_cache[k].missing_since_us == 0)
|
||||
count++;
|
||||
}
|
||||
|
||||
sm_game_cache_snapshot_entry_t *entries = NULL;
|
||||
if (count > 0) {
|
||||
entries = calloc(count, sizeof(*entries));
|
||||
if (!entries) {
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t copied = 0;
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us != 0)
|
||||
continue;
|
||||
sm_game_cache_snapshot_entry_t *entry = &entries[copied++];
|
||||
(void)strlcpy(entry->path, g_game_cache[k].path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->title_id, g_game_cache[k].title_id,
|
||||
sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->title_name, g_game_cache[k].title_name,
|
||||
sizeof(entry->title_name));
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
*entries_out = entries;
|
||||
*count_out = copied;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = NULL;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path && strcmp(g_game_cache[k].path, path) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
if (title_id && title_id[0] != '\0' &&
|
||||
strcmp(g_game_cache[k].title_id, title_id) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_cached_game(const char *path) {
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us != 0)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].path, path) != 0)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "removed from duplicate tracking");
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
+596
-82
@@ -1,23 +1,29 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fan.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
#define GAME_SANDBOX_ROOT "/mnt/sandbox"
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
@@ -26,59 +32,126 @@ typedef struct {
|
||||
uint64_t deadline_us;
|
||||
} pending_game_launch_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
pid_t pid;
|
||||
uint32_t app_id;
|
||||
uint64_t exit_time_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} pending_game_exit_t;
|
||||
|
||||
// SysCore can emit multiple child exec events before the actual game title_id
|
||||
// becomes visible, so keep a small raw exec candidate buffer here.
|
||||
static pending_game_launch_t
|
||||
g_pending_game_launches[MAX_PENDING_GAME_EXEC_CANDIDATES];
|
||||
static pthread_t g_game_lifecycle_thread;
|
||||
static bool g_game_lifecycle_thread_started = false;
|
||||
static volatile sig_atomic_t g_game_lifecycle_stop_requested = 0;
|
||||
static atomic_bool g_game_lifecycle_stop_requested = false;
|
||||
static int g_game_lifecycle_wake_pipe[2] = {-1, -1};
|
||||
static pthread_mutex_t g_game_lifecycle_start_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_game_lifecycle_start_ready = false;
|
||||
static bool g_game_lifecycle_start_success = false;
|
||||
static _Atomic pid_t g_active_game_pid = 0;
|
||||
static pthread_mutex_t g_active_game_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static _Atomic bool g_game_exit_handoff_pending = false;
|
||||
static uint32_t g_active_game_app_id = 0;
|
||||
static pending_game_exit_t g_pending_game_exit;
|
||||
static _Atomic uint32_t g_pending_app_focus_id;
|
||||
static _Atomic bool g_pending_app_focus_valid;
|
||||
// Title storage is owned exclusively by the lifecycle thread. Other threads
|
||||
// consume only atomically published state through public helpers.
|
||||
static char g_active_game_title_id[MAX_TITLE_ID];
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid);
|
||||
static void handle_game_exec(int kq, pid_t pid);
|
||||
static void handle_game_exit(pid_t pid);
|
||||
static void finalize_pending_game_exit(void);
|
||||
static void maybe_finalize_pending_game_exit(void);
|
||||
static bool try_game_process_handoff(int kq, pid_t pid, const char *title_id,
|
||||
uint32_t app_id);
|
||||
|
||||
static bool published_game_active(pid_t pid, bool handoff_pending) {
|
||||
return pid > 0 || handoff_pending;
|
||||
}
|
||||
|
||||
static void publish_active_game_pid(pid_t pid) {
|
||||
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
|
||||
if (previous_pid == pid)
|
||||
return;
|
||||
sm_scanner_wake();
|
||||
bool handoff_pending = atomic_load(&g_game_exit_handoff_pending);
|
||||
if (published_game_active(previous_pid, handoff_pending) !=
|
||||
published_game_active(pid, handoff_pending)) {
|
||||
sm_scanner_wake();
|
||||
}
|
||||
}
|
||||
|
||||
static void publish_active_game(pid_t pid, const char *title_id) {
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && title_id && title_id[0] != '\0')
|
||||
static void publish_game_exit_handoff_pending(bool pending) {
|
||||
bool previous_pending =
|
||||
atomic_exchange(&g_game_exit_handoff_pending, pending);
|
||||
pid_t active_pid = atomic_load(&g_active_game_pid);
|
||||
if (published_game_active(active_pid, previous_pending) !=
|
||||
published_game_active(active_pid, pending)) {
|
||||
sm_scanner_wake();
|
||||
}
|
||||
}
|
||||
|
||||
static void publish_active_game(pid_t pid, const char *title_id,
|
||||
uint32_t app_id) {
|
||||
if (pid > 0 && title_id && title_id[0] != '\0') {
|
||||
(void)strlcpy(g_active_game_title_id, title_id,
|
||||
sizeof(g_active_game_title_id));
|
||||
else
|
||||
g_active_game_app_id = app_id;
|
||||
} else {
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
g_active_game_app_id = 0;
|
||||
}
|
||||
publish_active_game_pid(pid);
|
||||
}
|
||||
|
||||
static bool consume_active_game_title(pid_t pid,
|
||||
char title_id_out[MAX_TITLE_ID]) {
|
||||
bool matched = false;
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && atomic_load(&g_active_game_pid) == pid &&
|
||||
g_active_game_title_id[0] != '\0') {
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
matched = true;
|
||||
}
|
||||
if (matched)
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
return matched;
|
||||
static bool consume_active_game(pid_t pid, char title_id_out[MAX_TITLE_ID],
|
||||
uint32_t *app_id_out) {
|
||||
if (pid <= 0 || atomic_load(&g_active_game_pid) != pid ||
|
||||
g_active_game_title_id[0] == '\0')
|
||||
return false;
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
if (app_id_out)
|
||||
*app_id_out = g_active_game_app_id;
|
||||
g_active_game_title_id[0] = '\0';
|
||||
g_active_game_app_id = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
|
||||
static void handle_pending_app_focus(void) {
|
||||
if (!atomic_exchange(&g_pending_app_focus_valid, false))
|
||||
return;
|
||||
|
||||
uint32_t focused_app_id = atomic_load(&g_pending_app_focus_id);
|
||||
if (atomic_load(&g_active_game_pid) <= 0 || g_active_game_app_id == 0 ||
|
||||
g_active_game_title_id[0] == '\0') {
|
||||
atomic_store(&g_pending_app_focus_valid, true);
|
||||
return;
|
||||
}
|
||||
if (focused_app_id == 0 || focused_app_id != g_active_game_app_id)
|
||||
return;
|
||||
|
||||
sm_fan_apply_game_target(g_active_game_title_id);
|
||||
}
|
||||
|
||||
static bool snapshot_active_game(pid_t pid, char title_id[MAX_TITLE_ID]) {
|
||||
title_id[0] = '\0';
|
||||
if (pid <= 0 || atomic_load(&g_active_game_pid) != pid ||
|
||||
g_active_game_title_id[0] == '\0')
|
||||
return false;
|
||||
(void)strlcpy(title_id, g_active_game_title_id, MAX_TITLE_ID);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool title_is_usb_backed(const char *title_id, const char *source_path,
|
||||
bool has_mount_link) {
|
||||
char image_path[MAX_PATH];
|
||||
if (read_mount_image_link(title_id, image_path, sizeof(image_path)) &&
|
||||
is_usb_storage_path(image_path)) {
|
||||
return true;
|
||||
}
|
||||
return has_mount_link && is_usb_storage_path(source_path);
|
||||
}
|
||||
|
||||
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
|
||||
@@ -164,19 +237,116 @@ static bool is_process_alive(pid_t pid) {
|
||||
return errno != ESRCH;
|
||||
}
|
||||
|
||||
static void clear_pending_game_exit(void) {
|
||||
memset(&g_pending_game_exit, 0, sizeof(g_pending_game_exit));
|
||||
publish_game_exit_handoff_pending(false);
|
||||
}
|
||||
|
||||
static void queue_pending_game_exit(pid_t pid, const char *title_id,
|
||||
uint32_t app_id) {
|
||||
memset(&g_pending_game_exit, 0, sizeof(g_pending_game_exit));
|
||||
g_pending_game_exit.active = true;
|
||||
g_pending_game_exit.pid = pid;
|
||||
g_pending_game_exit.app_id = app_id;
|
||||
g_pending_game_exit.exit_time_us = monotonic_time_us();
|
||||
(void)strlcpy(g_pending_game_exit.title_id, title_id,
|
||||
sizeof(g_pending_game_exit.title_id));
|
||||
publish_game_exit_handoff_pending(true);
|
||||
}
|
||||
|
||||
static bool same_application_identity(const char *title_id, uint32_t app_id,
|
||||
const char *other_title_id,
|
||||
uint32_t other_app_id) {
|
||||
if (!title_id || !other_title_id || strcmp(title_id, other_title_id) != 0)
|
||||
return false;
|
||||
// app_id should normally be stable across ExitSpawn. Treat zero as unknown so
|
||||
// title identity can still recover a handoff while AppInfo is incomplete.
|
||||
return app_id == 0 || other_app_id == 0 || app_id == other_app_id;
|
||||
}
|
||||
|
||||
static bool try_game_process_handoff(int kq, pid_t pid, const char *title_id,
|
||||
uint32_t app_id) {
|
||||
if (pid <= 0 || !title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
pid_t old_pid = 0;
|
||||
uint32_t old_app_id = 0;
|
||||
bool pending_exit = false;
|
||||
if (g_pending_game_exit.active &&
|
||||
same_application_identity(title_id, app_id,
|
||||
g_pending_game_exit.title_id,
|
||||
g_pending_game_exit.app_id)) {
|
||||
old_pid = g_pending_game_exit.pid;
|
||||
old_app_id = g_pending_game_exit.app_id;
|
||||
pending_exit = true;
|
||||
} else {
|
||||
pid_t active_pid = atomic_load(&g_active_game_pid);
|
||||
if (active_pid > 0 && active_pid != pid &&
|
||||
same_application_identity(title_id, app_id, g_active_game_title_id,
|
||||
g_active_game_app_id) &&
|
||||
!is_process_alive(active_pid)) {
|
||||
// NOTE_EXEC can race ahead of the old NOTE_EXIT. A dead old process with
|
||||
// the same title/app identity is the same ExitSpawn handoff.
|
||||
old_pid = active_pid;
|
||||
old_app_id = g_active_game_app_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (old_pid <= 0 || old_pid == pid)
|
||||
return false;
|
||||
if (!register_game_exit_watch(kq, pid)) {
|
||||
log_debug(" [GAME] process handoff exit watch failed: %s pid=%ld -> %ld",
|
||||
title_id, (long)old_pid, (long)pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep the ShellCore runtime across ExitSpawn/LoadExec, but deliberately
|
||||
// restart the per-process fakelib overlay. Holding the old nullfs overlay
|
||||
// can keep the old title sandbox busy and prevent ShellCore from removing
|
||||
// it. sm_fakelib_game_on_exec() also handles the NOTE_EXEC-before-NOTE_EXIT
|
||||
// race by cleaning any mount still owned by old_pid before mounting for pid.
|
||||
uint32_t effective_app_id = app_id != 0 ? app_id : old_app_id;
|
||||
sm_shellcore_service_bind_prepared_app(title_id, effective_app_id, pid);
|
||||
sm_fakelib_game_on_exec(pid, title_id, false);
|
||||
bool kstuff_rebound =
|
||||
sm_kstuff_game_handoff(old_pid, pid, title_id, effective_app_id);
|
||||
publish_active_game(pid, title_id, effective_app_id);
|
||||
if (pending_exit)
|
||||
clear_pending_game_exit();
|
||||
|
||||
log_debug(" [GAME] process handoff: %s app_id=0x%08X pid=%ld -> pid=%ld "
|
||||
"kstuff=%s",
|
||||
title_id, effective_app_id, (long)old_pid, (long)pid,
|
||||
kstuff_rebound ? "preserved" : "restarted/inactive");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
const char *title_id, uint32_t app_id) {
|
||||
if (try_game_process_handoff(kq, pid, title_id, app_id))
|
||||
return true;
|
||||
|
||||
if (!sm_shellcore_ensure_title_runtime(title_id)) {
|
||||
return false;
|
||||
}
|
||||
sm_shellcore_service_bind_prepared_app(title_id, app_id, pid);
|
||||
|
||||
if (!register_game_exit_watch(kq, pid)) {
|
||||
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
|
||||
title_id, (long)pid);
|
||||
if (sm_shellcore_service_title_is_prepared(title_id) &&
|
||||
!sm_shellcore_release_title_runtime(title_id)) {
|
||||
log_debug(" [SHELLCORE] failed to roll back untracked runtime: %s",
|
||||
title_id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
publish_active_game(pid, title_id);
|
||||
publish_active_game(pid, title_id, app_id);
|
||||
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
|
||||
sm_fakelib_game_on_exec(pid, title_id);
|
||||
sm_fakelib_game_on_exec(pid, title_id, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -199,13 +369,33 @@ static void close_game_lifecycle_wake_pipe(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static int register_game_sandbox_watch(int kq) {
|
||||
int fd = open(GAME_SANDBOX_ROOT, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
log_debug(" [GAME] sandbox watcher open failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct kevent kev;
|
||||
EV_SET(&kev, (uintptr_t)fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR,
|
||||
NOTE_WRITE | NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE,
|
||||
0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] sandbox watcher registration failed: %s",
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void drain_game_lifecycle_wake_pipe(void) {
|
||||
char drain_buf[32];
|
||||
while (read(g_game_lifecycle_wake_pipe[0], drain_buf, sizeof(drain_buf)) > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
static bool drain_game_lifecycle_events_nowait(int kq) {
|
||||
struct kevent events[16];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
@@ -221,17 +411,18 @@ static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
for (int i = 0; i < nev; ++i) {
|
||||
if (events[i].filter == EVFILT_PROC) {
|
||||
if ((events[i].fflags & NOTE_EXEC) != 0)
|
||||
handle_game_exec(kq, (pid_t)events[i].ident);
|
||||
if ((events[i].fflags & NOTE_EXIT) != 0)
|
||||
handle_game_exit((pid_t)events[i].ident);
|
||||
}
|
||||
}
|
||||
(void)sm_shellcore_service_release_exited_titles();
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void set_game_lifecycle_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_success = success;
|
||||
@@ -296,6 +487,12 @@ static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
|
||||
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
|
||||
uint64_t now_us,
|
||||
const char *title_id) {
|
||||
if (now_us == 0) {
|
||||
log_debug(" [GAME] cannot schedule launch retry without monotonic clock: "
|
||||
"%s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
return;
|
||||
}
|
||||
clear_all_pending_game_launches();
|
||||
queue_pending_game_launch(pid, exec_time_us, now_us);
|
||||
|
||||
@@ -324,14 +521,41 @@ static uint64_t next_pending_game_wake_us(uint64_t now_us) {
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
static uint64_t next_pending_exit_wake_us(uint64_t now_us,
|
||||
bool sandbox_watch_active) {
|
||||
if (!g_pending_game_exit.active)
|
||||
return 0;
|
||||
if (now_us == 0)
|
||||
return 1;
|
||||
|
||||
uint64_t grace_deadline_us =
|
||||
g_pending_game_exit.exit_time_us == 0
|
||||
? 0
|
||||
: g_pending_game_exit.exit_time_us + GAME_PROCESS_HANDOFF_GRACE_US;
|
||||
if (sandbox_watch_active && grace_deadline_us != 0 &&
|
||||
now_us < grace_deadline_us) {
|
||||
return grace_deadline_us;
|
||||
}
|
||||
if (sandbox_watch_active)
|
||||
return now_us + GAME_PROCESS_HANDOFF_FALLBACK_POLL_US;
|
||||
return now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
}
|
||||
|
||||
static const struct timespec *compute_game_wait_timeout(
|
||||
struct timespec *timeout_out) {
|
||||
struct timespec *timeout_out, bool sandbox_watch_active) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t next_wake_us = 0;
|
||||
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, next_pending_game_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_mdbg_next_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(
|
||||
next_wake_us,
|
||||
next_pending_exit_wake_us(now_us, sandbox_watch_active));
|
||||
if (!g_pending_game_exit.active) {
|
||||
next_wake_us =
|
||||
min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
|
||||
next_wake_us =
|
||||
min_nonzero_u64(next_wake_us, sm_mdbg_next_wake_us(now_us));
|
||||
}
|
||||
if (next_wake_us == 0)
|
||||
return NULL;
|
||||
|
||||
@@ -358,13 +582,28 @@ static void poll_game_modules(int kq) {
|
||||
if (resolve_game_title_id(entry->pid, title_id, &app_id)) {
|
||||
pid_t pid = entry->pid;
|
||||
uint64_t exec_time_us = entry->exec_time_us;
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (is_supported_game_title_id(title_id)) {
|
||||
if (entry->deadline_us == 0 || now_us == 0 ||
|
||||
now_us >= entry->deadline_us) {
|
||||
log_debug(" [GAME] runtime preparation timed out for %s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
clear_pending_game_launch(entry);
|
||||
break;
|
||||
}
|
||||
if (dispatch_game_launch(kq, pid, exec_time_us, title_id, app_id)) {
|
||||
clear_pending_game_launch(entry);
|
||||
clear_all_pending_game_launches();
|
||||
break;
|
||||
}
|
||||
defer_confirmed_game_launch_retry(pid, exec_time_us, now_us, title_id);
|
||||
uint64_t retry_now_us = monotonic_time_us();
|
||||
if (retry_now_us == 0 || retry_now_us >= entry->deadline_us) {
|
||||
log_debug(" [GAME] runtime preparation timed out for %s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
clear_pending_game_launch(entry);
|
||||
} else {
|
||||
defer_confirmed_game_launch_retry(pid, exec_time_us, retry_now_us,
|
||||
title_id);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
clear_pending_game_launch(entry);
|
||||
@@ -381,7 +620,14 @@ static void poll_game_modules(int kq) {
|
||||
}
|
||||
}
|
||||
|
||||
sm_kstuff_game_poll();
|
||||
maybe_finalize_pending_game_exit();
|
||||
if (g_pending_game_exit.active) {
|
||||
sm_kstuff_game_poll(false);
|
||||
return;
|
||||
}
|
||||
|
||||
handle_pending_app_focus();
|
||||
sm_kstuff_game_poll(true);
|
||||
sm_mdbg_poll();
|
||||
}
|
||||
|
||||
@@ -405,7 +651,7 @@ static void handle_game_exec(int kq, pid_t pid) {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (resolve_game_title_id(pid, title_id, &app_id)) {
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (is_supported_game_title_id(title_id)) {
|
||||
if (dispatch_game_launch(kq, pid, now_us, title_id, app_id))
|
||||
clear_all_pending_game_launches();
|
||||
else
|
||||
@@ -417,42 +663,240 @@ static void handle_game_exec(int kq, pid_t pid) {
|
||||
if (!is_process_alive(pid))
|
||||
return;
|
||||
|
||||
queue_pending_game_launch(pid, now_us, now_us);
|
||||
if (now_us != 0)
|
||||
queue_pending_game_launch(pid, now_us, now_us);
|
||||
}
|
||||
|
||||
static void handle_game_exit(pid_t pid) {
|
||||
char title_id[MAX_TITLE_ID] = {0};
|
||||
bool had_active_title = consume_active_game_title(pid, title_id);
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
if (atomic_load(&g_active_game_pid) == pid)
|
||||
publish_active_game_pid(0);
|
||||
static void finalize_game_exit(pid_t pid, const char *fallback_title_id) {
|
||||
char owned_title_id[MAX_TITLE_ID] = {0};
|
||||
bool owned_exit =
|
||||
sm_shellcore_service_note_game_exit(pid, owned_title_id);
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
sm_kstuff_game_on_exit(pid);
|
||||
if (had_active_title) {
|
||||
int snd0_updates = normalize_snd0info_for_title(title_id);
|
||||
if (owned_exit) {
|
||||
log_debug(" [SHELLCORE] runtime release awaiting sandbox removal: %s",
|
||||
owned_title_id);
|
||||
}
|
||||
|
||||
const char *exited_title_id =
|
||||
owned_exit ? owned_title_id : fallback_title_id;
|
||||
if (exited_title_id && exited_title_id[0] != '\0') {
|
||||
int snd0_updates = normalize_snd0info_for_title(exited_title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
|
||||
snd0_updates, title_id);
|
||||
snd0_updates, exited_title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void restore_suspended_game_if_alive(int kq, pid_t pid) {
|
||||
if (pid <= 0 || !is_process_alive(pid))
|
||||
static void finalize_pending_game_exit(void) {
|
||||
if (!g_pending_game_exit.active)
|
||||
return;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (!resolve_game_title_id(pid, title_id, &app_id))
|
||||
pending_game_exit_t exited = g_pending_game_exit;
|
||||
memset(&g_pending_game_exit, 0, sizeof(g_pending_game_exit));
|
||||
log_debug(" [GAME] finalizing stopped application: %s pid=%ld",
|
||||
exited.title_id, (long)exited.pid);
|
||||
finalize_game_exit(exited.pid, exited.title_id);
|
||||
// Keep the public active state asserted until all per-game cleanup has
|
||||
// completed, then publish the single logical active -> inactive transition.
|
||||
publish_game_exit_handoff_pending(false);
|
||||
}
|
||||
|
||||
static void maybe_finalize_pending_game_exit(void) {
|
||||
if (!g_pending_game_exit.active)
|
||||
return;
|
||||
if (!is_supported_title_id(title_id))
|
||||
|
||||
pid_t active_pid = atomic_load(&g_active_game_pid);
|
||||
if (active_pid > 0) {
|
||||
// A different application has already taken over. This cannot be a
|
||||
// same-app process handoff, so the old process may be finalized now.
|
||||
if (!same_application_identity(g_active_game_title_id,
|
||||
g_active_game_app_id,
|
||||
g_pending_game_exit.title_id,
|
||||
g_pending_game_exit.app_id)) {
|
||||
finalize_pending_game_exit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ExitSpawn keeps the same title sandbox alive. A real application exit
|
||||
// removes it; wait for that signal instead of equating NOTE_EXIT with app
|
||||
// termination. The grace period protects against a brief remove/recreate
|
||||
// transition while the replacement process is being published.
|
||||
if (sm_shellcore_service_title_sandbox_exists(
|
||||
g_pending_game_exit.title_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
log_debug(" [GAME] resumed: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
(void)dispatch_game_launch(kq, pid, now_us, title_id, app_id);
|
||||
if (g_pending_game_exit.exit_time_us != 0 && now_us != 0 &&
|
||||
now_us - g_pending_game_exit.exit_time_us <
|
||||
GAME_PROCESS_HANDOFF_GRACE_US) {
|
||||
return;
|
||||
}
|
||||
|
||||
finalize_pending_game_exit();
|
||||
}
|
||||
|
||||
static void handle_game_exit(pid_t pid) {
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
|
||||
// Duplicate NOTE_EXIT after an active process was already moved into the
|
||||
// handoff window must not turn the application into exit-pending.
|
||||
if (g_pending_game_exit.active && g_pending_game_exit.pid == pid)
|
||||
return;
|
||||
|
||||
char active_title_id[MAX_TITLE_ID] = {0};
|
||||
uint32_t active_app_id = 0;
|
||||
bool had_active_title =
|
||||
consume_active_game(pid, active_title_id, &active_app_id);
|
||||
if (had_active_title) {
|
||||
if (g_pending_game_exit.active)
|
||||
finalize_pending_game_exit();
|
||||
// Publish the pending flag before clearing the active PID so scanner/API
|
||||
// threads never observe an artificial "no game" gap during the handoff.
|
||||
queue_pending_game_exit(pid, active_title_id, active_app_id);
|
||||
publish_active_game_pid(0);
|
||||
|
||||
// Match the 1.6 fakelib lifetime: release the overlay as soon as this PID
|
||||
// exits. The nullfs mount otherwise holds common/lib inside the old sandbox
|
||||
// busy and can prevent that sandbox from being removed during ExitSpawn.
|
||||
// ShellCore/KStuff ownership remains pending and may still be handed to a
|
||||
// replacement PID of the same title/app.
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
|
||||
log_debug(" [GAME] process exited, awaiting same-app handoff: %s "
|
||||
"pid=%ld app_id=0x%08X",
|
||||
active_title_id, (long)pid, active_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-active exits (for example an outgoing title during a normal game
|
||||
// switch) retain the existing immediate per-process cleanup behavior.
|
||||
finalize_game_exit(pid, NULL);
|
||||
}
|
||||
|
||||
static bool terminate_usb_game_for_sleep(int kq, pid_t pid,
|
||||
const char *title_id) {
|
||||
if (kill(pid, SIGKILL) != 0) {
|
||||
if (errno == ESRCH) {
|
||||
handle_game_exit(pid);
|
||||
if (g_pending_game_exit.active && g_pending_game_exit.pid == pid)
|
||||
finalize_pending_game_exit();
|
||||
return true;
|
||||
}
|
||||
log_debug(" [SLEEP] failed to kill USB game: %s pid=%ld: %s", title_id,
|
||||
(long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
log_debug(" [SLEEP] killing USB game before suspend: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t deadline_us =
|
||||
now_us == 0 ? 0 : now_us + GAME_SLEEP_EXIT_TIMEOUT_US;
|
||||
while (!atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed) &&
|
||||
!should_stop_requested()) {
|
||||
struct kevent event;
|
||||
struct timespec timeout = {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = (long)GAME_LIFECYCLE_POLL_INTERVAL_US * 1000L,
|
||||
};
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, &timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SLEEP] NOTE_EXIT wait failed for %s pid=%ld: %s",
|
||||
title_id, (long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if (event.filter == EVFILT_PROC &&
|
||||
(event.fflags & NOTE_EXIT) != 0) {
|
||||
pid_t exited_pid = (pid_t)event.ident;
|
||||
handle_game_exit(exited_pid);
|
||||
if (exited_pid == pid) {
|
||||
if (g_pending_game_exit.active && g_pending_game_exit.pid == pid)
|
||||
finalize_pending_game_exit();
|
||||
log_debug(" [SLEEP] USB game stopped: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now_us = monotonic_time_us();
|
||||
if (deadline_us != 0 && now_us != 0 && now_us >= deadline_us) {
|
||||
if (!is_process_alive(pid)) {
|
||||
handle_game_exit(pid);
|
||||
if (g_pending_game_exit.active && g_pending_game_exit.pid == pid)
|
||||
finalize_pending_game_exit();
|
||||
log_debug(" [SLEEP] USB game exited without NOTE_EXIT: %s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
return true;
|
||||
}
|
||||
log_debug(" [SLEEP] timed out waiting NOTE_EXIT: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool wait_for_shellcore_exit_cleanup_for_sleep(int kq,
|
||||
bool require_no_owner) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t deadline_us =
|
||||
now_us == 0 ? 0 : now_us + GAME_SLEEP_EXIT_TIMEOUT_US;
|
||||
while (!atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed) &&
|
||||
!should_stop_requested()) {
|
||||
int release_status = sm_shellcore_service_release_exited_titles();
|
||||
if (release_status == 0 &&
|
||||
(!require_no_owner || !sm_shellcore_service_has_prepared_mount())) {
|
||||
return true;
|
||||
}
|
||||
if (release_status != 0 && release_status != EAGAIN) {
|
||||
log_debug(" [SLEEP] runtime release failed: %s",
|
||||
strerror(release_status));
|
||||
return false;
|
||||
}
|
||||
|
||||
struct kevent event;
|
||||
struct timespec timeout = {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = (long)GAME_LIFECYCLE_POLL_INTERVAL_US * 1000L,
|
||||
};
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, &timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SLEEP] sandbox cleanup wait failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if (event.filter == EVFILT_PROC &&
|
||||
(event.fflags & NOTE_EXIT) != 0) {
|
||||
handle_game_exit((pid_t)event.ident);
|
||||
}
|
||||
}
|
||||
|
||||
now_us = monotonic_time_us();
|
||||
if (deadline_us != 0 && now_us != 0 && now_us >= deadline_us) {
|
||||
log_debug(" [SLEEP] timed out waiting for sandbox cleanup");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void *game_lifecycle_watcher_main(void *arg) {
|
||||
@@ -492,19 +936,60 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int sandbox_watch_fd = register_game_sandbox_watch(kq);
|
||||
|
||||
set_game_lifecycle_start_result(true);
|
||||
log_debug(" [GAME] lifecycle watcher started");
|
||||
|
||||
bool sleep_cleanup_done = false;
|
||||
pid_t suspended_game_pid = 0;
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
while (!atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed) &&
|
||||
!should_stop_requested()) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (!sleep_cleanup_done) {
|
||||
if (!sleep_cleanup_done && sm_scanner_usb_watches_suspended()) {
|
||||
// A process handoff cannot safely span suspend cleanup: finalize the
|
||||
// dead owner now. A replacement NOTE_EXEC is handled normally after
|
||||
// resume and remounts any runtime that sleep cleanup released.
|
||||
if (g_pending_game_exit.active)
|
||||
finalize_pending_game_exit();
|
||||
suspended_game_pid = atomic_load(&g_active_game_pid);
|
||||
char suspended_game_title_id[MAX_TITLE_ID];
|
||||
bool active_game = snapshot_active_game(suspended_game_pid,
|
||||
suspended_game_title_id);
|
||||
char source_path[MAX_PATH];
|
||||
bool has_mount_link =
|
||||
active_game && read_mount_link(suspended_game_title_id, source_path,
|
||||
sizeof(source_path));
|
||||
bool usb_game =
|
||||
active_game && title_is_usb_backed(
|
||||
suspended_game_title_id, source_path,
|
||||
has_mount_link);
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_sleep_enter();
|
||||
publish_active_game(0, NULL);
|
||||
bool usb_cleanup_allowed = true;
|
||||
if (usb_game) {
|
||||
usb_cleanup_allowed = terminate_usb_game_for_sleep(
|
||||
kq, suspended_game_pid, suspended_game_title_id);
|
||||
if (usb_cleanup_allowed) {
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
}
|
||||
if (usb_cleanup_allowed) {
|
||||
usb_cleanup_allowed = wait_for_shellcore_exit_cleanup_for_sleep(
|
||||
kq, usb_game);
|
||||
}
|
||||
if (usb_cleanup_allowed) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
} else {
|
||||
log_debug("[SLEEP] USB cleanup skipped: game/sandbox cleanup "
|
||||
"incomplete");
|
||||
}
|
||||
// Keep a live non-USB game published while it is suspended so scanner
|
||||
// work cannot race its unchanged runtime mount.
|
||||
sleep_cleanup_done = true;
|
||||
}
|
||||
|
||||
@@ -523,22 +1008,30 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
continue;
|
||||
}
|
||||
if (sleep_cleanup_done) {
|
||||
if (!discard_game_lifecycle_events_nowait(kq))
|
||||
if (!drain_game_lifecycle_events_nowait(kq))
|
||||
break;
|
||||
sleep_cleanup_done = false;
|
||||
restore_suspended_game_if_alive(kq, suspended_game_pid);
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
if (suspended_game_pid > 0 &&
|
||||
atomic_load(&g_active_game_pid) == suspended_game_pid &&
|
||||
is_process_alive(suspended_game_pid)) {
|
||||
handle_game_exec(kq, suspended_game_pid);
|
||||
}
|
||||
sm_kstuff_sleep_leave();
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
|
||||
struct kevent event;
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
|
||||
const struct timespec *timeout_ptr = compute_game_wait_timeout(
|
||||
&timeout, sandbox_watch_fd >= 0);
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, timeout_ptr);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (g_game_lifecycle_stop_requested)
|
||||
if (atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed))
|
||||
break;
|
||||
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
|
||||
break;
|
||||
@@ -547,9 +1040,10 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
|
||||
} else if (event.filter == EVFILT_PROC &&
|
||||
(event.fflags & NOTE_TRACKERR) != 0) {
|
||||
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
|
||||
} else {
|
||||
} else if (event.filter == EVFILT_PROC) {
|
||||
if ((event.fflags & NOTE_EXEC) != 0)
|
||||
handle_game_exec(kq, (pid_t)event.ident);
|
||||
if ((event.fflags & NOTE_EXIT) != 0)
|
||||
@@ -560,12 +1054,16 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
poll_game_modules(kq);
|
||||
(void)sm_shellcore_service_release_exited_titles();
|
||||
}
|
||||
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_game_shutdown();
|
||||
publish_active_game(0, NULL);
|
||||
publish_active_game(0, NULL, 0);
|
||||
clear_pending_game_exit();
|
||||
if (sandbox_watch_fd >= 0)
|
||||
close(sandbox_watch_fd);
|
||||
close(kq);
|
||||
log_debug(" [GAME] lifecycle watcher stopped");
|
||||
return NULL;
|
||||
@@ -578,15 +1076,19 @@ bool start_game_lifecycle_watcher(void) {
|
||||
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
if (!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
atomic_store_explicit(&g_game_lifecycle_stop_requested, false,
|
||||
memory_order_relaxed);
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
clear_pending_game_exit();
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_ready = false;
|
||||
g_game_lifecycle_start_success = false;
|
||||
@@ -632,18 +1134,30 @@ void stop_game_lifecycle_watcher(void) {
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return;
|
||||
|
||||
g_game_lifecycle_stop_requested = 1;
|
||||
atomic_store_explicit(&g_game_lifecycle_stop_requested, true,
|
||||
memory_order_relaxed);
|
||||
wake_game_lifecycle_watcher();
|
||||
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
g_game_lifecycle_thread_started = false;
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
publish_active_game(0, NULL);
|
||||
atomic_store_explicit(&g_game_lifecycle_stop_requested, false,
|
||||
memory_order_relaxed);
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
publish_active_game(0, NULL, 0);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
|
||||
bool sm_game_lifecycle_has_active_game(void) {
|
||||
return atomic_load(&g_active_game_pid) > 0;
|
||||
return atomic_load(&g_active_game_pid) > 0 ||
|
||||
atomic_load(&g_game_exit_handoff_pending);
|
||||
}
|
||||
|
||||
void sm_game_lifecycle_note_app_focus(uint32_t app_id) {
|
||||
atomic_store(&g_pending_app_focus_id, app_id);
|
||||
atomic_store(&g_pending_app_focus_valid, true);
|
||||
sm_kstuff_note_app_focus(app_id);
|
||||
wake_game_lifecycle_watcher();
|
||||
}
|
||||
|
||||
bool refresh_game_lifecycle_watcher(void) {
|
||||
|
||||
+28
-2
@@ -6,8 +6,13 @@
|
||||
// --- Game Metadata Parsing (param.json) ---
|
||||
static int extract_json_string(const char *json, const char *key, char *out,
|
||||
size_t out_size) {
|
||||
if (!json || !key || !out || out_size == 0)
|
||||
return -1;
|
||||
|
||||
char search[64];
|
||||
snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
int written = snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
if (written < 0 || (size_t)written >= sizeof(search))
|
||||
return -1;
|
||||
const char *p = strstr(json, search);
|
||||
if (!p)
|
||||
return -1;
|
||||
@@ -27,9 +32,27 @@ static int extract_json_string(const char *json, const char *key, char *out,
|
||||
i++;
|
||||
}
|
||||
out[i] = '\0';
|
||||
if (p[i] != '"') {
|
||||
out[0] = '\0';
|
||||
return -4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool is_supported_game_title_id(const char *title_id) {
|
||||
if (!title_id || strlen(title_id) != 9u)
|
||||
return false;
|
||||
if (strncmp(title_id, "PPSA", 4u) != 0 &&
|
||||
strncmp(title_id, "CUSA", 4u) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 4u; i < 9u; ++i) {
|
||||
if (!isdigit((unsigned char)title_id[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name) {
|
||||
out_id[0] = '\0';
|
||||
@@ -77,7 +100,7 @@ bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
int res = extract_json_string(buf, "titleId", out_id, MAX_TITLE_ID);
|
||||
if (res != 0)
|
||||
res = extract_json_string(buf, "title_id", out_id, MAX_TITLE_ID);
|
||||
if (res == 0) {
|
||||
if (res == 0 && is_supported_game_title_id(out_id)) {
|
||||
const char *en_ptr = strstr(buf, "\"en-US\"");
|
||||
const char *search_start = en_ptr ? en_ptr : buf;
|
||||
if (extract_json_string(search_start, "titleName", out_name,
|
||||
@@ -86,6 +109,9 @@ bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
if (out_name[0] == '\0')
|
||||
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
|
||||
valid = true;
|
||||
} else {
|
||||
out_id[0] = '\0';
|
||||
out_name[0] = '\0';
|
||||
}
|
||||
free(buf);
|
||||
|
||||
|
||||
+803
-270
File diff suppressed because it is too large.
Load diff
+519
-19
@@ -5,36 +5,216 @@
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct ImageCache {
|
||||
uint64_t generation;
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
attached_device_state_t state;
|
||||
attached_unit_detach_state_t detach_state;
|
||||
bool detach_attempt_active;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static uint64_t g_image_cache_generation;
|
||||
|
||||
enum {
|
||||
IMAGE_CACHE_INDEX_CONFLICT = -2,
|
||||
};
|
||||
|
||||
static uint64_t next_cache_generation(void) {
|
||||
++g_image_cache_generation;
|
||||
if (g_image_cache_generation == 0)
|
||||
++g_image_cache_generation;
|
||||
return g_image_cache_generation;
|
||||
}
|
||||
|
||||
static bool attach_backend_is_valid(attach_backend_t backend) {
|
||||
return backend == ATTACH_BACKEND_MD || backend == ATTACH_BACKEND_LVD;
|
||||
}
|
||||
|
||||
static bool device_identity_matches(
|
||||
const attached_unit_detach_state_t *left,
|
||||
const attached_unit_detach_state_t *right) {
|
||||
if (!left->node_identity_valid || !right->node_identity_valid)
|
||||
return false;
|
||||
return left->node_device == right->node_device &&
|
||||
left->node_inode == right->node_inode &&
|
||||
left->node_rdev == right->node_rdev;
|
||||
}
|
||||
|
||||
static bool refresh_attached_unit_identity(struct ImageCache *entry) {
|
||||
if (!attach_backend_is_valid(entry->backend) || entry->unit_id < 0)
|
||||
return false;
|
||||
if (entry->detach_state.node_identity_valid)
|
||||
return true;
|
||||
|
||||
const char *prefix =
|
||||
entry->backend == ATTACH_BACKEND_MD ? "/dev/md" : "/dev/lvd";
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "%s%d", prefix, entry->unit_id);
|
||||
|
||||
struct stat st;
|
||||
if (stat(devname, &st) != 0)
|
||||
return false;
|
||||
|
||||
entry->detach_state.node_identity_valid = true;
|
||||
entry->detach_state.node_device = (uint64_t)st.st_dev;
|
||||
entry->detach_state.node_inode = (uint64_t)st.st_ino;
|
||||
entry->detach_state.node_rdev = (uint64_t)st.st_rdev;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void reset_attached_unit_identity(struct ImageCache *entry) {
|
||||
memset(&entry->detach_state, 0, sizeof(entry->detach_state));
|
||||
(void)refresh_attached_unit_identity(entry);
|
||||
}
|
||||
|
||||
static void copy_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
entry_out->generation = g_image_cache[index].generation;
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
entry_out->state = g_image_cache[index].state;
|
||||
entry_out->detach_state = g_image_cache[index].detach_state;
|
||||
}
|
||||
|
||||
static int find_source_path_index(const char *path) {
|
||||
if (!path)
|
||||
return -1;
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (mount_point && strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
return k;
|
||||
}
|
||||
if (path && strcmp(g_image_cache[k].path, path) == 0)
|
||||
if (strcmp(g_image_cache[k].path, path) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int upsert_image_source_mapping(const char *path, const char *mount_point) {
|
||||
int entry_index = find_cache_index(path, mount_point);
|
||||
static int find_mount_point_index(const char *mount_point) {
|
||||
if (!mount_point)
|
||||
return -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_image_cache[k].mount_point, mount_point) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool cache_entry_is_metadata_only(const struct ImageCache *entry) {
|
||||
return entry->state == ATTACHED_DEVICE_RELEASED && entry->unit_id < 0 &&
|
||||
entry->backend == ATTACH_BACKEND_NONE &&
|
||||
!entry->detach_attempt_active;
|
||||
}
|
||||
|
||||
static bool find_mapping_indices(const char *path, const char *mount_point,
|
||||
int *path_index_out,
|
||||
int *mount_index_out) {
|
||||
*path_index_out = -1;
|
||||
*mount_index_out = -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; ++k) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_image_cache[k].path, path) == 0) {
|
||||
if (*path_index_out >= 0) {
|
||||
errno = EEXIST;
|
||||
return false;
|
||||
}
|
||||
*path_index_out = k;
|
||||
}
|
||||
if (strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
if (*mount_index_out >= 0) {
|
||||
errno = EEXIST;
|
||||
return false;
|
||||
}
|
||||
*mount_index_out = k;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int resolve_or_merge_mapping_index(const char *path,
|
||||
const char *mount_point,
|
||||
bool *merged_out) {
|
||||
int path_index = -1;
|
||||
int mount_index = -1;
|
||||
*merged_out = false;
|
||||
if (!find_mapping_indices(path, mount_point, &path_index, &mount_index))
|
||||
return IMAGE_CACHE_INDEX_CONFLICT;
|
||||
|
||||
if (path_index < 0 || mount_index < 0 || path_index == mount_index)
|
||||
return path_index >= 0 ? path_index : mount_index;
|
||||
|
||||
struct ImageCache *path_entry = &g_image_cache[path_index];
|
||||
struct ImageCache *mount_entry = &g_image_cache[mount_index];
|
||||
if (!cache_entry_is_metadata_only(path_entry) ||
|
||||
!cache_entry_is_metadata_only(mount_entry)) {
|
||||
errno = EBUSY;
|
||||
return IMAGE_CACHE_INDEX_CONFLICT;
|
||||
}
|
||||
|
||||
// Both IDs are metadata-only. The requested pair supersedes their stale
|
||||
// half-mappings, so merge them into the source-path owner.
|
||||
memset(mount_entry, 0, sizeof(*mount_entry));
|
||||
*merged_out = true;
|
||||
return path_index;
|
||||
}
|
||||
|
||||
static struct ImageCache *find_attachment_record(
|
||||
const char *path, uint64_t generation, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].unit_id != unit_id ||
|
||||
g_image_cache[index].backend != backend) {
|
||||
return NULL;
|
||||
}
|
||||
return &g_image_cache[index];
|
||||
}
|
||||
|
||||
static int upsert_image_source_mapping(const char *path,
|
||||
const char *mount_point) {
|
||||
if (!path || path[0] == '\0' || !mount_point || mount_point[0] == '\0') {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (strnlen(path, MAX_PATH) >= MAX_PATH ||
|
||||
strnlen(mount_point, MAX_PATH) >= MAX_PATH) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool merged = false;
|
||||
int entry_index =
|
||||
resolve_or_merge_mapping_index(path, mount_point, &merged);
|
||||
if (entry_index == IMAGE_CACHE_INDEX_CONFLICT)
|
||||
return -1;
|
||||
if (entry_index >= 0) {
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
bool mapping_changed = merged || strcmp(entry->path, path) != 0 ||
|
||||
strcmp(entry->mount_point, mount_point) != 0;
|
||||
if (!cache_entry_is_metadata_only(entry) && mapping_changed) {
|
||||
errno = EBUSY;
|
||||
return -1;
|
||||
}
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
|
||||
if (mapping_changed)
|
||||
entry->generation = next_cache_generation();
|
||||
return entry_index;
|
||||
}
|
||||
|
||||
@@ -45,11 +225,14 @@ static int upsert_image_source_mapping(const char *path, const char *mount_point
|
||||
sizeof(g_image_cache[k].mount_point));
|
||||
g_image_cache[k].unit_id = -1;
|
||||
g_image_cache[k].backend = ATTACH_BACKEND_NONE;
|
||||
g_image_cache[k].state = ATTACHED_DEVICE_RELEASED;
|
||||
g_image_cache[k].generation = next_cache_generation();
|
||||
g_image_cache[k].valid = true;
|
||||
return k;
|
||||
}
|
||||
}
|
||||
|
||||
errno = ENOSPC;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -60,8 +243,114 @@ bool cache_image_source_mapping(const char *path, const char *mount_point) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool begin_image_attachment(const char *path, const char *mount_point,
|
||||
uint64_t *generation_out) {
|
||||
if (!path || !mount_point || !generation_out) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
if (!cache_entry_is_metadata_only(&g_image_cache[entry_index])) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EBUSY;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->generation = next_cache_generation();
|
||||
entry->unit_id = -1;
|
||||
entry->backend = ATTACH_BACKEND_NONE;
|
||||
entry->state = ATTACHED_DEVICE_ATTACHING;
|
||||
memset(&entry->detach_state, 0, sizeof(entry->detach_state));
|
||||
entry->detach_attempt_active = false;
|
||||
*generation_out = entry->generation;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cancel_image_attachment(const char *path, uint64_t generation) {
|
||||
if (!path || generation == 0)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].state != ATTACHED_DEVICE_ATTACHING ||
|
||||
g_image_cache[index].unit_id >= 0 ||
|
||||
g_image_cache[index].backend != ATTACH_BACKEND_NONE) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool publish_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].state != ATTACHED_DEVICE_ATTACHING ||
|
||||
g_image_cache[index].unit_id >= 0 ||
|
||||
g_image_cache[index].backend != ATTACH_BACKEND_NONE) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[index];
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
(void)refresh_attached_unit_identity(entry);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool complete_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].unit_id != unit_id ||
|
||||
g_image_cache[index].backend != backend ||
|
||||
g_image_cache[index].state != ATTACHED_DEVICE_ATTACHING) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_image_cache[index].state = ATTACHED_DEVICE_MOUNTED;
|
||||
(void)refresh_attached_unit_identity(&g_image_cache[index]);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
if (!path || !mount_point || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0) {
|
||||
@@ -70,8 +359,12 @@ bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->generation = next_cache_generation();
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
entry->state = ATTACHED_DEVICE_MOUNTED;
|
||||
entry->detach_attempt_active = false;
|
||||
reset_attached_unit_identity(entry);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -83,32 +376,159 @@ bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
copy_cache_entry(index, entry_out);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate_image_cache_entry(int index) {
|
||||
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
int *index_out) {
|
||||
if (!path || !entry_out)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
copy_cache_entry(index, entry_out);
|
||||
if (index_out)
|
||||
*index_out = index;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mark_image_cache_detach_requested(const char *path, uint64_t generation,
|
||||
int unit_id,
|
||||
attach_backend_t backend) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
struct ImageCache *entry = find_attachment_record(
|
||||
path, generation, unit_id, backend);
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
entry->state = ATTACHED_DEVICE_DETACH_REQUESTED;
|
||||
if (!refresh_attached_unit_identity(entry)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool claim_image_cache_detach(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend,
|
||||
image_cache_entry_t *entry_out) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend) || !entry_out) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
struct ImageCache *entry =
|
||||
find_attachment_record(path, generation, unit_id, backend);
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
if (entry->detach_attempt_active) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EINPROGRESS;
|
||||
return false;
|
||||
}
|
||||
if (!refresh_attached_unit_identity(entry)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->state = ATTACHED_DEVICE_DETACH_REQUESTED;
|
||||
entry->detach_attempt_active = true;
|
||||
copy_cache_entry((int)(entry - g_image_cache), entry_out);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool finish_image_cache_detach_attempt(
|
||||
const char *path, uint64_t generation, int unit_id,
|
||||
attach_backend_t backend,
|
||||
const attached_unit_detach_state_t *detach_state) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend) || !detach_state ||
|
||||
!detach_state->node_identity_valid) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
struct ImageCache *entry = find_attachment_record(
|
||||
path, generation, unit_id, backend);
|
||||
if (!entry || !entry->detach_attempt_active ||
|
||||
!device_identity_matches(&entry->detach_state, detach_state)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->detach_state = *detach_state;
|
||||
entry->state = ATTACHED_DEVICE_DETACH_REQUESTED;
|
||||
entry->detach_attempt_active = false;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool invalidate_image_cache_entry(int index,
|
||||
const image_cache_entry_t *expected) {
|
||||
if (!expected) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_cache[index].valid) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[index];
|
||||
if (entry->generation != expected->generation ||
|
||||
strcmp(entry->path, expected->path) != 0 ||
|
||||
strcmp(entry->mount_point, expected->mount_point) != 0 ||
|
||||
entry->unit_id != expected->unit_id ||
|
||||
entry->backend != expected->backend ||
|
||||
entry->state != expected->state || entry->detach_attempt_active ||
|
||||
entry->detach_state.requested != expected->detach_state.requested ||
|
||||
!device_identity_matches(&entry->detach_state,
|
||||
&expected->detach_state)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
g_image_cache[index].state = ATTACHED_DEVICE_RELEASED;
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
int entry_index = find_mount_point_index(mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
@@ -128,7 +548,7 @@ bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
int entry_index = find_mount_point_index(mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
@@ -140,3 +560,83 @@ bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int find_owning_cache_index(const char *path) {
|
||||
int best_index = -1;
|
||||
size_t best_length = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (!g_image_cache[i].valid)
|
||||
continue;
|
||||
size_t length = strlen(g_image_cache[i].mount_point);
|
||||
if (length <= best_length || strncmp(path, g_image_cache[i].mount_point,
|
||||
length) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (path[length] != '\0' && path[length] != '/')
|
||||
continue;
|
||||
best_index = i;
|
||||
best_length = length;
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
static size_t find_owning_chain_indices(const char *path,
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH]) {
|
||||
char current[MAX_PATH];
|
||||
(void)strlcpy(current, path, sizeof(current));
|
||||
|
||||
size_t count = 0;
|
||||
while (count < MAX_IMAGE_CHAIN_DEPTH) {
|
||||
int index = find_owning_cache_index(current);
|
||||
if (index < 0)
|
||||
break;
|
||||
indices[count++] = index;
|
||||
(void)strlcpy(current, g_image_cache[index].path, sizeof(current));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool resolve_owning_image_source_from_mount_cache(const char *path,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
if (!path || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_owning_cache_index(path);
|
||||
if (index >= 0)
|
||||
(void)strlcpy(path_out, g_image_cache[index].path, path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return index >= 0;
|
||||
}
|
||||
|
||||
size_t resolve_image_source_chain_from_mount_cache(
|
||||
const char *path,
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]) {
|
||||
if (!path || !chain)
|
||||
return 0;
|
||||
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH];
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
size_t count = find_owning_chain_indices(path, indices);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
(void)strlcpy(chain[i], g_image_cache[indices[count - i - 1u]].path,
|
||||
MAX_PATH);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool resolve_outermost_image_source_from_mount_cache(
|
||||
const char *path, char *path_out, size_t path_out_size) {
|
||||
if (!path || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH];
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
size_t count = find_owning_chain_indices(path, indices);
|
||||
if (count > 0)
|
||||
(void)strlcpy(path_out, g_image_cache[indices[count - 1u]].path,
|
||||
path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return count > 0;
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#define IMAGE_INDEX_FILE IMAGE_INDEX_FILE_PATH
|
||||
#define IMAGE_INDEX_TEMP_FILE LOG_DIR "/image_index.bin.tmp"
|
||||
#define IMAGE_INDEX_MAGIC 0x58444953u
|
||||
#define IMAGE_INDEX_VERSION 4u
|
||||
#define IMAGE_INDEX_BUILD_TIME_LENGTH 24u
|
||||
|
||||
#ifndef SHADOWMOUNT_BUILD_TIME
|
||||
#define SHADOWMOUNT_BUILD_TIME __DATE__ " " __TIME__
|
||||
#endif
|
||||
|
||||
_Static_assert(sizeof(SHADOWMOUNT_BUILD_TIME) <=
|
||||
IMAGE_INDEX_BUILD_TIME_LENGTH,
|
||||
"ShadowMount+ build time does not fit the image index");
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
int64_t size;
|
||||
int64_t mtime_sec;
|
||||
int32_t mtime_nsec;
|
||||
uint8_t complete;
|
||||
uint8_t valid;
|
||||
uint8_t reserved[2];
|
||||
} image_index_entry_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t image_index;
|
||||
uint8_t valid;
|
||||
uint8_t reserved;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} image_index_title_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t inode;
|
||||
uint64_t size;
|
||||
int64_t mtime_sec;
|
||||
int64_t ctime_sec;
|
||||
int32_t mtime_nsec;
|
||||
int32_t ctime_nsec;
|
||||
uint8_t present;
|
||||
uint8_t reserved[7];
|
||||
} image_index_file_stamp_t;
|
||||
|
||||
typedef struct {
|
||||
image_index_file_stamp_t config;
|
||||
image_index_file_stamp_t autotune;
|
||||
} image_index_context_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t entry_capacity;
|
||||
uint32_t title_capacity;
|
||||
char build_time[IMAGE_INDEX_BUILD_TIME_LENGTH];
|
||||
image_index_context_t context;
|
||||
} image_index_header_t;
|
||||
|
||||
static image_index_entry_t g_image_index[MAX_IMAGE_MOUNTS];
|
||||
static image_index_title_t g_image_titles[MAX_IMAGE_TITLES];
|
||||
static bool g_image_index_loaded;
|
||||
static bool g_image_index_dirty;
|
||||
static bool g_image_index_context_valid;
|
||||
static image_index_context_t g_image_index_context;
|
||||
static pthread_mutex_t g_image_index_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static bool read_file_stamp(const char *path,
|
||||
image_index_file_stamp_t *stamp) {
|
||||
memset(stamp, 0, sizeof(*stamp));
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0)
|
||||
return errno == ENOENT;
|
||||
|
||||
stamp->inode = (uint64_t)st.st_ino;
|
||||
stamp->size = (uint64_t)st.st_size;
|
||||
stamp->mtime_sec = (int64_t)st.st_mtim.tv_sec;
|
||||
stamp->ctime_sec = (int64_t)st.st_ctim.tv_sec;
|
||||
stamp->mtime_nsec = (int32_t)st.st_mtim.tv_nsec;
|
||||
stamp->ctime_nsec = (int32_t)st.st_ctim.tv_nsec;
|
||||
stamp->present = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_index_context(image_index_context_t *context) {
|
||||
bool config_ready = read_file_stamp(CONFIG_FILE, &context->config);
|
||||
bool autotune_ready = read_file_stamp(AUTOTUNE_FILE, &context->autotune);
|
||||
return config_ready && autotune_ready;
|
||||
}
|
||||
|
||||
static bool file_stamp_equals(const image_index_file_stamp_t *a,
|
||||
const image_index_file_stamp_t *b) {
|
||||
return a->present == b->present && a->inode == b->inode &&
|
||||
a->size == b->size && a->mtime_sec == b->mtime_sec &&
|
||||
a->mtime_nsec == b->mtime_nsec && a->ctime_sec == b->ctime_sec &&
|
||||
a->ctime_nsec == b->ctime_nsec;
|
||||
}
|
||||
|
||||
static void clear_index(void) {
|
||||
memset(g_image_index, 0, sizeof(g_image_index));
|
||||
memset(g_image_titles, 0, sizeof(g_image_titles));
|
||||
g_image_index_dirty = false;
|
||||
}
|
||||
|
||||
static void discard_index(const char *reason) {
|
||||
clear_index();
|
||||
(void)unlink(IMAGE_INDEX_FILE);
|
||||
if (reason)
|
||||
log_debug(" [IMGIDX] cache discarded (%s): %s", reason,
|
||||
IMAGE_INDEX_FILE);
|
||||
}
|
||||
|
||||
static bool refresh_index_context(void) {
|
||||
image_index_context_t context;
|
||||
if (!read_index_context(&context)) {
|
||||
discard_index(g_image_index_context_valid
|
||||
? "configuration stamp unavailable"
|
||||
: NULL);
|
||||
g_image_index_context_valid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!g_image_index_context_valid) {
|
||||
discard_index(NULL);
|
||||
} else if (!file_stamp_equals(&g_image_index_context.config,
|
||||
&context.config)) {
|
||||
discard_index("config.ini changed");
|
||||
} else if (!file_stamp_equals(&g_image_index_context.autotune,
|
||||
&context.autotune)) {
|
||||
discard_index("autotune.ini changed");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
g_image_index_context = context;
|
||||
g_image_index_context_valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool stamp_matches(const image_index_entry_t *entry,
|
||||
const struct stat *st) {
|
||||
return entry->size == (int64_t)st->st_size &&
|
||||
entry->mtime_sec == (int64_t)st->st_mtim.tv_sec &&
|
||||
entry->mtime_nsec == (int32_t)st->st_mtim.tv_nsec;
|
||||
}
|
||||
|
||||
static int find_entry(const char *path) {
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid && strcmp(g_image_index[i].path, path) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void clear_entry_titles(int entry_index) {
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (g_image_titles[i].valid &&
|
||||
g_image_titles[i].image_index == (uint16_t)entry_index) {
|
||||
memset(&g_image_titles[i], 0, sizeof(g_image_titles[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_entry(int entry_index) {
|
||||
clear_entry_titles(entry_index);
|
||||
memset(&g_image_index[entry_index], 0, sizeof(g_image_index[entry_index]));
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
|
||||
static bool save_index(void) {
|
||||
if (!g_image_index_dirty)
|
||||
return true;
|
||||
|
||||
FILE *file = fopen(IMAGE_INDEX_TEMP_FILE, "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
image_index_header_t header = {
|
||||
.magic = IMAGE_INDEX_MAGIC,
|
||||
.version = IMAGE_INDEX_VERSION,
|
||||
.entry_capacity = MAX_IMAGE_MOUNTS,
|
||||
.title_capacity = MAX_IMAGE_TITLES,
|
||||
.context = g_image_index_context,
|
||||
};
|
||||
(void)strlcpy(header.build_time, SHADOWMOUNT_BUILD_TIME,
|
||||
sizeof(header.build_time));
|
||||
bool ok = fwrite(&header, sizeof(header), 1, file) == 1 &&
|
||||
fwrite(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
|
||||
fwrite(g_image_titles, sizeof(g_image_titles), 1, file) == 1 &&
|
||||
fflush(file) == 0;
|
||||
if (ok && fsync(fileno(file)) != 0)
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (!ok || rename(IMAGE_INDEX_TEMP_FILE, IMAGE_INDEX_FILE) != 0) {
|
||||
int saved_errno = errno;
|
||||
if (saved_errno == 0)
|
||||
saved_errno = EIO;
|
||||
(void)unlink(IMAGE_INDEX_TEMP_FILE);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
g_image_index_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool loaded_index_is_valid(void) {
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
const image_index_entry_t *entry = &g_image_index[i];
|
||||
if (!entry->valid)
|
||||
continue;
|
||||
if (entry->complete > 1 || entry->path[0] == '\0' ||
|
||||
memchr(entry->path, '\0', sizeof(entry->path)) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (!title->valid)
|
||||
continue;
|
||||
if (title->image_index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_index[title->image_index].valid ||
|
||||
title->title_id[0] == '\0' ||
|
||||
memchr(title->title_id, '\0', sizeof(title->title_id)) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *index_header_invalid_reason(
|
||||
const image_index_header_t *header) {
|
||||
if (header->magic != IMAGE_INDEX_MAGIC ||
|
||||
header->version != IMAGE_INDEX_VERSION ||
|
||||
header->entry_capacity != MAX_IMAGE_MOUNTS ||
|
||||
header->title_capacity != MAX_IMAGE_TITLES) {
|
||||
return "cache format changed";
|
||||
}
|
||||
if (strncmp(header->build_time, SHADOWMOUNT_BUILD_TIME,
|
||||
sizeof(header->build_time)) != 0) {
|
||||
return "ShadowMount+ build changed";
|
||||
}
|
||||
if (!g_image_index_context_valid)
|
||||
return "configuration stamp unavailable";
|
||||
if (!file_stamp_equals(&header->context.config,
|
||||
&g_image_index_context.config)) {
|
||||
return "config.ini changed";
|
||||
}
|
||||
if (!file_stamp_equals(&header->context.autotune,
|
||||
&g_image_index_context.autotune)) {
|
||||
return "autotune.ini changed";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void load_index(void) {
|
||||
if (g_image_index_loaded)
|
||||
return;
|
||||
g_image_index_loaded = true;
|
||||
|
||||
g_image_index_context_valid = read_index_context(&g_image_index_context);
|
||||
|
||||
FILE *file = fopen(IMAGE_INDEX_FILE, "rb");
|
||||
if (!file)
|
||||
return;
|
||||
image_index_header_t header;
|
||||
bool header_ready = fread(&header, sizeof(header), 1, file) == 1;
|
||||
const char *invalid_reason =
|
||||
header_ready ? index_header_invalid_reason(&header)
|
||||
: "cache header is incomplete";
|
||||
bool ok = !invalid_reason &&
|
||||
fread(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
|
||||
fread(g_image_titles, sizeof(g_image_titles), 1, file) == 1;
|
||||
(void)fclose(file);
|
||||
if (ok) {
|
||||
ok = loaded_index_is_valid();
|
||||
if (!ok)
|
||||
invalid_reason = "cache entries are invalid";
|
||||
} else if (!invalid_reason) {
|
||||
invalid_reason = "cache data is incomplete";
|
||||
}
|
||||
if (!ok) {
|
||||
discard_index(invalid_reason);
|
||||
}
|
||||
}
|
||||
|
||||
static int reserve_entry(const char *path) {
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index >= 0)
|
||||
return entry_index;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (!g_image_index[i].valid) {
|
||||
g_image_index[i].valid = 1;
|
||||
(void)strlcpy(g_image_index[i].path, path, sizeof(g_image_index[i].path));
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void begin_scan_locked(const char *path, const struct stat *st) {
|
||||
int entry_index = reserve_entry(path);
|
||||
if (entry_index < 0) {
|
||||
log_debug(" [IMGIDX] cache full, scanning without fingerprint: %s", path);
|
||||
return;
|
||||
}
|
||||
clear_entry_titles(entry_index);
|
||||
image_index_entry_t *entry = &g_image_index[entry_index];
|
||||
entry->size = (int64_t)st->st_size;
|
||||
entry->mtime_sec = (int64_t)st->st_mtim.tv_sec;
|
||||
entry->mtime_nsec = (int32_t)st->st_mtim.tv_nsec;
|
||||
entry->complete = 0;
|
||||
entry->valid = 1;
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
|
||||
static bool cached_titles_ready(int entry_index,
|
||||
const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready) {
|
||||
const char *image_path = g_image_index[entry_index].path;
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (!g_image_titles[i].valid ||
|
||||
g_image_titles[i].image_index != (uint16_t)entry_index) {
|
||||
continue;
|
||||
}
|
||||
const char *title_id = g_image_titles[i].title_id;
|
||||
if (!is_installed(title_id) || !has_appmeta_data(title_id))
|
||||
return false;
|
||||
char runtime_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (!read_mount_link(title_id, runtime_path, sizeof(runtime_path)) ||
|
||||
!is_under_image_mount_base(runtime_path) ||
|
||||
!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (app_db_titles_ready &&
|
||||
!app_db_title_list_contains(app_db_titles, title_id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_image_index_visit_ready_titles(
|
||||
const char *path, const struct stat *st,
|
||||
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready,
|
||||
sm_image_index_title_visitor_t visitor) {
|
||||
if (!path || !st || !visitor)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
bool ready = entry_index >= 0 && g_image_index[entry_index].complete &&
|
||||
stamp_matches(&g_image_index[entry_index], st) &&
|
||||
cached_titles_ready(entry_index, app_db_titles,
|
||||
app_db_titles_ready);
|
||||
if (ready) {
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (title->valid && title->image_index == (uint16_t)entry_index)
|
||||
visitor(title->title_id);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return ready;
|
||||
}
|
||||
|
||||
bool sm_image_index_has_source_for_title(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (!title->valid || strcmp(title->title_id, title_id) != 0)
|
||||
continue;
|
||||
|
||||
const image_index_entry_t *entry = &g_image_index[title->image_index];
|
||||
found = entry->valid && path_exists(entry->path);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return found;
|
||||
}
|
||||
|
||||
void sm_image_index_begin_scan(const char *path, const struct stat *st) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
begin_scan_locked(path, st);
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
bool sm_image_index_record_game(const char *game_path, const char *title_id) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
char image_path[MAX_PATH];
|
||||
if (!resolve_outermost_image_source_from_mount_cache(
|
||||
game_path, image_path, sizeof(image_path))) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return true;
|
||||
}
|
||||
int entry_index = find_entry(image_path);
|
||||
if (entry_index < 0) {
|
||||
struct stat st;
|
||||
if (stat(image_path, &st) != 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
begin_scan_locked(image_path, &st);
|
||||
entry_index = find_entry(image_path);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
char tracked_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (is_installed(title_id) &&
|
||||
read_mount_link(title_id, tracked_path, sizeof(tracked_path)) &&
|
||||
strcmp(tracked_path, game_path) == 0 &&
|
||||
(!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) &&
|
||||
!write_mount_image_link(title_id, image_path)) {
|
||||
log_debug(" [IMGIDX] failed to repair image link: %s -> %s", title_id,
|
||||
image_path);
|
||||
}
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (g_image_titles[i].valid &&
|
||||
g_image_titles[i].image_index == (uint16_t)entry_index &&
|
||||
strcmp(g_image_titles[i].title_id, title_id) == 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (g_image_titles[i].valid)
|
||||
continue;
|
||||
g_image_titles[i].valid = 1;
|
||||
g_image_titles[i].image_index = (uint16_t)entry_index;
|
||||
(void)strlcpy(g_image_titles[i].title_id, title_id,
|
||||
sizeof(g_image_titles[i].title_id));
|
||||
g_image_index_dirty = true;
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return true;
|
||||
}
|
||||
log_debug(" [IMGIDX] title cache full: %s", title_id);
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
void sm_image_index_complete_scan(const char *path) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return;
|
||||
}
|
||||
image_index_entry_t *entry = &g_image_index[entry_index];
|
||||
if (!entry->complete) {
|
||||
entry->complete = 1;
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
void sm_image_index_flush(void) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
if (refresh_index_context() && !save_index())
|
||||
log_debug(" [IMGIDX] failed to persist scan index: %s", strerror(errno));
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
void sm_image_index_prune(void) {
|
||||
struct stat st;
|
||||
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
bool context_ready = refresh_index_context();
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid && stat(g_image_index[i].path, &st) != 0 &&
|
||||
errno == ENOENT) {
|
||||
clear_entry(i);
|
||||
}
|
||||
}
|
||||
if (context_ready && !save_index())
|
||||
log_debug(" [IMGIDX] failed to persist prune: %s", strerror(errno));
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
bool sm_image_index_snapshot(sm_image_index_snapshot_entry_t **entries_out,
|
||||
size_t *count_out) {
|
||||
if (!entries_out || !count_out)
|
||||
return false;
|
||||
*entries_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
size_t count = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid)
|
||||
count++;
|
||||
}
|
||||
|
||||
sm_image_index_snapshot_entry_t *entries = NULL;
|
||||
if (count > 0) {
|
||||
entries = calloc(count, sizeof(*entries));
|
||||
if (!entries) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t copied = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
const image_index_entry_t *source = &g_image_index[i];
|
||||
if (!source->valid)
|
||||
continue;
|
||||
sm_image_index_snapshot_entry_t *entry = &entries[copied++];
|
||||
(void)strlcpy(entry->path, source->path, sizeof(entry->path));
|
||||
entry->size = source->size;
|
||||
entry->mtime_sec = source->mtime_sec;
|
||||
entry->mtime_nsec = source->mtime_nsec;
|
||||
entry->complete = source->complete != 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
*entries_out = entries;
|
||||
*count_out = copied;
|
||||
return true;
|
||||
}
|
||||
+51
-60
@@ -14,6 +14,7 @@
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
@@ -61,29 +62,6 @@ static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool write_link_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [LINK] open failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
if (fprintf(f, "%s", value) < 0)
|
||||
saved_errno = errno;
|
||||
if (fflush(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [LINK] write failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_appmeta_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
@@ -139,11 +117,11 @@ static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
|
||||
char dst_path[MAX_PATH];
|
||||
|
||||
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, relative_path);
|
||||
if (access(src_path, F_OK) != 0)
|
||||
if (!path_exists(src_path))
|
||||
return true;
|
||||
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
|
||||
if (access(dst_path, F_OK) == 0)
|
||||
if (path_exists(dst_path))
|
||||
return true;
|
||||
|
||||
if (copy_file(src_path, dst_path) == 0)
|
||||
@@ -182,6 +160,13 @@ static bool update_trophy_metadata(const char *title_id,
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void update_registered_title_snd0info(const char *title_id) {
|
||||
sceKernelUsleep(200000);
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
|
||||
// --- Install/Remount Action ---
|
||||
static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
const char *title_name, bool is_remount,
|
||||
@@ -193,7 +178,8 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
char user_sce_sys[MAX_PATH];
|
||||
char src_sce_sys[MAX_PATH];
|
||||
char src_snd0[MAX_PATH];
|
||||
char image_source_path[MAX_PATH];
|
||||
char image_source_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_source_count = 0;
|
||||
bool has_image_source = false;
|
||||
bool appmeta_missing = false;
|
||||
bool metadata_restaged = false;
|
||||
@@ -206,8 +192,9 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id);
|
||||
|
||||
if (is_under_image_mount_base(src_path)) {
|
||||
has_image_source = resolve_image_source_from_mount_cache(
|
||||
src_path, image_source_path, sizeof(image_source_path));
|
||||
image_source_count = resolve_image_source_chain_from_mount_cache(
|
||||
src_path, image_source_chain);
|
||||
has_image_source = image_source_count > 0;
|
||||
if (!has_image_source) {
|
||||
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
|
||||
src_path);
|
||||
@@ -289,7 +276,8 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
mkdir(APP_BASE, 0777);
|
||||
mkdir(user_app_dir, 0777);
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_link_file(lnk_path, src_path)) {
|
||||
if (!write_mount_link(title_id, src_path)) {
|
||||
log_debug(" [LINK] write failed for %s: %s", lnk_path, strerror(errno));
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
@@ -301,17 +289,18 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
|
||||
user_app_dir);
|
||||
if (has_image_source) {
|
||||
if (!write_link_file(img_lnk_path, image_source_path)) {
|
||||
if (!write_mount_image_chain(title_id, image_source_chain,
|
||||
image_source_count)) {
|
||||
(void)unlink(lnk_path);
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
|
||||
image_source_path);
|
||||
if (!cache_image_source_mapping(image_source_path, src_path)) {
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s layers=%zu",
|
||||
img_lnk_path, image_source_chain[0], image_source_count);
|
||||
if (!cache_image_source_mapping(image_source_chain[0], src_path)) {
|
||||
log_debug(" [LINK] image source cache update failed: %s -> %s",
|
||||
src_path, image_source_path);
|
||||
image_source_chain[0], src_path);
|
||||
}
|
||||
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
@@ -342,43 +331,45 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
}
|
||||
|
||||
// REGISTER
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
int res = 0;
|
||||
if (sm_shellcore_install_bridge_enabled()) {
|
||||
if (!sm_shellcore_install_title_dir(title_id, APP_BASE "/", &res)) {
|
||||
log_debug(" [REG] internal AppInstallTitleDir bridge unavailable");
|
||||
notify_system_l10n(SM_L10N_REGISTER_BRIDGE_UNAVAILABLE, title_name,
|
||||
title_id);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
} else {
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system_l10n(SM_L10N_REGISTER_FAILED_UNAVAILABLE, title_name,
|
||||
title_id);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
res = app_install_title_dir_fn(title_id, APP_BASE "/", NULL);
|
||||
}
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
if (res == 0) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Installed NEW!");
|
||||
notify_game_installed_rich(title_id);
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
if (has_src_snd0)
|
||||
update_registered_title_snd0info(title_id);
|
||||
} else if ((uint32_t)res == 0x80990002u) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Restored.");
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
if (has_src_snd0)
|
||||
update_registered_title_snd0info(title_id);
|
||||
// Silent on restore/remount to avoid spam
|
||||
} else {
|
||||
log_debug(" [REG] FAIL: 0x%x", res);
|
||||
notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id,
|
||||
(uint32_t)res);
|
||||
notify_system_l10n(SM_L10N_REGISTER_FAILED_CODE, title_name, title_id,
|
||||
(uint32_t)res);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -406,8 +397,8 @@ void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
} else {
|
||||
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
|
||||
if (!use_app_install_all) {
|
||||
notify_system_info("Installing: %s (%s)...", c->title_name,
|
||||
c->title_id);
|
||||
notify_system_info_l10n(SM_L10N_INSTALLING, c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-7
@@ -9,6 +9,7 @@
|
||||
#include "sm_log.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
@@ -49,6 +50,10 @@ static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
bool sm_install_has_pending_work(void) {
|
||||
return g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *find_pending_install_entry(
|
||||
const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
@@ -239,8 +244,8 @@ static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
|
||||
|
||||
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
notify_system("Install failed: %s (%s)\nTimed out after 5 minutes",
|
||||
entry->title_name, entry->title_id);
|
||||
notify_system_l10n(SM_L10N_INSTALL_FAILED_TIMEOUT, entry->title_name,
|
||||
entry->title_id);
|
||||
(void)note_pending_install_failure(entry);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
@@ -280,6 +285,8 @@ static void poll_pending_installs(void) {
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] deferred release after install completion");
|
||||
}
|
||||
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
@@ -365,7 +372,8 @@ static void notify_queued_install_batch(void) {
|
||||
|
||||
char message[3075];
|
||||
int shown_count = 0;
|
||||
snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count);
|
||||
snprintf(message, sizeof(message), sm_l10n_get(SM_L10N_BATCH_INSTALL_QUEUED),
|
||||
queued_count);
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
@@ -378,7 +386,8 @@ static void notify_queued_install_batch(void) {
|
||||
if (shown_count < queued_count) {
|
||||
size_t used = strlen(message);
|
||||
if (used < sizeof(message)) {
|
||||
(void)snprintf(message + used, sizeof(message) - used, "\n... and %d more",
|
||||
(void)snprintf(message + used, sizeof(message) - used,
|
||||
sm_l10n_get(SM_L10N_BATCH_INSTALL_AND_MORE),
|
||||
queued_count - shown_count);
|
||||
}
|
||||
}
|
||||
@@ -425,12 +434,12 @@ bool sm_install_submit_queued(void) {
|
||||
log_queued_install_batch();
|
||||
notify_queued_install_batch();
|
||||
|
||||
int res = sceAppInstUtilAppInstallAll();
|
||||
int res = sceAppInstUtilAppInstallAll(NULL);
|
||||
if (res != 0) {
|
||||
log_debug(" [REG] Batch install request failed: 0x%x", res);
|
||||
if (!g_queued_install_submit_failure_notified) {
|
||||
notify_system("Batch install failed for %d app(s).\ncode=0x%08X",
|
||||
queued_count, (uint32_t)res);
|
||||
notify_system_l10n(SM_L10N_BATCH_INSTALL_FAILED, queued_count,
|
||||
(uint32_t)res);
|
||||
g_queued_install_submit_failure_notified = true;
|
||||
}
|
||||
schedule_queued_install_submit_retry(now_us);
|
||||
|
||||
+32
-4
@@ -708,8 +708,8 @@ static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
|
||||
if (fully_applied) {
|
||||
log_debug(" [KSTUFF] %s both sysentvecs", enabled ? "enabled" : "disabled");
|
||||
if (notify_user) {
|
||||
notify_system_info(enabled ? "KStuff active again"
|
||||
: "KStuff paused while the game is running");
|
||||
notify_system_info_l10n(enabled ? SM_L10N_KSTUFF_ACTIVE
|
||||
: SM_L10N_KSTUFF_PAUSED);
|
||||
}
|
||||
} else {
|
||||
log_debug(" [KSTUFF] %s request incomplete (ps5=%s ps4=%s)",
|
||||
@@ -784,6 +784,34 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
image_backed ? "image" : "direct", delay_seconds);
|
||||
}
|
||||
|
||||
bool sm_kstuff_game_handoff(pid_t old_pid, pid_t new_pid,
|
||||
const char *title_id, uint32_t app_id) {
|
||||
if (old_pid <= 0 || new_pid <= 0 || old_pid == new_pid || !title_id ||
|
||||
title_id[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
if (!g_kstuff.game.active || g_kstuff.game.pid != old_pid ||
|
||||
strcmp(g_kstuff.game.title_id, title_id) != 0) {
|
||||
// Config may have enabled tracking while the old process was pending, or
|
||||
// the old slot may already have been cleared. Start a fresh slot instead
|
||||
// of leaving the replacement process untracked.
|
||||
sm_kstuff_game_on_exec(new_pid, title_id, app_id, monotonic_time_us());
|
||||
return false;
|
||||
}
|
||||
|
||||
g_kstuff.game.pid = new_pid;
|
||||
if (app_id != 0)
|
||||
g_kstuff.game.app_id = app_id;
|
||||
// The auto-pause state/deadline belongs to the application lifetime, not to
|
||||
// the individual process. Only mdbg must follow the replacement PID.
|
||||
sm_mdbg_game_on_exec(new_pid, title_id, g_kstuff.game.app_id);
|
||||
log_debug(" [KSTUFF] process handoff: %s pid=%ld -> pid=%ld "
|
||||
"(pause_applied=%s)",
|
||||
title_id, (long)old_pid, (long)new_pid,
|
||||
g_kstuff.game.pause_applied ? "yes" : "no");
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id) {
|
||||
atomic_store(&g_pending_app_focus_id, app_id);
|
||||
atomic_store(&g_pending_app_focus_valid, true);
|
||||
@@ -802,11 +830,11 @@ void sm_kstuff_game_on_exit(pid_t pid) {
|
||||
finish_tracked_game_clear("tracked game exit");
|
||||
}
|
||||
|
||||
void sm_kstuff_game_poll(void) {
|
||||
void sm_kstuff_game_poll(bool process_active) {
|
||||
if (atomic_exchange(&g_pending_config_reload, false))
|
||||
apply_kstuff_config_reload();
|
||||
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
if (!process_active || !sm_kstuff_game_feature_enabled())
|
||||
return;
|
||||
|
||||
if (atomic_exchange(&g_pending_app_focus_valid, false)) {
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
#include "sm_l10n.h"
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define SCE_SYSTEM_SERVICE_PARAM_ID_LANG 1
|
||||
#define SM_LANGUAGE_EN_US 1
|
||||
#define SM_SYSTEM_LANGUAGE_COUNT 31
|
||||
#define SM_LANGUAGE_UNINITIALIZED (-2)
|
||||
|
||||
#include "lang/ar_sa.inc"
|
||||
#include "lang/cs_cz.inc"
|
||||
#include "lang/da_dk.inc"
|
||||
#include "lang/de_de.inc"
|
||||
#include "lang/el_gr.inc"
|
||||
#include "lang/en_gb.inc"
|
||||
#include "lang/en_us.inc"
|
||||
#include "lang/es_es.inc"
|
||||
#include "lang/es_mx.inc"
|
||||
#include "lang/fi_fi.inc"
|
||||
#include "lang/fr_ca.inc"
|
||||
#include "lang/fr_fr.inc"
|
||||
#include "lang/hu_hu.inc"
|
||||
#include "lang/id_id.inc"
|
||||
#include "lang/it_it.inc"
|
||||
#include "lang/ja_jp.inc"
|
||||
#include "lang/ko_kr.inc"
|
||||
#include "lang/nl_nl.inc"
|
||||
#include "lang/no_no.inc"
|
||||
#include "lang/pl_pl.inc"
|
||||
#include "lang/pt_br.inc"
|
||||
#include "lang/pt_pt.inc"
|
||||
#include "lang/ro_ro.inc"
|
||||
#include "lang/ru_ru.inc"
|
||||
#include "lang/sv_se.inc"
|
||||
#include "lang/th_th.inc"
|
||||
#include "lang/tr_tr.inc"
|
||||
#include "lang/uk_ua.inc"
|
||||
#include "lang/vi_vn.inc"
|
||||
#include "lang/zh_cn.inc"
|
||||
#include "lang/zh_tw.inc"
|
||||
|
||||
typedef struct {
|
||||
const char *country;
|
||||
const char *lang;
|
||||
const char *locale;
|
||||
const char *const *catalog;
|
||||
} sm_region_t;
|
||||
|
||||
static const sm_region_t g_regions[] = {
|
||||
{"jp", "ja", "ja-JP", g_ja_jp}, {"us", "en", "en-US", g_en_us},
|
||||
{"fr", "fr", "fr-FR", g_fr_fr}, {"es", "es", "es-ES", g_es_es},
|
||||
{"de", "de", "de-DE", g_de_de}, {"it", "it", "it-IT", g_it_it},
|
||||
{"nl", "nl", "nl-NL", g_nl_nl}, {"pt", "pt", "pt-PT", g_pt_pt},
|
||||
{"ru", "ru", "ru-RU", g_ru_ru}, {"kr", "ko", "ko-KR", g_ko_kr},
|
||||
{"tw", "zh", "zh-TW", g_zh_tw}, {"cn", "zh", "zh-CN", g_zh_cn},
|
||||
{"fi", "fi", "fi-FI", g_fi_fi}, {"se", "sv", "sv-SE", g_sv_se},
|
||||
{"dk", "da", "da-DK", g_da_dk}, {"no", "no", "no-NO", g_no_no},
|
||||
{"pl", "pl", "pl-PL", g_pl_pl}, {"br", "pt", "pt-BR", g_pt_br},
|
||||
{"gb", "en", "en-GB", g_en_gb}, {"tr", "tr", "tr-TR", g_tr_tr},
|
||||
{"mx", "es", "es-MX", g_es_mx}, {"sa", "ar", "ar-SA", g_ar_sa},
|
||||
{"ca", "fr", "fr-CA", g_fr_ca}, {"cz", "cs", "cs-CZ", g_cs_cz},
|
||||
{"hu", "hu", "hu-HU", g_hu_hu}, {"gr", "el", "el-GR", g_el_gr},
|
||||
{"ro", "ro", "ro-RO", g_ro_ro}, {"th", "th", "th-TH", g_th_th},
|
||||
{"vn", "vi", "vi-VN", g_vi_vn}, {"id", "id", "id-ID", g_id_id},
|
||||
{"ua", "uk", "uk-UA", g_uk_ua},
|
||||
};
|
||||
|
||||
#define SM_REGION_COUNT (sizeof(g_regions) / sizeof(g_regions[0]))
|
||||
|
||||
_Static_assert(SM_REGION_COUNT == SM_SYSTEM_LANGUAGE_COUNT,
|
||||
"unexpected language count");
|
||||
|
||||
static atomic_int g_active_lang = ATOMIC_VAR_INIT(SM_LANGUAGE_UNINITIALIZED);
|
||||
|
||||
static bool is_valid_language_id(int32_t language_id) {
|
||||
return language_id >= 0 && language_id < (int32_t)SM_REGION_COUNT;
|
||||
}
|
||||
|
||||
static bool is_valid_system_language_id(int32_t language_id) {
|
||||
return language_id >= 0 && language_id < SM_SYSTEM_LANGUAGE_COUNT;
|
||||
}
|
||||
|
||||
static bool language_code_matches(const char *value, const char *code) {
|
||||
char normalized[16];
|
||||
size_t len = strlen(code);
|
||||
if (len >= sizeof(normalized))
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
char ch = code[i];
|
||||
normalized[i] = (ch == '-') ? '_' : ch;
|
||||
}
|
||||
normalized[len] = '\0';
|
||||
|
||||
return strcasecmp(value, code) == 0 || strcasecmp(value, normalized) == 0;
|
||||
}
|
||||
|
||||
bool sm_l10n_parse_language_id(const char *value, int32_t *language_id_out) {
|
||||
if (!value || !language_id_out)
|
||||
return false;
|
||||
|
||||
if (strcasecmp(value, "auto") == 0 || strcasecmp(value, "system") == 0 ||
|
||||
strcasecmp(value, "default") == 0) {
|
||||
*language_id_out = SM_LANGUAGE_AUTO;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < SM_REGION_COUNT; ++i) {
|
||||
const sm_region_t *region = &g_regions[i];
|
||||
if (language_code_matches(value, region->locale) ||
|
||||
strcasecmp(value, region->country) == 0 ||
|
||||
strcasecmp(value, region->lang) == 0) {
|
||||
*language_id_out = (int32_t)i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *sm_l10n_language_name(int32_t language_id) {
|
||||
if (language_id == SM_LANGUAGE_AUTO)
|
||||
return "auto";
|
||||
if (!is_valid_language_id(language_id))
|
||||
return "invalid";
|
||||
return g_regions[language_id].locale;
|
||||
}
|
||||
|
||||
void sm_l10n_init(void) {
|
||||
int32_t sys_lang = -1;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
int32_t active_lang = cfg->language_id;
|
||||
bool auto_language = (active_lang == SM_LANGUAGE_AUTO);
|
||||
|
||||
if (auto_language &&
|
||||
sceSystemServiceParamGetInt(SCE_SYSTEM_SERVICE_PARAM_ID_LANG,
|
||||
&sys_lang) == 0 &&
|
||||
is_valid_system_language_id(sys_lang)) {
|
||||
active_lang = sys_lang;
|
||||
} else if (auto_language) {
|
||||
active_lang = SM_LANGUAGE_EN_US;
|
||||
}
|
||||
|
||||
if (!is_valid_language_id(active_lang))
|
||||
active_lang = SM_LANGUAGE_EN_US;
|
||||
|
||||
int previous_lang = atomic_load_explicit(&g_active_lang, memory_order_relaxed);
|
||||
atomic_store_explicit(&g_active_lang, active_lang, memory_order_release);
|
||||
|
||||
if (previous_lang != active_lang) {
|
||||
const sm_region_t *region = &g_regions[active_lang];
|
||||
if (auto_language) {
|
||||
log_debug(" [L10N] language=auto active=%d country=%s lang=%s locale=%s",
|
||||
active_lang, region->country, region->lang, region->locale);
|
||||
} else {
|
||||
log_debug(" [L10N] language=config active=%d country=%s lang=%s locale=%s",
|
||||
active_lang, region->country, region->lang, region->locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char *sm_l10n_get(sm_l10n_key_t key) {
|
||||
if (key < 0 || key >= SM_L10N_COUNT)
|
||||
return "";
|
||||
|
||||
int active_lang = atomic_load_explicit(&g_active_lang, memory_order_acquire);
|
||||
if (!is_valid_language_id(active_lang)) {
|
||||
sm_l10n_init();
|
||||
active_lang = atomic_load_explicit(&g_active_lang, memory_order_acquire);
|
||||
}
|
||||
|
||||
const char *const *catalog = g_regions[active_lang].catalog;
|
||||
if (catalog[key])
|
||||
return catalog[key];
|
||||
return g_en_us[key] ? g_en_us[key] : "";
|
||||
}
|
||||
+56
-17
@@ -135,6 +135,7 @@ static bool build_notification_metadata(char *created_at,
|
||||
static bool send_rich_notification(const char *message) {
|
||||
char escaped_message[4096];
|
||||
char escaped_version[128];
|
||||
char escaped_action[256];
|
||||
char payload[8192];
|
||||
char created_at[32];
|
||||
char notification_id[32];
|
||||
@@ -151,9 +152,12 @@ static bool send_rich_notification(const char *message) {
|
||||
|
||||
escaped_message[0] = '\0';
|
||||
escaped_version[0] = '\0';
|
||||
escaped_action[0] = '\0';
|
||||
append_json_escaped(escaped_message, sizeof(escaped_message), message);
|
||||
append_json_escaped(escaped_version, sizeof(escaped_version),
|
||||
SHADOWMOUNT_VERSION);
|
||||
append_json_escaped(escaped_action, sizeof(escaped_action),
|
||||
sm_l10n_get(SM_L10N_ACTION_DEBUG_SETTINGS));
|
||||
if (!build_notification_metadata(created_at, notification_id,
|
||||
sizeof(notification_id))) {
|
||||
return false;
|
||||
@@ -186,7 +190,7 @@ static bool send_rich_notification(const char *message) {
|
||||
" },\n"
|
||||
" \"actions\": [\n"
|
||||
" {\n"
|
||||
" \"actionName\": \"Go to Debug Settings\",\n"
|
||||
" \"actionName\": \"%s\",\n"
|
||||
" \"actionType\": \"DeepLink\",\n"
|
||||
" \"defaultFocus\": true,\n"
|
||||
" \"parameters\": {\n"
|
||||
@@ -214,8 +218,8 @@ static bool send_rich_notification(const char *message) {
|
||||
" \"createdDateTime\": \"%s\",\n"
|
||||
" \"localNotificationId\": \"%s\"\n"
|
||||
"}",
|
||||
escaped_message, escaped_version, escaped_message, created_at,
|
||||
notification_id);
|
||||
escaped_message, escaped_version, escaped_action, escaped_message,
|
||||
created_at, notification_id);
|
||||
if (len < 0 || (size_t)len >= sizeof(payload))
|
||||
return false;
|
||||
|
||||
@@ -248,6 +252,10 @@ static void notify_system_plain_v(const char *fmt, va_list args) {
|
||||
notify_system_plain_message(message);
|
||||
}
|
||||
|
||||
static void notify_system_plain_l10n_v(sm_l10n_key_t key, va_list args) {
|
||||
notify_system_plain_v(sm_l10n_get(key), args);
|
||||
}
|
||||
|
||||
static FILE *ensure_log_file_open_locked(void) {
|
||||
if (g_log_file)
|
||||
return g_log_file;
|
||||
@@ -311,15 +319,15 @@ void sm_log_shutdown(void) {
|
||||
pthread_mutex_unlock(&g_log_mutex);
|
||||
}
|
||||
|
||||
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
|
||||
void notify_system_rich_l10n(bool allow_in_quiet_mode, sm_l10n_key_t key, ...) {
|
||||
char message[3075];
|
||||
va_list args;
|
||||
|
||||
if (runtime_config()->quiet_mode && !allow_in_quiet_mode)
|
||||
return;
|
||||
|
||||
va_start(args, fmt);
|
||||
vsnprintf(message, sizeof(message), fmt, args);
|
||||
va_start(args, key);
|
||||
vsnprintf(message, sizeof(message), sm_l10n_get(key), args);
|
||||
va_end(args);
|
||||
|
||||
if (!send_rich_notification(message)) {
|
||||
@@ -332,8 +340,10 @@ void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
|
||||
|
||||
void notify_game_installed_rich(const char *title_id) {
|
||||
if (!send_game_installed_rich_notification(title_id)) {
|
||||
notify_system_info("Installed: %s",
|
||||
(title_id && title_id[0] != '\0') ? title_id : "Unknown");
|
||||
notify_system_info_l10n(
|
||||
SM_L10N_INSTALLED, (title_id && title_id[0] != '\0')
|
||||
? title_id
|
||||
: sm_l10n_get(SM_L10N_UNKNOWN));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,12 +360,22 @@ void notify_system_info(const char *fmt, ...) {
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void notify_system_info_l10n(sm_l10n_key_t key, ...) {
|
||||
if (runtime_config()->quiet_mode)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, key);
|
||||
notify_system_plain_l10n_v(key, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void sm_error_clear(void) {
|
||||
memset(&g_last_error, 0, sizeof(g_last_error));
|
||||
}
|
||||
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...) {
|
||||
static void sm_error_set_v(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, va_list args) {
|
||||
memset(&g_last_error, 0, sizeof(g_last_error));
|
||||
g_last_error.valid = true;
|
||||
g_last_error.code = code;
|
||||
@@ -364,12 +384,24 @@ void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
sizeof(g_last_error.subsystem));
|
||||
if (path && path[0] != '\0')
|
||||
(void)strlcpy(g_last_error.path, path, sizeof(g_last_error.path));
|
||||
if (fmt && fmt[0] != '\0') {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
if (fmt && fmt[0] != '\0')
|
||||
vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
sm_error_set_v(subsystem, code, path, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void sm_error_set_l10n(const char *subsystem, int code, const char *path,
|
||||
sm_l10n_key_t key, ...) {
|
||||
va_list args;
|
||||
va_start(args, key);
|
||||
sm_error_set_v(subsystem, code, path, sm_l10n_get(key), args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
const sm_error_t *sm_last_error(void) {
|
||||
@@ -391,6 +423,13 @@ void notify_system(const char *fmt, ...) {
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void notify_system_l10n(sm_l10n_key_t key, ...) {
|
||||
va_list args;
|
||||
va_start(args, key);
|
||||
notify_system_plain_l10n_v(key, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void notify_image_mount_failed(const char *path, int mount_err) {
|
||||
if (g_last_error.valid && g_last_error.message[0] != '\0') {
|
||||
const char *error_path =
|
||||
@@ -400,6 +439,6 @@ void notify_image_mount_failed(const char *path, int mount_err) {
|
||||
return;
|
||||
}
|
||||
|
||||
notify_system("Image mount failed: 0x%08X (%s)\n%s", (uint32_t)mount_err,
|
||||
strerror(mount_err), path);
|
||||
notify_system_l10n(SM_L10N_IMAGE_MOUNT_FAILED, (uint32_t)mount_err,
|
||||
strerror(mount_err), path);
|
||||
}
|
||||
@@ -292,8 +292,6 @@ static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
|
||||
}
|
||||
if (remove_line) {
|
||||
changed = true;
|
||||
if (truncated)
|
||||
drain_manual_file_line(in);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
+222
-42
@@ -25,6 +25,10 @@ int mdbg_call(void *cmd, void *req, void *res);
|
||||
#define MDBG_AUTOTUNE_WINDOW_US (300ull * 1000000ull)
|
||||
#define MDBG_LOG_LINE_BUFFER_SIZE 512u
|
||||
#define MDBG_RTLD_ERROR_PREFIX_SIZE 32u
|
||||
#define MDBG_FATAL_ERROR_BUFFER_SIZE 256u
|
||||
#define MDBG_FATAL_EXCEPTION_PREFIX "# exception: "
|
||||
#define MDBG_FATAL_THREAD_NAME_PREFIX "# thread name: "
|
||||
#define MDBG_FATAL_PROCESS_ID_PREFIX "# proc ID: "
|
||||
#ifndef MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_USE_PRIVATE_LOG_TEXT 0
|
||||
#endif
|
||||
@@ -56,6 +60,12 @@ typedef struct {
|
||||
uint64_t reserved[2];
|
||||
} mdbg_res_t;
|
||||
|
||||
typedef enum {
|
||||
MDBG_FAILURE_GENERIC = 0,
|
||||
MDBG_FAILURE_RTLD,
|
||||
MDBG_FAILURE_FATAL_SIGNAL,
|
||||
} mdbg_failure_kind_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool log_monitoring_active;
|
||||
@@ -75,9 +85,13 @@ typedef struct {
|
||||
size_t log_buffer_size;
|
||||
size_t log_snapshot_length;
|
||||
size_t log_line_length;
|
||||
size_t fatal_error_length;
|
||||
pid_t fatal_error_pid;
|
||||
bool fatal_error_active;
|
||||
char *log_snapshot;
|
||||
char *log_storage;
|
||||
char log_line[MDBG_LOG_LINE_BUFFER_SIZE];
|
||||
char fatal_error[MDBG_FATAL_ERROR_BUFFER_SIZE];
|
||||
mdbg_game_state_t game;
|
||||
} sm_mdbg_state_t;
|
||||
|
||||
@@ -93,6 +107,7 @@ static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out);
|
||||
static bool is_mdbg_process_gone_error(int ret);
|
||||
static void reset_log_line_buffer(void);
|
||||
static void reset_fatal_error_buffer(void);
|
||||
static void reset_log_snapshot(void);
|
||||
static void free_log_buffers(void);
|
||||
static bool ensure_log_buffers(void);
|
||||
@@ -104,11 +119,18 @@ static bool matches_tracked_rtld_error(const char *text);
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error);
|
||||
static void handle_pre_pause_failure(const char *reason);
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us);
|
||||
static void handle_pre_pause_failure(const char *reason,
|
||||
mdbg_failure_kind_t kind);
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us,
|
||||
mdbg_failure_kind_t kind);
|
||||
static void append_fatal_error_detail(const char *detail);
|
||||
static void parse_fatal_error_pid(const char *line);
|
||||
static void finish_fatal_error(uint64_t now_us);
|
||||
static void process_log_line(const char *line, uint64_t now_us);
|
||||
static void flush_log_line(uint64_t now_us);
|
||||
static void append_log_char(char ch, uint64_t now_us);
|
||||
static void poll_log_monitor(uint64_t now_us);
|
||||
static void drain_log_monitor_before_clear(uint64_t now_us);
|
||||
static void start_log_monitoring(void);
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
|
||||
|
||||
@@ -179,7 +201,7 @@ static int query_mdbg_flags(pid_t pid, uint64_t *flags_out) {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
int ret = mdbg_call_raw(pid, MDBG_SUBCMD_FLAGS, 0, &status, &value);
|
||||
if (ret < 0)
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
if (status != 0)
|
||||
return (int)status;
|
||||
@@ -197,9 +219,17 @@ static void reset_log_line_buffer(void) {
|
||||
g_mdbg.log_line[0] = '\0';
|
||||
}
|
||||
|
||||
static void reset_fatal_error_buffer(void) {
|
||||
g_mdbg.fatal_error_active = false;
|
||||
g_mdbg.fatal_error_length = 0;
|
||||
g_mdbg.fatal_error_pid = 0;
|
||||
g_mdbg.fatal_error[0] = '\0';
|
||||
}
|
||||
|
||||
static void reset_log_snapshot(void) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
reset_log_line_buffer();
|
||||
reset_fatal_error_buffer();
|
||||
}
|
||||
|
||||
static void free_log_buffers(void) {
|
||||
@@ -344,14 +374,15 @@ static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
open_paren ? strchr(open_paren + 1, ')') : NULL;
|
||||
if (open_paren && close_paren && close_paren > open_paren + 1) {
|
||||
int written = snprintf(summary_out, summary_out_size,
|
||||
"can't load module %.*s after KStuff pause",
|
||||
sm_l10n_get(SM_L10N_RTLD_MODULE_AFTER_KSTUFF),
|
||||
(int)(close_paren - open_paren - 1),
|
||||
open_paren + 1);
|
||||
if (written > 0 && (size_t)written < summary_out_size)
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, "can't load module after KStuff pause",
|
||||
(void)strlcpy(summary_out,
|
||||
sm_l10n_get(SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK),
|
||||
summary_out_size);
|
||||
return;
|
||||
}
|
||||
@@ -359,19 +390,25 @@ static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
(void)strlcpy(summary_out, reason, summary_out_size);
|
||||
}
|
||||
|
||||
static void handle_pre_pause_failure(const char *reason) {
|
||||
static void handle_pre_pause_failure(const char *reason,
|
||||
mdbg_failure_kind_t kind) {
|
||||
log_debug(" [MDBG] %s crashed before kstuff auto-pause%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed before KStuff pause: %s. KStuff is not to "
|
||||
"blame.",
|
||||
g_mdbg.game.title_id);
|
||||
if (kind == MDBG_FAILURE_FATAL_SIGNAL) {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_BEFORE_KSTUFF_FATAL,
|
||||
g_mdbg.game.title_id, reason);
|
||||
} else {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_BEFORE_KSTUFF,
|
||||
g_mdbg.game.title_id);
|
||||
}
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us,
|
||||
mdbg_failure_kind_t kind) {
|
||||
if (!g_mdbg.game.pause_seen || g_mdbg.game.pause_time_us == 0 ||
|
||||
now_us < g_mdbg.game.pause_time_us) {
|
||||
handle_pre_pause_failure(reason);
|
||||
handle_pre_pause_failure(reason, kind);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -380,14 +417,20 @@ static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
log_debug(" [MDBG] %s crashed %us after kstuff pause; autotune skipped%s%s",
|
||||
g_mdbg.game.title_id, (unsigned)(post_pause_us / 1000000ull),
|
||||
reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed after KStuff pause: %s. Autotune skipped.",
|
||||
g_mdbg.game.title_id);
|
||||
if (kind == MDBG_FAILURE_FATAL_SIGNAL) {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED,
|
||||
g_mdbg.game.title_id, reason);
|
||||
} else {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED,
|
||||
g_mdbg.game.title_id);
|
||||
}
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
char reason_summary[128];
|
||||
bool is_rtld_error = matches_tracked_rtld_error(reason);
|
||||
bool is_rtld_error = kind == MDBG_FAILURE_RTLD;
|
||||
bool is_fatal_error = kind == MDBG_FAILURE_FATAL_SIGNAL;
|
||||
summarize_failure_reason(reason, reason_summary, sizeof(reason_summary),
|
||||
is_rtld_error);
|
||||
|
||||
@@ -400,16 +443,19 @@ static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
if (reason_summary[0] != '\0')
|
||||
log_debug(" [MDBG] autotune trigger: %s", reason_summary);
|
||||
if (is_rtld_error) {
|
||||
notify_system_info("%s: %s. Delay increased to %us.\nLaunch the game again.",
|
||||
g_mdbg.game.title_id,
|
||||
reason_summary[0] != '\0' ? reason_summary
|
||||
: "Can't load module after "
|
||||
"KStuff pause",
|
||||
tuned_delay_seconds);
|
||||
notify_system_info_l10n(
|
||||
SM_L10N_DELAY_INCREASED_RELAUNCH, g_mdbg.game.title_id,
|
||||
reason_summary[0] != '\0'
|
||||
? reason_summary
|
||||
: sm_l10n_get(SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK),
|
||||
tuned_delay_seconds);
|
||||
} else if (is_fatal_error) {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_FATAL_DELAY_INCREASED,
|
||||
g_mdbg.game.title_id, reason,
|
||||
tuned_delay_seconds);
|
||||
} else {
|
||||
notify_system_info("Crash detected after KStuff pause: pause delay for %s "
|
||||
"increased to %us. Launch the game again.",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
notify_system_info_l10n(SM_L10N_CRASH_DELAY_INCREASED,
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
}
|
||||
clear_tracked_game();
|
||||
return;
|
||||
@@ -417,18 +463,114 @@ static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
|
||||
log_debug(" [MDBG] failed to persist autotune pause delay for %s%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("Crash detected after KStuff pause: %s. Failed to update "
|
||||
"autotune delay.",
|
||||
g_mdbg.game.title_id);
|
||||
if (is_fatal_error) {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED,
|
||||
g_mdbg.game.title_id, reason);
|
||||
} else {
|
||||
notify_system_info_l10n(SM_L10N_CRASH_DELAY_UPDATE_FAILED,
|
||||
g_mdbg.game.title_id);
|
||||
}
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void append_fatal_error_detail(const char *detail) {
|
||||
if (!detail || detail[0] == '\0' ||
|
||||
g_mdbg.fatal_error_length + 1u >= sizeof(g_mdbg.fatal_error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t remaining = sizeof(g_mdbg.fatal_error) - g_mdbg.fatal_error_length;
|
||||
int written = snprintf(g_mdbg.fatal_error + g_mdbg.fatal_error_length,
|
||||
remaining, "%s%s",
|
||||
g_mdbg.fatal_error_length != 0 ? "\n" : "",
|
||||
detail);
|
||||
if (written < 0)
|
||||
return;
|
||||
if ((size_t)written >= remaining) {
|
||||
g_mdbg.fatal_error_length = sizeof(g_mdbg.fatal_error) - 1u;
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.fatal_error_length += (size_t)written;
|
||||
}
|
||||
|
||||
static void parse_fatal_error_pid(const char *line) {
|
||||
const char *value = line + strlen(MDBG_FATAL_PROCESS_ID_PREFIX);
|
||||
char *end = NULL;
|
||||
long parsed = strtol(value, &end, 10);
|
||||
pid_t pid = (pid_t)parsed;
|
||||
|
||||
if (end == value || end[0] != '\0' || pid <= 0 || (long)pid != parsed)
|
||||
return;
|
||||
|
||||
g_mdbg.fatal_error_pid = pid;
|
||||
}
|
||||
|
||||
static void finish_fatal_error(uint64_t now_us) {
|
||||
if (!g_mdbg.fatal_error_active)
|
||||
return;
|
||||
|
||||
g_mdbg.fatal_error_active = false;
|
||||
if (g_mdbg.fatal_error_length == 0) {
|
||||
reset_fatal_error_buffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.fatal_error_pid != g_mdbg.game.pid) {
|
||||
log_debug(" [MDBG] ignoring fatal error for pid=%ld while tracking pid=%ld",
|
||||
(long)g_mdbg.fatal_error_pid, (long)g_mdbg.game.pid);
|
||||
reset_fatal_error_buffer();
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] fatal error for %s:\n%s", g_mdbg.game.title_id,
|
||||
g_mdbg.fatal_error);
|
||||
handle_post_pause_failure(g_mdbg.fatal_error, now_us,
|
||||
MDBG_FAILURE_FATAL_SIGNAL);
|
||||
}
|
||||
|
||||
static void process_log_line(const char *line, uint64_t now_us) {
|
||||
if (!strncmp(line, MDBG_FATAL_EXCEPTION_PREFIX,
|
||||
strlen(MDBG_FATAL_EXCEPTION_PREFIX))) {
|
||||
reset_fatal_error_buffer();
|
||||
g_mdbg.fatal_error_active = true;
|
||||
append_fatal_error_detail(line + strlen(MDBG_FATAL_EXCEPTION_PREFIX));
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.fatal_error_active) {
|
||||
if (line[0] != '#')
|
||||
return;
|
||||
|
||||
if (!strcmp(line, "#")) {
|
||||
finish_fatal_error(now_us);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!strncmp(line, MDBG_FATAL_THREAD_NAME_PREFIX,
|
||||
strlen(MDBG_FATAL_THREAD_NAME_PREFIX))) {
|
||||
append_fatal_error_detail(line + 2);
|
||||
} else if (!strncmp(line, MDBG_FATAL_PROCESS_ID_PREFIX,
|
||||
strlen(MDBG_FATAL_PROCESS_ID_PREFIX))) {
|
||||
parse_fatal_error_pid(line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matches_tracked_rtld_error(line))
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] log load error for %s: %s", g_mdbg.game.title_id, line);
|
||||
handle_post_pause_failure(line, now_us);
|
||||
handle_post_pause_failure(line, now_us, MDBG_FAILURE_RTLD);
|
||||
}
|
||||
|
||||
static void flush_log_line(uint64_t now_us) {
|
||||
if (g_mdbg.log_line_length == 0)
|
||||
return;
|
||||
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void append_log_char(char ch, uint64_t now_us) {
|
||||
@@ -436,18 +578,14 @@ static void append_log_char(char ch, uint64_t now_us) {
|
||||
return;
|
||||
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
if (g_mdbg.log_line_length == 0)
|
||||
return;
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
flush_log_line(now_us);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.log_line_length + 1u >= sizeof(g_mdbg.log_line)) {
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
flush_log_line(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.log_line[g_mdbg.log_line_length++] = ch;
|
||||
@@ -474,6 +612,11 @@ static void poll_log_monitor(uint64_t now_us) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text_len == g_mdbg.log_snapshot_length &&
|
||||
(text_len == 0 || !memcmp(g_mdbg.log_snapshot, text, text_len))) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t skip = 0;
|
||||
if (g_mdbg.log_snapshot_length != 0 && text_len >= g_mdbg.log_snapshot_length &&
|
||||
!memcmp(g_mdbg.log_snapshot, text, g_mdbg.log_snapshot_length)) {
|
||||
@@ -484,6 +627,7 @@ static void poll_log_monitor(uint64_t now_us) {
|
||||
log_debug(" [MDBG] log stream reset or truncated for %s",
|
||||
g_mdbg.game.title_id);
|
||||
reset_log_line_buffer();
|
||||
reset_fatal_error_buffer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +640,18 @@ static void poll_log_monitor(uint64_t now_us) {
|
||||
update_log_snapshot(text, text_len);
|
||||
}
|
||||
|
||||
static void drain_log_monitor_before_clear(uint64_t now_us) {
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
flush_log_line(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
finish_fatal_error(now_us);
|
||||
}
|
||||
|
||||
static void start_log_monitoring(void) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
@@ -525,6 +681,7 @@ static void start_log_monitoring(void) {
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
reset_log_line_buffer();
|
||||
reset_fatal_error_buffer();
|
||||
}
|
||||
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
|
||||
@@ -534,7 +691,7 @@ static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
|
||||
log_debug(" [MDBG] crash-candidate: %s pid=%ld flags=0x%08" PRIx64,
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid, flags);
|
||||
|
||||
handle_post_pause_failure("crash-candidate", now_us);
|
||||
handle_post_pause_failure("crash-candidate", now_us, MDBG_FAILURE_GENERIC);
|
||||
}
|
||||
|
||||
void sm_mdbg_init(void) {
|
||||
@@ -577,6 +734,7 @@ void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id) {
|
||||
|
||||
log_debug(" [MDBG] tracking crash-candidate state: %s pid=%ld app_id=0x%08X",
|
||||
g_mdbg.game.title_id, (long)pid, app_id);
|
||||
start_log_monitoring();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
@@ -584,20 +742,29 @@ void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
if (g_mdbg.game.log_monitoring_active) {
|
||||
poll_log_monitor(pause_time_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.game.pause_seen = true;
|
||||
g_mdbg.game.pause_time_us = pause_time_us;
|
||||
g_mdbg.game.monitor_deadline_us =
|
||||
g_mdbg.game.pause_time_us + MDBG_AUTOTUNE_WINDOW_US;
|
||||
g_mdbg.game.pause_delay_seconds = pause_delay_seconds;
|
||||
g_mdbg.game.next_poll_us = pause_time_us;
|
||||
start_log_monitoring();
|
||||
if (!g_mdbg.game.log_monitoring_active)
|
||||
start_log_monitoring();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exit(pid_t pid) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
clear_tracked_game();
|
||||
drain_log_monitor_before_clear(monotonic_time_us());
|
||||
if (g_mdbg.game.active)
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_shutdown(void) {
|
||||
@@ -642,16 +809,23 @@ void sm_mdbg_poll(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
if (g_mdbg.game.pause_seen || g_mdbg.fatal_error_active) {
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.game.next_poll_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
|
||||
if (g_mdbg.fatal_error_active)
|
||||
return;
|
||||
|
||||
uint64_t flags = 0;
|
||||
int ret = query_mdbg_flags(g_mdbg.game.pid, &flags);
|
||||
if (is_mdbg_process_gone_error(ret)) {
|
||||
clear_tracked_game();
|
||||
drain_log_monitor_before_clear(now_us);
|
||||
if (g_mdbg.game.active)
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
if (ret != 0)
|
||||
@@ -660,5 +834,11 @@ void sm_mdbg_poll(void) {
|
||||
if ((flags & MDBG_FLAG_EXCEPTION_STOP) == 0)
|
||||
return;
|
||||
|
||||
if (!g_mdbg.game.pause_seen) {
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active || g_mdbg.fatal_error_active)
|
||||
return;
|
||||
}
|
||||
|
||||
handle_crash_candidate(flags, now_us);
|
||||
}
|
||||
+209
-44
@@ -6,7 +6,6 @@
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_mount_defs.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_stability.h"
|
||||
|
||||
const char *attach_backend_name(attach_backend_t backend) {
|
||||
@@ -20,8 +19,13 @@ const char *attach_backend_name(attach_backend_t backend) {
|
||||
// --- Device Node Wait and Source Stability ---
|
||||
bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
|
||||
if (path_exists(devname) == should_exist)
|
||||
struct stat st;
|
||||
if (stat(devname, &st) == 0) {
|
||||
if (should_exist)
|
||||
return true;
|
||||
} else if (!should_exist && (errno == ENOENT || errno == ENOTDIR)) {
|
||||
return true;
|
||||
}
|
||||
sceKernelUsleep(LVD_NODE_WAIT_US);
|
||||
}
|
||||
|
||||
@@ -105,7 +109,18 @@ bool resolve_device_from_mount(const char *mount_point,
|
||||
|
||||
static bool is_path_mountpoint(const char *path) {
|
||||
struct statfs sfs;
|
||||
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
|
||||
if (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0)
|
||||
return true;
|
||||
|
||||
// An unreadable filesystem can make statfs(path) fail while it is still in
|
||||
// the mount table. Treat it as active until unmount removes that entry.
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
for (int i = 0; i < mntcount && mntbuf; ++i) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, path) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_active_image_mount_point(const char *path) {
|
||||
@@ -161,9 +176,165 @@ bool wait_for_lvd_release(void) {
|
||||
}
|
||||
|
||||
// --- Device Detach Helpers ---
|
||||
static bool detach_lvd_unit(int unit_id) {
|
||||
if (unit_id < 0)
|
||||
static void build_attached_unit_devname(attach_backend_t backend, int unit_id,
|
||||
char devname[64]) {
|
||||
const char *prefix = backend == ATTACH_BACKEND_MD ? "/dev/md" : "/dev/lvd";
|
||||
snprintf(devname, 64, "%s%d", prefix, unit_id);
|
||||
}
|
||||
|
||||
static bool read_device_node_identity(
|
||||
const char *devname, attached_unit_detach_state_t *state) {
|
||||
struct stat st;
|
||||
if (stat(devname, &st) != 0)
|
||||
return false;
|
||||
|
||||
state->node_identity_valid = true;
|
||||
state->node_device = (uint64_t)st.st_dev;
|
||||
state->node_inode = (uint64_t)st.st_ino;
|
||||
state->node_rdev = (uint64_t)st.st_rdev;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool device_node_is_mounted(const char *devname,
|
||||
char mount_point[MNAMELEN]) {
|
||||
struct statfs *mounts = NULL;
|
||||
int mount_count = getmntinfo(&mounts, MNT_NOWAIT);
|
||||
for (int i = 0; i < mount_count && mounts; ++i) {
|
||||
if (strcmp(mounts[i].f_mntfromname, devname) != 0)
|
||||
continue;
|
||||
if (mount_point)
|
||||
(void)strlcpy(mount_point, mounts[i].f_mntonname, MNAMELEN);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool detach_target_is_released_or_reused(
|
||||
attach_backend_t backend, int unit_id, const char *devname,
|
||||
const attached_unit_detach_state_t *state) {
|
||||
if (!state || !state->node_identity_valid) {
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(devname, &st) != 0) {
|
||||
if (errno != ENOENT && errno != ENOTDIR)
|
||||
return false;
|
||||
log_debug(" [IMG][%s] detach target released: unit=%d node removed",
|
||||
attach_backend_name(backend), unit_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state->node_device != (uint64_t)st.st_dev ||
|
||||
state->node_inode != (uint64_t)st.st_ino ||
|
||||
state->node_rdev != (uint64_t)st.st_rdev) {
|
||||
log_debug(" [IMG][%s] detach target released: unit=%d node identity "
|
||||
"changed",
|
||||
attach_backend_name(backend), unit_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
char mount_point[MNAMELEN];
|
||||
if (device_node_is_mounted(devname, mount_point)) {
|
||||
log_debug(" [IMG][%s] detach target released: unit=%d reused at %s",
|
||||
attach_backend_name(backend), unit_id, mount_point);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
ATTACHED_UNIT_DETACH_COMPLETE = 0,
|
||||
ATTACHED_UNIT_DETACH_REQUEST,
|
||||
ATTACHED_UNIT_DETACH_PENDING,
|
||||
} attached_unit_detach_prepare_t;
|
||||
|
||||
static attached_unit_detach_prepare_t prepare_attached_unit_detach(
|
||||
attach_backend_t backend, int unit_id,
|
||||
const attached_unit_detach_state_t *state, char devname[64],
|
||||
attached_unit_detach_state_t *request_state) {
|
||||
build_attached_unit_devname(backend, unit_id, devname);
|
||||
*request_state = *state;
|
||||
if (!request_state->node_identity_valid &&
|
||||
!read_device_node_identity(devname, request_state)) {
|
||||
log_debug(" [IMG][%s] detach deferred without device identity: unit=%d",
|
||||
attach_backend_name(backend), unit_id);
|
||||
errno = EAGAIN;
|
||||
return ATTACHED_UNIT_DETACH_PENDING;
|
||||
}
|
||||
|
||||
// A freshly attached unit may not have published its device node yet. An
|
||||
// absent node is only proof of release after the node identity was observed
|
||||
// or a detach request was accepted.
|
||||
if (detach_target_is_released_or_reused(backend, unit_id, devname,
|
||||
request_state)) {
|
||||
return ATTACHED_UNIT_DETACH_COMPLETE;
|
||||
}
|
||||
|
||||
if (request_state->requested) {
|
||||
errno = EINPROGRESS;
|
||||
return ATTACHED_UNIT_DETACH_PENDING;
|
||||
}
|
||||
|
||||
return ATTACHED_UNIT_DETACH_REQUEST;
|
||||
}
|
||||
|
||||
bool wait_for_attached_unit_release(
|
||||
attach_backend_t backend, int unit_id,
|
||||
const attached_unit_detach_state_t *state) {
|
||||
if (!state || !state->node_identity_valid) {
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
char devname[64];
|
||||
build_attached_unit_devname(backend, unit_id, devname);
|
||||
if (wait_for_dev_node_state(devname, false))
|
||||
return true;
|
||||
if (detach_target_is_released_or_reused(backend, unit_id, devname, state))
|
||||
return true;
|
||||
|
||||
log_debug(" [IMG][%s] detach pending after timeout: unit=%d node=%s",
|
||||
attach_backend_name(backend), unit_id, devname);
|
||||
errno = EINPROGRESS;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool finish_accepted_detach(
|
||||
attached_unit_detach_state_t *state,
|
||||
attached_unit_detach_state_t *request_state) {
|
||||
request_state->requested = true;
|
||||
*state = *request_state;
|
||||
errno = EINPROGRESS;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool finish_failed_detach(attach_backend_t backend, int unit_id,
|
||||
const char *devname,
|
||||
const attached_unit_detach_state_t *state,
|
||||
int detach_errno) {
|
||||
// Before a detach request is accepted, an unobserved node may still belong
|
||||
// to the freshly attached unit. Keep it queued when the ioctl itself fails.
|
||||
if (state->node_identity_valid &&
|
||||
detach_target_is_released_or_reused(backend, unit_id, devname, state)) {
|
||||
return true;
|
||||
}
|
||||
errno = detach_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool detach_lvd_unit(int unit_id,
|
||||
attached_unit_detach_state_t *state) {
|
||||
char devname[64];
|
||||
attached_unit_detach_state_t request_state;
|
||||
attached_unit_detach_prepare_t prepare = prepare_attached_unit_detach(
|
||||
ATTACH_BACKEND_LVD, unit_id, state, devname, &request_state);
|
||||
if (prepare == ATTACHED_UNIT_DETACH_COMPLETE)
|
||||
return true;
|
||||
if (prepare == ATTACHED_UNIT_DETACH_PENDING)
|
||||
return false;
|
||||
|
||||
int fd = open(LVD_CTRL_PATH, O_RDWR);
|
||||
if (fd < 0) {
|
||||
@@ -177,27 +348,30 @@ static bool detach_lvd_unit(int unit_id) {
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.device_id = unit_id;
|
||||
|
||||
bool ok = true;
|
||||
if (ioctl(fd, SCE_LVD_IOC_DETACH, &req) != 0) {
|
||||
int detach_errno = errno;
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id, strerror(errno));
|
||||
ok = false;
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id,
|
||||
strerror(detach_errno));
|
||||
close(fd);
|
||||
return finish_failed_detach(ATTACH_BACKEND_LVD, unit_id, devname,
|
||||
&request_state, detach_errno);
|
||||
}
|
||||
close(fd);
|
||||
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "/dev/lvd%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname, false)) {
|
||||
log_debug(" [IMG][%s] device node still present after detach: /dev/lvd%d",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
return finish_accepted_detach(state, &request_state);
|
||||
}
|
||||
|
||||
static bool detach_md_unit(int unit_id) {
|
||||
if (unit_id < 0)
|
||||
static bool detach_md_unit(int unit_id,
|
||||
attached_unit_detach_state_t *state) {
|
||||
char devname[64];
|
||||
attached_unit_detach_state_t request_state;
|
||||
attached_unit_detach_prepare_t prepare = prepare_attached_unit_detach(
|
||||
ATTACH_BACKEND_MD, unit_id, state, devname, &request_state);
|
||||
if (prepare == ATTACHED_UNIT_DETACH_COMPLETE)
|
||||
return true;
|
||||
if (prepare == ATTACHED_UNIT_DETACH_PENDING)
|
||||
return false;
|
||||
|
||||
int fd = open(MD_CTRL_PATH, O_RDWR);
|
||||
if (fd < 0) {
|
||||
@@ -212,37 +386,28 @@ static bool detach_md_unit(int unit_id) {
|
||||
req.md_version = MDIOVERSION;
|
||||
req.md_unit = (unsigned int)unit_id;
|
||||
|
||||
bool ok = true;
|
||||
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
|
||||
int err = errno;
|
||||
req.md_options = MD_FORCE;
|
||||
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(errno));
|
||||
ok = false;
|
||||
} else {
|
||||
log_debug(" [IMG][%s] detach %d forced after error: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(err));
|
||||
}
|
||||
int detach_errno = errno;
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(detach_errno));
|
||||
close(fd);
|
||||
return finish_failed_detach(ATTACH_BACKEND_MD, unit_id, devname,
|
||||
&request_state, detach_errno);
|
||||
}
|
||||
close(fd);
|
||||
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "/dev/md%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname, false)) {
|
||||
log_debug(" [IMG][%s] device node still present after detach: /dev/md%d",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
return finish_accepted_detach(state, &request_state);
|
||||
}
|
||||
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id) {
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id,
|
||||
attached_unit_detach_state_t *state) {
|
||||
if (!state || unit_id < 0 ||
|
||||
(backend != ATTACH_BACKEND_MD && backend != ATTACH_BACKEND_LVD)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return detach_md_unit(unit_id);
|
||||
else if (backend == ATTACH_BACKEND_LVD)
|
||||
return detach_lvd_unit(unit_id);
|
||||
return true;
|
||||
return detach_md_unit(unit_id, state);
|
||||
return detach_lvd_unit(unit_id, state);
|
||||
}
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user