Compare commits

...
129 Commits
Author SHA1 Message Date
Roman Tarasov 1cc20f44af Remove deprecated KStuff pause/autotune support 2026-06-21 01:54:55 +03:00
Roman Tarasov 2eb9164a51 Add configurable notification language 2026-06-21 01:30:20 +03:00
Drakmor 98bf401481 Merge pull request #62 from emald6969/patch-3
Update fr_fr.inc
2026-06-07 15:05:07 +03:00
Drakmor 6dc8059ada Merge pull request #65 from emald6969/patch-6
Update fr_ca.inc
2026-06-07 15:04:43 +03:00
emald6969 cf7ef3517a Update fr_ca.inc 2026-06-04 23:07:16 +01:00
emald6969 5b2e057295 Update fr_fr.inc
fixed a typo sorry
2026-06-04 20:20:38 +01:00
Drakmor 1b5d178889 Merge pull request #60 from PSBrew/l10n
l10n: Brazilian Portuguese (pt-BR) translation review
2026-06-04 09:46:42 +03:00
Renan 2622818dd5 l10n: Brazilian Portuguese (pt-BR) translation review 2026-06-03 23:22:43 -03:00
Renan 0a3c380766 l10n: Brazilian Portuguese (pt-BR) translation review 2026-06-03 23:17:05 -03:00
Drakmor c05ff8899b Merge pull request #58 from emald6969/patch-1
Update ar_sa.inc
2026-06-03 23:51:07 +03:00
emald6969 8f89369449 Update ar_sa.inc 2026-06-03 21:33:03 +01:00
Roman Tarasov 31181f2d60 Add localized notifications 2026-06-03 22:44:37 +03:00
Roman Tarasov f20b97db65 funding 2026-06-03 17:53:44 +03:00
Roman Tarasov 9d6ab61369 Added description of compressed PFS format 2026-06-03 17:49:43 +03:00
Roman Tarasov 25182da557 Fix PFS mount 2026-06-03 16:09:11 +03:00
Drakmor 72cf4460a9 Merge pull request #55 from drakmor/alert-autofix-1
Potential fix for code scanning alert no. 1: Workflow does not contain permissions
2026-06-03 02:26:06 +03:00
DrakmorandCopilot Autofix powered by AI cc0f4e0684 Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-03 01:49:51 +03:00
Roman Tarasov 8cdcd112f5 Nested images 2026-06-03 01:34:23 +03:00
Roman Tarasov c41c3eeba8 Attempting to restore occupied mount points for nullfs 2026-06-03 01:19:38 +03:00
Drakmor 3d6a300742 Update README.md 2026-06-02 11:51:43 +03:00
Roman Tarasov 51d01011ff Fixed mounting of exfat 2026-06-02 10:14:47 +03:00
Roman Tarasov 9e803cff7f Added support for compressed PFS 2026-06-01 21:28:57 +03:00
Roman Tarasov dee175e2bd Added copying of trophy data 2026-06-01 01:12:30 +03:00
Roman Tarasov 860e9de3b1 Fix duplicate uninstall requests during scan cleanup 2026-06-01 00:05:13 +03:00
Roman Tarasov e0b41b3ca6 Fix cleanup for non-uninstallable app.db entries 2026-05-30 17:00:11 +03:00
Roman Tarasov 5c4470cdcd Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-05-30 11:13:24 +03:00
Roman Tarasov 5b6be76272 Fix same-session manual reinstalls by clearing stale install/register state and refreshing app.db before manual scans 2026-05-30 11:12:44 +03:00
Roman Tarasov c66bff6d2a Prevent kstuff auto-resume during shutdown 2026-05-30 10:59:17 +03:00
Drakmor 2df61d7e2d Update README.md 2026-05-25 22:51:30 +03:00
Drakmor e960fc8079 Update README.md 2026-05-25 22:51:03 +03:00
Drakmor d1c6978e78 Update README.md 2026-05-25 22:44:36 +03:00
Roman Tarasov 4eadb06f57 Add manual install list support 2026-05-25 18:00:14 +03:00
Drakmor 78f536b41a maximum mounted images 64->256 2026-05-23 18:47:58 +03:00
Drakmor 99e000eed9 Update README.md
kstuff link
2026-05-23 16:17:25 +03:00
Drakmor 8622d8757f Update README.md 2026-05-14 11:55:28 +03:00
Roman Tarasov bcffa18997 Redice poll STOP file 2026-05-13 05:24:52 +03:00
Roman Tarasov e00dda0075 Lower default scan interval to 15 seconds 2026-05-13 05:21:47 +03:00
Roman Tarasov 315265f5db Code review 2026-05-13 05:19:22 +03:00
Drakmor 30b04358f4 Update README.md 2026-05-10 19:51:33 +03:00
Roman Tarasov a6ad4922a9 Support link 2026-05-10 19:50:17 +03:00
Roman Tarasov ebd649e0ea Add more detailed diagnostics when entering sleep 2026-05-10 19:45:07 +03:00
Roman Tarasov 4072ced15d Pause kstuff during sleep 2026-05-10 19:44:53 +03:00
Roman Tarasov fee1029b0a Enter sleep mode earlier 2026-05-10 19:18:32 +03:00
Roman Tarasov cbd0fce4f9 Fix snd0Info paths 2026-05-10 18:24:29 +03:00
Roman Tarasov 9fa6ad94de Handle suspend sleep mode and USB cleanup. Thx to @hadobedo 2026-05-10 16:01:59 +03:00
Roman TarasovandCopilot d4c4cda739 kstuff autopause 12.70-13.20
Co-authored-by: Copilot <copilot@github.com>
2026-05-10 12:31:15 +03:00
Roman Tarasov 954b3a7d17 Fix discovery after removable drive changes 2026-05-10 12:21:09 +03:00
Roman Tarasov 3652f506c0 Add global fakelib overlay support 2026-05-10 11:50:18 +03:00
Roman Tarasov 6c188a03bb typo 2026-05-10 10:09:47 +03:00
Roman Tarasov 2680ed28d7 ci/cd sdk 2026-04-17 02:44:55 +03:00
Roman Tarasov f401660113 stub path 2026-04-17 02:39:45 +03:00
Roman Tarasov 11a135cf1c typo 2026-04-17 02:35:15 +03:00
Roman Tarasov 146942c138 mdbg: simplify crash monitor and log snapshot handling 2026-04-17 02:30:07 +03:00
Roman Tarasov a8f1a216bd mdbg: switch klog monitoring to debug log snapshots 2026-04-17 01:51:45 +03:00
Roman Tarasov c37088543d /dev/klog reopen 2026-04-16 02:36:49 +03:00
Roman Tarasov 059f8b40f1 Rework crash detection with sm_mdbg and post-pause autotune 2026-04-16 01:50:51 +03:00
Roman Tarasov 6f61e8d29e Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-04-15 00:09:45 +03:00
Roman Tarasov a5471da456 Stop background full scans while a game is running 2026-04-15 00:09:39 +03:00
Roman Tarasov 0abfecd3e7 Rework backport overlay resolution and mount handling 2026-04-14 23:52:09 +03:00
Drakmor 9f1786905c Use SDK ps5-payload-dev/pacbrew-repo 2026-04-05 10:50:53 +03:00
Roman Tarasov fa714f868f Dynamically load AppInstallTitleDir on FW below 12 2026-04-05 10:40:50 +03:00
Roman Tarasov 17bb561e8d Only log pre-scan cleanup during the initial startup scan 2026-04-04 23:23:47 +03:00
Roman Tarasov 0efeec16fa Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-04-04 22:55:09 +03:00
Roman Tarasov a584121d76 Create config.ini from bundled template when missing 2026-04-04 22:55:03 +03:00
Roman Tarasov 45da514549 Increase default scan interval to 60 seconds and clarify config example 2026-04-04 22:42:32 +03:00
Roman Tarasov e272c9736c Remove kstuff noautomount sentinel files on shutdown 2026-04-04 21:57:48 +03:00
Roman Tarasov 902a6917df Removed the extra log messages 2026-04-04 21:55:07 +03:00
Roman Tarasov 6df8a472e3 Add optional AppInstallAll mode with FW 12+ override 2026-04-04 21:53:27 +03:00
Roman Tarasov 4e9afa7885 Fast shutdown 2026-04-04 21:27:10 +03:00
Drakmor 40967215b4 Update README.md 2026-03-30 01:39:04 +03:00
Roman Tarasov a23822a26f Disabled crash detection by default, increased kstuff pause timeouts 2026-03-30 01:25:26 +03:00
Roman Tarasov 97ffedf0df Create target /system_ex/app mount directory before nullfs mount 2026-03-29 21:38:09 +03:00
Roman Tarasov bdd3083d65 Temporary use of the EchoStretch repository 2026-03-29 20:15:50 +03:00
Roman Tarasov bcb906e5b6 Add per-image sector-size overrides with autotune on cluster-size mount errors 2026-03-29 16:32:28 +03:00
Roman Tarasov 47a033b50b Make kstuff crash detection optional and reset heuristic state on config reload 2026-03-29 16:05:33 +03:00
Roman Tarasov 47bded61da Restore missing appmeta on remount and replace access checks with stat 2026-03-29 15:49:03 +03:00
Roman Tarasov 5d0ca6d5d1 Add kstuff crash autotune with autotune.ini pause-delay overrides 2026-03-29 03:27:43 +03:00
Roman Tarasov ebfe0376c6 Detect early post-kstuff game crashes 2026-03-29 03:01:38 +03:00
Roman Tarasov a6a5437a42 Reduce install retry log spam 2026-03-29 00:08:31 +03:00
Roman Tarasov 8c1ebbdf28 Wait for sce_sys stabilization before folder install 2026-03-28 23:19:54 +03:00
Roman Tarasov 853ff28b85 Typo and move release archive contents to a manifest file 2026-03-28 01:42:38 +03:00
Roman Tarasov e9098631c1 Add GitHub Actions workflow for PS5 build 2026-03-28 01:34:06 +03:00
Roman Tarasov bf24df419b Added 10.20 to 12.00 kstuff pause support 2026-03-28 01:04:03 +03:00
Roman Tarasov ff70cb1bc8 Deduplicate device node wait helper across image and mount code 2026-03-24 00:23:52 +03:00
Roman Tarasov 9254525a6c Tune release build flags 2026-03-23 23:54:29 +03:00
Roman Tarasov 26ba93c9aa Streamline sm_log synchronization and reuse the open log file 2026-03-23 23:50:40 +03:00
Roman Tarasov 43d61dce37 Move scan work buffers off the stack into reusable scanner workspace 2026-03-23 23:35:49 +03:00
Roman Tarasov 7759b83293 Fix scan-now request synchronization and simplify pending-state handling 2026-03-23 23:14:33 +03:00
Roman Tarasov 0eca3fa138 Fix kstuff auto-pause focus handling for initially-disabled state 2026-03-23 22:47:45 +03:00
Roman Tarasov e30a296a53 Add autopause.txt per-game delay overrides for kstuff auto-pause 2026-03-22 20:49:28 +03:00
Roman Tarasov b374c3163e Add runtime config hot-reload 2026-03-22 20:25:08 +03:00
Roman Tarasov a71a03d0db Probe kstuff availability at auto-pause time instead of startup 2026-03-22 19:20:21 +03:00
Roman Tarasov 56e8ba2185 Prefer valid installed sources when resolving duplicate title IDs 2026-03-22 19:04:55 +03:00
Roman Tarasov 18a97ecca9 Refactor directory candidate collection into focused scan helpers 2026-03-22 18:43:53 +03:00
Roman Tarasov 270264cc75 Detect unreadable image mounts and report them as damaged images 2026-03-22 18:28:36 +03:00
Roman Tarasov ef814ad56b Unify scan and watcher tree traversal with a shared scan walker 2026-03-22 17:54:58 +03:00
Roman Tarasov cec94656c6 Cache owning scan roots for game entries and simplify targeted backport reconciliation 2026-03-22 17:29:47 +03:00
Roman Tarasov 4ff9a3dffe Optimize scanner watcher registry with fd indexing and per-root watch lists 2026-03-22 17:21:15 +03:00
Roman Tarasov fe7dad099c Optimize scanner watcher rebuilds to refresh only affected subtrees 2026-03-22 16:20:26 +03:00
Roman Tarasov c02f237ddf Cache mounted games when app.db title list is temporarily unavailable 2026-03-22 15:53:02 +03:00
Roman Tarasov d318b5e607 Refactor scanning to use kevent-driven dirty roots, debounce filesystem events, run targeted per-root cleanup and discovery, and support bounded scan_depth traversal for nested directories and image files 2026-03-22 15:39:31 +03:00
Roman Tarasov 6f97cde602 Remove redundant internal guard checks 2026-03-22 13:56:09 +03:00
Roman Tarasov edcac6bdd7 Refactor scanning into a scanner service, replace main loop sleep polling with kevent/wake-pipe waiting, and keep polling as a runtime fallback 2026-03-22 13:13:52 +03:00
Roman Tarasov 27feba2085 Add bounded scan_depth with recursive_scan compatibility remap
Add scan_depth config, remap legacy recursive_scan=1 to scan_depth=2, and avoid duplicate traversal of nested scan roots
2026-03-22 12:43:55 +03:00
Roman Tarasov 82603fce7d Fixed waiting for images to stabilize 2026-03-22 12:09:05 +03:00
Roman Tarasov 01b64346c8 Strip all from elf 2026-03-22 12:07:43 +03:00
Roman Tarasov 7e48103149 Updated readme 2026-03-16 22:04:49 +03:00
Roman Tarasov 124e757199 Disable kstuff auto-pause when kstuff is not installed 2026-03-16 20:44:34 +03:00
Roman Tarasov 0fcc970a20 Create /data/.kstuff_noautomount 2026-03-16 01:18:56 +03:00
Roman Tarasov 7684e8431d Increased block size to 64kb for UFS 2026-03-15 14:28:24 +03:00
Roman Tarasov 3166434db1 The start notification has been fixed and its sound has been disabled. 2026-03-15 12:27:11 +03:00
Roman Tarasov c785daaddf Changed image mount directory to /mnt/shadowmnt 2026-03-14 23:41:28 +03:00
Roman Tarasov 2db6b49439 Fix /dev/lvd attach 2026-03-14 23:18:19 +03:00
Roman Tarasov aff2fc0e6c Remove stale duplicate-tracking entries when sources disappear 2026-03-14 20:08:29 +03:00
Roman Tarasov ae36dd5871 unionfs options 2026-03-13 02:57:57 +03:00
Roman Tarasov cc3e07d201 Update compatibility note in README.md 2026-03-10 10:24:30 +03:00
Roman Tarasov 2c03ede25c Update README.md 2026-03-10 02:43:18 +03:00
Roman Tarasov a212a883b3 1.6b4 2026-03-10 02:24:03 +03:00
Roman Tarasov 99a2956c28 upd .gitingore 2026-03-10 02:23:13 +03:00
Roman Tarasov 93071924b5 wait /dev/lvd1 -> /dev/lvd2 2026-03-04 23:47:40 +03:00
Roman Tarasov 47e47477d1 Add macOS exFAT image builder and improve LVD startup wait handling 2026-02-28 20:21:46 +03:00
Roman Tarasov 31c28e1896 Block mounting of images with too small a cluster size. 2026-02-27 22:20:16 +03:00
Roman Tarasov c1f34ffa22 Version bump 2026-02-27 01:09:34 +03:00
Roman Tarasov 851c269647 v 1.6beta2 Stabilization of registration
Wait for /dev/lvd1 to be freed to avoid remounting after closing y2bj
The number of game registration attempts has been increased to 2x
The backport is mounted only after successful registration to ensure its successful completion. If you delete a game with a backport, re-registration will only occur after a reboot.
2026-02-27 01:07:49 +03:00
Roman Tarasov c9b3415048 Fixed instant game mounting 2026-02-25 04:05:03 +03:00
Roman Tarasov 11b32a97cf 1.6test2 Restored nullfs mounting 2026-02-23 23:50:10 +03:00
Roman Tarasov a43401391d 1.6test1
Added backport support for images from /data/backports/<TitleID>.
Image mount folder layout has been standardized to /data/imgmnt/<FS>mnt/<filename>
2026-02-23 20:44:07 +03:00
Roman Tarasov 0f7bc24928 Add "force_mount" option 2026-02-23 02:35:29 +03:00
Roman Tarasov 1db534cf82 Update readme 2026-02-23 01:50:05 +03:00
98 changed files with 18830 additions and 3387 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
custom:
- https://github.com/drakmor/shadowMountPlus#support-development
+12
View File
@@ -0,0 +1,12 @@
# Files included in the release archive.
# One path per line, relative to the repository root.
shadowmountplus.elf
config.ini.example
LICENSE
make_image.bat
mkexfat.sh
mkexfat_macos.sh
mkufs2.sh
New-OsfExfatImage.ps1
README.md
+130
View File
@@ -0,0 +1,130 @@
name: PS5 Build and Release
on:
push:
paths-ignore:
- "**/*.md"
pull_request:
paths-ignore:
- "**/*.md"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
outputs:
archive_name: ${{ steps.package.outputs.archive_name }}
runs-on: ubuntu-latest
steps:
- name: Checkout Payload
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout pacbrew-repo
uses: actions/checkout@v4
with:
repository: ps5-payload-dev/pacbrew-repo
path: pacbrew-repo
- name: Checkout SDK stubs
uses: actions/checkout@v4
with:
repository: ps5-payload-dev/sdk
path: ps5-sdk
- name: Setup build environment
run: |
sudo apt update
sudo apt install -y \
autoconf \
automake \
build-essential \
clang-18 \
curl \
git \
libarchive-tools \
libtool \
lld-18 \
makepkg \
meson \
pacman-package-manager \
pkg-config \
xxd \
zip
- name: Build PS5 payload SDK
working-directory: pacbrew-repo/sdk
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: Build payload
run: |
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
- name: Create release archive
id: package
shell: bash
run: |
set -euo pipefail
version_tag="$(git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo "${GITHUB_SHA::7}")"
archive_name="ShadowMountPlus_${version_tag}.zip"
manifest_file=".github/release-files.txt"
mapfile -t release_files < <(grep -Ev '^[[:space:]]*(#|$)' "${manifest_file}")
if [ "${#release_files[@]}" -eq 0 ]; then
echo "Release manifest is empty: ${manifest_file}" >&2
exit 1
fi
for release_file in "${release_files[@]}"; do
if [ ! -f "${release_file}" ]; then
echo "Missing release file: ${release_file}" >&2
exit 1
fi
done
zip -9 "${archive_name}" "${release_files[@]}"
echo "archive_name=${archive_name}" >> "$GITHUB_OUTPUT"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.archive_name }}
path: ${{ steps.package.outputs.archive_name }}
if-no-files-found: error
release:
needs: build
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.archive_name }}
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: dist/${{ needs.build.outputs.archive_name }}
+3
View File
@@ -3,3 +3,6 @@
daemon.lock
debug.log
notify.txt
src/notify_icon_asset.c
refs/*
docs/*
+32 -7
View File
@@ -1,22 +1,47 @@
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)
# Standard Flags (No extra libraries)
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc
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 :=
LDFLAGS := -flto=thin -Wl,--gc-sections
# Standard Libraries Only
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
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)
HEADERS := $(wildcard include/*.h)
# Targets
all: shadowmountplus.elf
# Build Daemon
shadowmountplus.elf: src/main.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
rm -f src/notify_icon_asset.c src/config_ini_example_asset.c
$(KERNEL_SYS_STUB_SO): $(KERNEL_SYS_STUB_SRCS)
test -f "$(PS5_SCE_STUBS_DIR)/libkernel_sys.c"
$(CC) -shared -Wl,-soname=libkernel_sys.sprx -o $@ $^
src/notify_icon_asset.c: smp_icon.png
xxd -i $< > $@
src/config_ini_example_asset.c: config.ini.example
xxd -i $< > $@
src/%.o: src/%.c $(HEADERS)
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f shadowmountplus.elf kill.elf src/*.o
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
+237 -43
View File
@@ -1,16 +1,28 @@
# ShadowMountPlus (PS5)
**Version:** `1.5beta6`
**Repository:** https://github.com/drakmor/shadowMountPlus
**Discord:** https://discord.gg/x2Ppvzwjhm
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.7**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite](https://github.com/EchoStretch/kstuff-lite)**.
**Important:** This version no longer supports ShadowMount-managed Kstuff autopause. Use a current Kstuff-lite build; older Kstuff/autopause setups are not supported.
## 💜 Support Development
If you want to support this project, you can donate
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
## Current image support
@@ -19,34 +31,51 @@
| Extension | Mounted FS | Attach backend | Status |
| --- | --- | --- | --- |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Recommended |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Compatibility / external-drive-only titles |
| `.ffpfs` | `pfs` | `LVD` | Experimental |
| `.ffpfsc` | `pfs` container | `LVD` | Experimental container for nested images |
Notes:
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
- **exFAT is the preferred image filesystem, including on 4.xx firmware, with no known reboot/shutdown issues in typical use.**
- **UFS (`.ffpkg`) is the recommended image format for normal use.**
- **Use exFAT (`.exfat`) only for titles that need external-drive-style compatibility.**
- **When building exFAT images manually, keep the cluster size at `64 KB`; smaller clusters can reduce performance.**
## Recommended FS choice
- Prefer **exFAT (`.exfat`)** in most cases: it is generally more performant, case-insensitive, and does not have reboot/shutdown issues in typical use.
- Use **UFS (`.ffpkg`)** only when specifically needed for compatibility with your game/setup.
- Prefer **UFS (`.ffpkg`)** in most cases: this is the recommended default image format for ShadowMountPlus.
- Use **exFAT (`.exfat`)** only for games that do not work correctly unless they are handled like external-drive content.
- If you create an **exFAT (`.exfat`)** image manually, use a **`64 KB` cluster size**. Smaller clusters can cause a noticeable performance loss.
## Runtime config (`/data/shadowmount/config.ini`)
This file is optional. If it does not exist, built-in defaults are used.
This file is optional. If it does not exist, ShadowMountPlus creates it from the bundled `config.ini.example` template on startup and uses built-in defaults until you uncomment overrides.
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`)
- `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+`)
- `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)
- `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`)
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; 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`)
- `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`)
- `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)
- `/data/shadowmount/autotune.ini` may also provide image-sector overrides with highest priority:
- `image_sector=<image_filename>:<sector_size>`
- `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`)
@@ -62,18 +91,41 @@ Per-image mode override behavior:
- Example:
```ini
mount_read_only=1
image_rw=PPSA08766-Cocoon-01.004.000.ffpfs
image_rw=DIRT 5.exfat
image_rw=PPSA1234-my-image.ffpfs
image_rw=MYGame 123.exfat
image_ro=legacy_dump.ffpkg
image_sector=MYGame 123.exfat:65536
```
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.
Scan path behavior:
- If at least one `scanpath=...` is present, only those custom paths are used.
- `/data/imgmnt` is always added automatically, even with custom paths.
- With `recursive_scan=0` (default), only first-level subfolders are checked.
- With `recursive_scan=1`, subfolders are scanned recursively.
- Full scan loop runs every `scan_interval_seconds` (default: `10`).
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always added automatically, even with custom paths.
- With `scan_depth=1` (default), only first-level subfolders are checked.
- With `scan_depth=2`, one additional nested level is checked.
- 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`).
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
Backport overlay behavior:
- For each `scanpath`, use:
- `<scanpath>/backports/<TITLE_ID>/`
- 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.
- 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.
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
Validation:
- See `config.ini.example` for a ready-to-use template.
@@ -82,7 +134,11 @@ Validation:
Image mountpoints are created under:
`/data/imgmnt/<image_name>-<fs_suffix>`
`/mnt/shadowmnt/<image_name>_<hash>`
PFSC container mountpoints are created under:
`/mnt/shadowmnt/pfsc/<image_name>_<hash>`
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Game files must be placed at the image root.
@@ -90,6 +146,92 @@ Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Valid example: `/sce_sys/param.json` exists directly from image root.
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
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.
## Compressed PFS containers (`.ffpfsc`)
Compressed PFS mode is intended only for nested images. During packing, data is
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
as an outer container for another image rather than as a direct game-file
layout.
Recommended layouts:
- exFAT image inside compressed PFS.
- Uncompressed PFS image inside compressed PFS.
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
speed. Keep this in mind when choosing which games to pack. Games that read large
amounts of data or stream textures continuously may stutter.
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
PFS images.
### Packing an uncompressed PFS image into compressed PFS
First create an uncompressed nested PFS image:
```bash
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
'./PPSA07923/PPSA07923-app' \
'./pfs_image.dat'
```
Then pack the nested image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./pfs_image.dat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary nested image can be removed:
```bash
rm './pfs_image.dat'
```
### Packing an exFAT image into compressed PFS
First create a normal exFAT image using one of the methods from
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
extension, for example `PPSA12345.exfat`.
Linux:
```bash
chmod +x mkexfat.sh
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
```
Windows:
```cmd
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
```
The exFAT image must contain the game files at the image root, without an extra
top-level folder.
Then pack the exFAT image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./PPSA12345.exfat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary exFAT image can be removed:
```bash
rm './PPSA12345.exfat'
```
## Scan paths
Default scan locations:
@@ -104,22 +246,66 @@ Default scan locations:
- `/mnt/usb0` .. `/mnt/usb7`
- `/mnt/ext0`
- `/mnt/ext1`
- `/data/imgmnt` (mounted image content scan)
- `/mnt/shadowmnt/pfsc` (mounted PFSC container scan)
- `/mnt/shadowmnt` (mounted image content scan)
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
## Manual install list
For games that should not live under the normal scan paths, ShadowMountPlus also
checks:
`/data/shadowmount/manual.lst`
Add one source per line:
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
- Path to a supported image file: `.ffpkg`, `.exfat`, `.ffpfs`, or `.ffpfsc`.
- Empty lines and lines starting with `#` are ignored.
Example:
```text
/mnt/usb0/MyGames/PPSA12345
/mnt/usb0/images/PPSA54321.ffpkg
# /mnt/usb0/disabled/PPSA00000
```
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
rescans shortly after the write, mounts images when needed, and installs the
game through the same pipeline as normal scan path discoveries.
Manual install state is tracked in:
`/data/shadowmount/manual.status`
This status file is managed by ShadowMountPlus. If a manually installed game is
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
as deleted in `manual.status` and removes the matching source line from
`manual.lst` so it is not installed again automatically. If you later add the
same source path back to `manual.lst`, it will be processed again and the status
will be updated after installation.
You can use **Dump Installer** to install or prepare game dumps for this manual
workflow, then add the resulting game folder path or image file path to
`manual.lst`.
Recommended folder structure:
- Default mode (`recursive_scan=0`):
- Default mode (`scan_depth=1`):
- `/data/homebrew/<TITLE_ID>/`
- `/data/etaHEN/games/<TITLE_ID>/`
- `/data/homebrew/backports/<TITLE_ID>/`
- `/data/etaHEN/games/backports/<TITLE_ID>/`
- Recursive mode (`recursive_scan=1`):
- Nested mode (`scan_depth=2`):
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/backports/<TITLE_ID>/`
## Creating an exFAT image
Recommended only for titles that need external-drive-style compatibility. For general use, prefer `.ffpkg`.
Linux (Ubuntu/Debian):
- Required components installation:
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
@@ -131,6 +317,7 @@ 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`
@@ -141,6 +328,7 @@ 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.
- Usage:
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
- Behavior:
@@ -162,14 +350,20 @@ FreeBSD:
- Source folder must be the game root and contain `eboot.bin`.
- The script auto-calculates image size using rounded file allocation + metadata + safety margin.
- Recommended `newfs` parameters for UFS2:
- Large-file profile: `newfs -O 2 -b 65536 -f 4096 -m 0 -i 262144`
- Small/mixed-file profile: `newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144`
- `mkufs2.sh` selects between these two profiles automatically (based on average file size).
- `newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096`
- `mkufs2.sh` keeps this fixed block/fragment/sector profile and auto-tunes `-i` based on source file/directory count.
- Rough manual `-i` estimate for manual builds:
- `target_inodes ~= file_count + dir_count + 2048`
- `bytes_per_inode ~= image_size_bytes / target_inodes`
- Round `bytes_per_inode` down to a multiple of `4096`, then keep it in the practical range `65536..262144`.
- Practical rule of thumb: use `262144` for normal game dumps, `131072` for tens of thousands of files, and `65536` only for very file-dense images.
- Example: for an `8 GiB` image with `60000` files and `4000` directories, `-i ~= 8*1024^3 / (60000 + 4000 + 2048) ~= 130312`, so use `-i 131072`.
Windows:
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
- Example:
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
- `UFS2Tool.exe newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
- For manual builds, use `-i 262144` as the baseline and lower it for images with many small files.
## Installation and usage
@@ -178,22 +372,17 @@ Windows:
### Method 1: Manual Payload Injection (Port 9021)
Use a payload sender (such as NetCat GUI or a web-based loader) to send the files to **Port 9021**.
1. Send `notify.elf` (Optional).
* *Only send this if you want graphical pop-ups. Skip if you prefer standard notifications.*
2. Send `shadowmountplus.elf`.
3. Wait for the notification: *"ShadowMount+"*.
1. Send `shadowmountplus.elf`.
2. Wait for the notification: *"ShadowMount+"*.
### Method 2: PLK Autoloader (Recommended)
Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it starts automatically on every boot.
**Sample Configuration:**
```ini
!1000
kstuff.elf
!1000
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
!1000
shadowmountplus.elf
!3000
kstuff.elf
```
---
@@ -205,16 +394,18 @@ If a game is not mounted:
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
- Verify scan roots:
- if `scanpath=...` is set, only these paths are scanned;
- `/data/imgmnt` is always scanned.
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always scanned.
- Verify scan depth:
- `recursive_scan=0` scans only first-level subfolders;
- `recursive_scan=1` scans recursively.
- `scan_depth=1` scans only first-level subfolders;
- `scan_depth=2` scans one additional nested level;
- `recursive_scan=1` is treated as deprecated compatibility mode and forces `scan_depth=2`.
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
- Verify game structure:
- folder game: `<GAME_DIR>/sce_sys/param.json`;
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
- If you see `missing/invalid param.json` for an image, check via FTP that `/data/imgmnt/<TITLE_ID>/` contains full game files and `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder);
- PFSC container (`.ffpfsc`): nested supported image files are scanned; direct game files inside the container are ignored.
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS, `.ffpfsc`=PFS container).
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
If a game is mounted but does not start:
@@ -230,10 +421,13 @@ If a game is mounted but does not start:
* **Drakmor** - Evolution of ShadowMount to ShadowMountPlus
* **Special Thanks:**
* VoidWhisper for ShadowMount [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/voidwhisper)
* EchoStretch
* VoidWhisper for ShadowMount
* BestPig for BackPort
* EchoStretch for kstuff-toggle and etc
* Idlesauce
* Gezine
* earthonion
* LightningMods
* john-tornblom
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
* john-tornblom for SDK
* PS5 R&D Community
+3583
View File
File diff suppressed because it is too large. Load diff
+94 -21
View File
@@ -1,44 +1,103 @@
# ShadowMount runtime config example
# Place as: /data/shadowmount/config.ini
#
# All keys are optional.
# If a key is missing, built-in defaults are used.
# This file is a template.
# Nothing below is active until you remove the leading '#'.
#
# How to use this file:
# 1. Uncomment only the parameters you really want to override.
# 2. Lines that start with '# key=value' are real parameters you can uncomment directly.
# 3. Lines that start with '##' are examples/templates only. Edit them before use.
# Debug logging:
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
# 0/false/no/off -> disable log_debug output
debug=1
# Default: 1
# debug=1
# Quiet mode:
# 1/true/yes/on -> only show the startup version popup and runtime errors
# 0/false/no/off -> also show normal informational popups
# 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
# Global mount mode:
# 1/true/yes/on/ro -> read-only mounts
# 0/false/no/off/rw -> read-write mounts
mount_read_only=1
# Default: 1
# mount_read_only=1
# Per-image mount mode override by file name (can be repeated many times):
# image_ro=<image_filename>
# image_rw=<image_filename>
# image_ro=PPSA17221.ffpfs
# image_rw=My Game Dump.exfat
# Mounting even damaged file systems:
# 1/true/yes/on -> include force option
# 0/false/no/off -> default mount options
# Default: 0
# force_mount=0
# Recursive scan mode:
# 1/true/yes/on -> scan subdirectories recursively (no depth limit)
# 0/false/no/off -> scan only first-level subdirectories (default)
recursive_scan=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
# 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
# Legacy compatibility:
# recursive_scan=1 forces scan_depth=2
# Default: disabled
# recursive_scan=0
# Full scan loop interval (seconds), range: 1..3600
scan_interval_seconds=10
# Default: 15
# scan_interval_seconds=15
# Source stabilization wait window (seconds), range: 0..3600
# If source mtime is newer than this window, it will be deferred to next scan.
stability_wait_seconds=10
# If source mtime/ctime is newer than this window, it will be deferred to next scan.
# Default: 10
# stability_wait_seconds=10
# Backend selection per filesystem:
# lvd -> /dev/lvdctl -> /dev/lvdN
# md -> /dev/mdctl -> /dev/mdN
exfat_backend=lvd
ufs_backend=lvd
# Defaults: exfat=lvd, ufs=lvd
# exfat_backend=lvd
# ufs_backend=lvd
# Backend-specific overrides (optional):
# Sandbox fakelib backport watcher:
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
# 0/false/no/off -> disable fakelib watcher
# Default: 1
# backport_fakelib=1
# Global fakelib overlay:
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
# 0/false/no/off -> do not use the global fakelib folder
# Default: 1
# global_fakelib=1
#
# 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)
# Default: game
# global_fakelib_priority=game
#
## Exclude specific title IDs from the global fakelib overlay (repeatable):
## global_fakelib_exclude=PPSA12345
# Backend-specific sector size overrides:
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
# lvd_exfat_sector_size=512
# lvd_ufs_sector_size=4096
# lvd_pfs_sector_size=32768
@@ -47,6 +106,20 @@ ufs_backend=lvd
# Optional custom scan roots (can be repeated):
# If at least one scanpath=... is present, only these paths are used.
# "/data/imgmnt" is always added automatically.
# "/mnt/shadowmnt/pfsc" and "/mnt/shadowmnt" are always added automatically.
# Default: use built-in scan path list
# scanpath=/data/homebrew
# scanpath=/data/etaHEN/games
## Per-image mount mode override by file name (repeatable):
## image_ro=<image_filename>
## image_rw=<image_filename>
## image_sector=<image_filename>:<sector_size>
##
## Examples:
## image_ro=PPSA12345.ffpfs
## image_rw=My Game Dump.exfat
## image_sector=My Game Dump.exfat:65536
## More custom scan root examples:
## scanpath=/data/homebrew
## scanpath=/data/etaHEN/games
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_ar_sa[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "الانتقال إلى إعدادات التصحيح",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "+شادوماونت v%s exFAT/UFS/PFS",
/* 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] = ":\n%s\n%s\n0x%08X فشلت تهيئة اللعبة للإصدارات الأقدم",
/* English: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = ":%s تمت تهيئة اللعبة للإصدارات الأقدم",
/* 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)\ncode=0x%08X */ [SM_L10N_REGISTER_FAILED_CODE] = "فشل التسجيل: %s (%s)\nالرمز=0x%08X",
/* English: Installing: %s (%s)... */ [SM_L10N_INSTALLING] = "جارٍ التثبيت: %s (%s)...",
/* English: Installed: %s */ [SM_L10N_INSTALLED] = "تم التثبيت: %s",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "ملف param.json مفقود أو غير صالح:\n%s",
/* English: Found %d new games. Executing... */ [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] = "شادوماونت+: تمت إعادة تحميل الإعدادات.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "تم تجاهل عنصر مكرر.\n%s\n%s\nالموجود مسبقاً\n%s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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+: hloubka skenu %u povolena.",
/* English: Backport overlay failed: %s\n%s\n0x%08X */ [SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport overlay selhal: %s\n%s\n0x%08X",
/* English: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Backport hry použit: %s",
/* 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ětší cluster v obrazu nebo nižší sector size v config.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrace selhala: %s (%s)\nAppInstallTitleDir 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: Installed: %s */ [SM_L10N_INSTALLED] = "Nainstalováno: %s",
/* 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] = "Cache obrazů plná (%u položek), nelze sledovat mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache obrazů plná (%u položek), vracím mount 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "Chybí nebo je neplatný param.json:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Nalezeno %d nových her. Spouští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+: config znovu načten.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplicitní %s ignorováno:\n%s\nexistující: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Spil backportet: %s",
/* 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 afvist:\n%s\nKlyngestørrelsen er for lille (%llu < %u).\nimage_sector-tilsidesættelse gemt: %u.\nPrøv at mounte 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] = "Image-mount afvist:\n%s\nKlyngestørrelsen er for lille (%llu < %u).\nBrug større klynger i image eller lavere sector size i config.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Installeret: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Image-mount 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] = "Image-cache fuld (%u poster), kan ikke spore mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-cache fuld (%u poster), ruller mount 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] = "Batchinstallation mislykkedes for %d app(s).\nkode=0x%08X",
/* English: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json mangler eller er ugyldig:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Fandt %d nye spil. Kører...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Bibliotek synkroniseret.\nFandt %d spil.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config genindlæst.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Dublet %s ignoreret:\n%s\neksisterende: %s",
};
+32
View File
@@ -0,0 +1,32 @@
static const char *const g_de_de[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "Zu Debug-Einstellungen",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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 aktiviert.",
/* English: Backport overlay failed: %s\n%s\n0x%08X */ [SM_L10N_BACKPORT_OVERLAY_FAILED] = "Backport-Overlay fehlgeschlagen: %s\n%s\n0x%08X",
/* English: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Spiel backportiert: %s",
/* 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 abgelehnt:\n%s\nClustergröße ist zu klein (%llu < %u).\nimage_sector-Override gespeichert: %u.\nErneut mounten.",
/* 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 abgelehnt:\n%s\nClustergröße ist zu klein (%llu < %u).\nVerwende eine größere Clustergröße im Image oder eine kleinere sector size in der Config.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Installiert: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Image-Mount 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), Mount %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), Mount wird zurückgesetzt",
/* 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 App(s) fehlgeschlagen.\nCode=0x%08X",
/* 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. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "%d neue Spiele gefunden. Ausführung...",
/* 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+: Config neu geladen.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s ignoriert:\n%s\nvorhanden: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_el_gr[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "Μετάβαση στις ρυθμίσεις αποσφαλμάτωσης",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Εφαρμόστηκε backport στο παιχνίδι: %s",
/* 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] = "Απορρίφθηκε το mount του image:\n%s\nΤο μέγεθος cluster είναι πολύ μικρό (%llu < %u).\nΑποθηκεύτηκε override 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] = "Απορρίφθηκε το mount του image:\n%s\nΤο μέγεθος cluster είναι πολύ μικρό (%llu < %u).\nΧρησιμοποιήστε μεγαλύτερο cluster στο image ή μικρότερο sector size στο config.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Αποτυχία εγγραφής: %s (%s)\nΤο AppInstallTitleDir δεν είναι διαθέσιμο",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "Εγκαταστάθηκε: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Αποτυχία mount 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] = "Το μέγεθος 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] = "Η cache image είναι πλήρης (%u εγγραφές), δεν μπορεί να παρακολουθηθεί το mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Η cache image είναι πλήρης (%u εγγραφές), γίνεται rollback του mount",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "Λείπει ή είναι άκυρο το param.json:\n%s",
/* English: Found %d new games. Executing... */ [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+: το config επαναφορτώθηκε.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Το διπλότυπο %s αγνοήθηκε:\n%s\nυπάρχον: %s",
};
+43
View File
@@ -0,0 +1,43 @@
static const char *const g_en_gb[SM_L10N_COUNT] = {
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Go to Debug Settings",
[SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
[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] = "Game backported: %s",
[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_FAILED_CODE] = "Register failed: %s (%s)\ncode=0x%08X",
[SM_L10N_INSTALLING] = "Installing: %s (%s)...",
[SM_L10N_INSTALLED] = "Installed: %s",
[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_MISSING_PARAM] = "Missing/invalid param.json:\n%s",
[SM_L10N_FOUND_NEW_GAMES] = "Found %d new games. Executing...",
[SM_L10N_LIBRARY_SYNCED] = "Library Synchronised.\nFound %d games.",
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config reloaded.",
[SM_L10N_DUPLICATE_IGNORED] =
"Duplicate %s ignored:\n%s\nexisting: %s",
};
+43
View File
@@ -0,0 +1,43 @@
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 exFAT/UFS/PFS",
[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] = "Game backported: %s",
[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_FAILED_CODE] = "Register failed: %s (%s)\ncode=0x%08X",
[SM_L10N_INSTALLING] = "Installing: %s (%s)...",
[SM_L10N_INSTALLED] = "Installed: %s",
[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_MISSING_PARAM] = "Missing/invalid param.json:\n%s",
[SM_L10N_FOUND_NEW_GAMES] = "Found %d new games. Executing...",
[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",
};
+69
View File
@@ -0,0 +1,69 @@
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 exFAT/UFS/PFS */
[SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */
[SM_L10N_GAME_BACKPORTED] = "Juego con backport aplicado: %s",
/* 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).\n"
"Anulación image_sector guardada: %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 de clúster es demasiado pequeño (%llu < %u).\n"
"Use un clúster mayor en la imagen o un sector size 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)\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: Installed: %s */
[SM_L10N_INSTALLED] = "Instalado: %s",
/* 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] =
"Caché de imágenes llena (%u entradas), no se puede rastrear el mount %s",
/* English: Image cache full (%u entries), rolling back mount */
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] =
"Caché de imágenes llena (%u entradas), revirtiendo mount",
/* 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] =
"Error de instalación por lotes para %d aplicación(es).\ncódigo=0x%08X",
/* 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. Executing... */
[SM_L10N_FOUND_NEW_GAMES] = "Se encontraron %d juegos nuevos. Ejecutando...",
/* 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",
};
+69
View File
@@ -0,0 +1,69 @@
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 exFAT/UFS/PFS */
[SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */
[SM_L10N_GAME_BACKPORTED] = "Backport aplicado al juego: %s",
/* 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).\n"
"Se guardó la anulación 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 de clúster es demasiado pequeño (%llu < %u).\n"
"Usa un clúster más grande en la imagen o un sector size 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)\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: Installed: %s */
[SM_L10N_INSTALLED] = "Instalado: %s",
/* 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] =
"Caché de imágenes llena (%u entradas), no se puede rastrear el mount %s",
/* English: Image cache full (%u entries), rolling back mount */
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] =
"Caché de imágenes llena (%u entradas), revirtiendo mount",
/* 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] =
"Falló la instalación por lotes para %d app(s).\ncódigo=0x%08X",
/* 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. Executing... */
[SM_L10N_FOUND_NEW_GAMES] = "Se encontraron %d juegos nuevos. Ejecutando...",
/* 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",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Peli backportattu: %s",
/* 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] = "Imagen liittäminen hylätty:\n%s\nKlusterikoko on liian pieni (%llu < %u).\nKäytä imagessa suurempaa klusteria tai configissa pienempää sector size -arvoa.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Asennettu: %s",
/* 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 image ei ole luettavissa tai on vioittunut",
/* English: Image cache full (%u entries), cannot track mount %s */ [SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Image-välimuisti täynnä (%u merkintää), mountia %s ei voi seurata",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-välimuisti täynnä (%u merkintää), mount 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json puuttuu tai on virheellinen:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Löytyi %d uutta peliä. Suoritetaan...",
/* 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+: config ladattu uudelleen.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Kaksoiskappale %s ohitettu:\n%s\nolemassa: %s",
};
+69
View File
@@ -0,0 +1,69 @@
static const char *const g_fr_ca[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Allez aux paramètres de débogage",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */
[SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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 de l'affichage du backport : %s\n%s\n0x%08X",
/* English: Game backported: %s */
[SM_L10N_GAME_BACKPORTED] = "Jeu backporté : %s",
/* 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 du regroupement d'analyse est trop petite (%llu < %u).\n"
"Remplacement image_sector enregistré : %u.\nEssayez de remonter.",
/* 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 du regroupement d'analyse est trop petite (%llu < %u).\n"
"Utilisez un regroupement d'analyse plus grand dans l'image ou une taille de secteur plus petite dans la config.",
/* 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)\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: Installed: %s */
[SM_L10N_INSTALLED] = "Installé : %s",
/* 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 du regroupement d'analyse du système de fichiers (%llu) est inférieure à la taille du secteur 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] =
"Cache d'images plein (%u entrées), impossible de suivre le mount %s",
/* English: Image cache full (%u entries), rolling back mount */
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] =
"Cache d'images 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: Missing/invalid param.json:\n%s */
[SM_L10N_MISSING_PARAM] = "param.json manquant ou invalide :\n%s",
/* English: Found %d new games. Executing... */
[SM_L10N_FOUND_NEW_GAMES] = "%d nouveaux jeux trouvés. Exécution...",
/* 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+ : config rechargée.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
[SM_L10N_DUPLICATE_IGNORED] =
"Doublon %s ignoré :\n%s\nexistant : %s",
};
+69
View File
@@ -0,0 +1,69 @@
static const char *const g_fr_fr[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Allez aux paramètres de débogage",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */
[SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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 de l'affichage du backport : %s\n%s\n0x%08X",
/* English: Game backported: %s */
[SM_L10N_GAME_BACKPORTED] = "Jeu backporté : %s",
/* 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 du regroupement d'analyse est trop petite (%llu < %u).\n"
"Remplacement image_sector enregistré : %u.\nEssayez de remonter.",
/* 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 du regroupement d'analyse est trop petite (%llu < %u).\n"
"Utilisez un regroupement d'analyse plus grand dans l'image ou une taille de secteur plus petite dans la config.",
/* 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)\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: Installed: %s */
[SM_L10N_INSTALLED] = "Installé : %s",
/* 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 du regroupement d'analyse du système de fichiers (%llu) est inférieure à la taille du secteur 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] =
"Cache d'images plein (%u entrées), impossible de suivre le mount %s",
/* English: Image cache full (%u entries), rolling back mount */
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] =
"Cache d'images 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: Missing/invalid param.json:\n%s */
[SM_L10N_MISSING_PARAM] = "param.json manquant ou invalide :\n%s",
/* English: Found %d new games. Executing... */
[SM_L10N_FOUND_NEW_GAMES] = "%d nouveaux jeux trouvés. Exécution...",
/* 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+ : config rechargée.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
[SM_L10N_DUPLICATE_IGNORED] =
"Doublon %s ignoré :\n%s\nexistant : %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Játék backportolva: %s",
/* 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 elutasítva:\n%s\nA cluster mérete túl kicsi (%llu < %u).\nimage_sector felülírás mentve: %u.\nPróbáld újra mountolni.",
/* 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 elutasítva:\n%s\nA cluster mérete túl kicsi (%llu < %u).\nHasználj nagyobb clustert az image-ben vagy kisebb sector size értéket a configban.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Telepítve: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Image mount 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] = "Az image cache megtelt (%u bejegyzés), a mount nem követhető: %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Az image cache megtelt (%u bejegyzés), mount visszavonása",
/* 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] = "Kötegelt telepítés sikertelen %d appnál.\nkód=0x%08X",
/* 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. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "%d új játék található. Végrehajtá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+: config újratöltve.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplikált %s kihagyva:\n%s\nlétező: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Backport game diterapkan: %s",
/* 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] = "Mount image ditolak:\n%s\nUkuran cluster terlalu kecil (%llu < %u).\nOverride image_sector disimpan: %u.\nCoba mount 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] = "Mount image ditolak:\n%s\nUkuran cluster terlalu kecil (%llu < %u).\nGunakan cluster lebih besar di image atau sector size lebih rendah di config.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrasi gagal: %s (%s)\nAppInstallTitleDir 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: Installed: %s */ [SM_L10N_INSTALLED] = "Terinstal: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Mount 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), tidak dapat melacak mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache image penuh (%u entri), mengembalikan mount",
/* 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 app.\nkode=0x%08X",
/* English: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json hilang atau tidak valid:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Menemukan %d game baru. Menjalankan...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Library disinkronkan.\nMenemukan %d game.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config dimuat ulang.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s diabaikan:\n%s\nyang ada: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */ [SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 è deprecato, uso scan_depth=2.",
/* English: ShadowMount+: scan depth %u enabled. */ [SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: profondità 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Gioco backportato: %s",
/* 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 immagine rifiutato:\n%s\nLa dimensione cluster è troppo piccola (%llu < %u).\nUsa un cluster più grande nell'immagine o una sector size più bassa nella config.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Installato: %s",
/* 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] = "Cache immagini piena (%u voci), impossibile tracciare il mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache immagini piena (%u voci), annullo il mount",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json mancante o non valido:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Trovati %d nuovi giochi. Esecuzione...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Libreria sincronizzata.\nTrovati %d giochi.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config ricaricata.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplicato %s ignorato:\n%s\nesistente: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_ja_jp[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "デバッグ設定へ移動",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "ゲームを backport しました: %s",
/* 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イメージのクラスタサイズを大きくするか、config の sector size を小さくしてください。",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "登録に失敗しました: %s (%s)\nAppInstallTitleDir が利用できません",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "インストール済み: %s",
/* 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) が %s のデバイスセクタサイズ (%u) より小さいです",
/* 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 件)。mount %s を追跡できません",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "イメージキャッシュが満杯です (%u 件)。mount を元に戻します",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json が見つからないか無効です:\n%s",
/* English: Found %d new games. Executing... */ [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+: config を再読み込みしました。",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "重複 %s を無視しました:\n%s\n既存: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_ko_kr[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "디버그 설정으로 이동",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "게임 backport 적용됨: %s",
/* 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이미지에서 더 큰 클러스터를 사용하거나 config에서 더 낮은 sector size를 사용하세요.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "등록 실패: %s (%s)\nAppInstallTitleDir 사용 불가",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "설치됨: %s",
/* 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)가 %s의 장치 섹터 크기(%u)보다 작습니다",
/* 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개), mount %s 추적 불가",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "이미지 캐시가 가득 참(%u개), mount 롤백 중",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json 누락 또는 잘못됨:\n%s",
/* English: Found %d new games. Executing... */ [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+: config 다시 로드됨.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "중복 %s 무시됨:\n%s\n기존: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Game gebackport: %s",
/* 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 geweigerd:\n%s\nClustergrootte is te klein (%llu < %u).\nimage_sector-override opgeslagen: %u.\nProbeer opnieuw te mounten.",
/* 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 geweigerd:\n%s\nClustergrootte is te klein (%llu < %u).\nGebruik een grotere cluster in de image of een lagere sector size in config.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registreren mislukt: %s (%s)\nAppInstallTitleDir 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: Installed: %s */ [SM_L10N_INSTALLED] = "Geïnstalleerd: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Image-mount 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), kan mount %s niet volgen",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Imagecache vol (%u vermeldingen), mount wordt teruggedraaid",
/* 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 app(s).\ncode=0x%08X",
/* English: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json ontbreekt of is ongeldig:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "%d nieuwe games gevonden. Uitvoeren...",
/* 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+: config herladen.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplicaat %s genegeerd:\n%s\nbestaand: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Spill backportet: %s",
/* 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 avvist:\n%s\nKlyngestørrelsen er for liten (%llu < %u).\nimage_sector-overstyring lagret: %u.\nPrøv å mounte 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] = "Image-mount avvist:\n%s\nKlyngestørrelsen er for liten (%llu < %u).\nBruk større klynge i imaget eller lavere sector size i config.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Registrering mislyktes: %s (%s)\nAppInstallTitleDir utilgjengelig",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "Installert: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Image-mount 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-cache full (%u oppføringer), kan ikke spore mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-cache full (%u oppføringer), ruller mount 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] = "Batchinstallasjon mislyktes for %d app(er).\nkode=0x%08X",
/* English: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json mangler eller er ugyldig:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Fant %d nye spill. Kjører...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Bibliotek synkronisert.\nFant %d spill.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config lastet på nytt.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s ignorert:\n%s\neksisterende: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */ [SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 jest przestarzałe, używam scan_depth=2.",
/* English: ShadowMount+: scan depth %u enabled. */ [SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: głębokość skanowania %u włączona.",
/* English: Backport overlay failed: %s\n%s\n0x%08X */ [SM_L10N_BACKPORT_OVERLAY_FAILED] = "Overlay backportu nie powiódł się: %s\n%s\n0x%08X",
/* English: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Backport gry zastosowany: %s",
/* 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).\nZapisano 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 klastra w obrazie lub mniejszego sector size w config.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Zainstalowano: %s",
/* 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] = "Cache obrazów pełny (%u wpisów), nie można śledzić mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache obrazów pełny (%u wpisów), wycofywanie mount",
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */ [SM_L10N_INSTALL_FAILED_TIMEOUT] = "Instalacja nie powiodła się: %s (%s)\nLimit czasu po 5 minutach",
/* English: Batch install queued (%d): */ [SM_L10N_BATCH_INSTALL_QUEUED] = "Instalacja zbiorcza w kolejce (%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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "Brakujący lub nieprawidłowy param.json:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Znaleziono %d nowych gier. Wykonywanie...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Biblioteka zsynchronizowana.\nZnaleziono %d gier.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config przeładowany.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplikat %s zignorowany:\n%s\nistniejący: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Backport aplicado ao jogo: %s",
/* 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] = "Registro falhou: %s (%s)\nAppInstallTitleDir indisponí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: Installed: %s */ [SM_L10N_INSTALLED] = "Instalado: %s",
/* 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] = "Instalação em lote falhou para %d app(s).\ncódigo=0x%08X",
/* 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. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "%d novos jogos encontrados. Executando...",
/* 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",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Jogo com backport aplicado: %s",
/* 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).\nUse um cluster maior na imagem ou uma sector size 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Instalado: %s",
/* 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] = "Cache de imagens cheia (%u entradas), não é possível seguir o mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache de imagens cheia (%u entradas), a reverter mount",
/* 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] = "Instalação em lote falhou para %d aplicação(ões).\ncódigo=0x%08X",
/* 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. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "%d novos jogos encontrados. A executar...",
/* 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",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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âncime scanare %u 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Backport aplicat jocului: %s",
/* 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 respinsă:\n%s\nDimensiunea clusterului este prea mică (%llu < %u).\nFolosiți un cluster mai mare în imagine sau sector size mai mic în config.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Înregistrare eșuată: %s (%s)\nAppInstallTitleDir indisponibil",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "Instalat: %s",
/* 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), mount %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), se anulează mount",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json lipsește sau este invalid:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "S-au găsit %d jocuri noi. Se execută...",
/* 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+: config reîncărcat.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Duplicatul %s ignorat:\n%s\nexistent: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_ru_ru[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "Открыть меню отладки",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Бэкпорт применён к игре: %s",
/* 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Используйте больший размер кластера в образе или меньший sector size в конфиге.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Ошибка регистрации: %s (%s)\nAppInstallTitleDir недоступен",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "Установлено: %s",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "Отсутствует или неверный param.json:\n%s",
/* English: Found %d new games. Executing... */ [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",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Spelet backportat: %s",
/* 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] = "Image-montering nekades:\n%s\nKlusterstorleken är för liten (%llu < %u).\nAnvänd större kluster i imagen eller lägre sector size i config.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Installerat: %s",
/* 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-cache full (%u poster), kan inte spåra mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image-cache full (%u poster), återställer mount",
/* 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] = "Batchinstallation misslyckades för %d app(ar).\nkod=0x%08X",
/* English: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json saknas eller är ogiltig:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Hittade %d nya spel. Kör...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Bibliotek synkroniserat.\nHittade %d spel.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: config omladdad.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Dubblett %s ignorerad:\n%s\nbefintlig: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_th_th[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "ไปที่การตั้งค่าดีบัก",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "ใช้ backport กับเกมแล้ว: %s",
/* 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] = "ปฏิเสธการ mount image:\n%s\nขนาด cluster เล็กเกินไป (%llu < %u)\nบันทึกค่า override image_sector แล้ว: %u\nลอง mount อีกครั้ง",
/* 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] = "ปฏิเสธการ mount image:\n%s\nขนาด cluster เล็กเกินไป (%llu < %u)\nใช้ cluster ที่ใหญ่กว่าใน image หรือ sector size ที่ต่ำกว่าใน config",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "ลงทะเบียนล้มเหลว: %s (%s)\nAppInstallTitleDir ไม่พร้อมใช้งาน",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "ติดตั้งแล้ว: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "mount 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] = "ขนาด cluster ของ filesystem (%llu) เล็กกว่าขนาด sector ของอุปกรณ์ (%u) สำหรับ %s",
/* English: Mounted image is unreadable or damaged */ [SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "image ที่ mount อ่านไม่ได้หรือเสียหาย",
/* English: Image cache full (%u entries), cannot track mount %s */ [SM_L10N_IMAGE_CACHE_FULL_TRACK] = "cache image เต็ม (%u รายการ), ติดตาม mount %s ไม่ได้",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "cache image เต็ม (%u รายการ), กำลังย้อนกลับ mount",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json หายไปหรือไม่ถูกต้อง:\n%s",
/* English: Found %d new games. Executing... */ [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+: โหลด config ใหม่แล้ว",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "ข้ามรายการซ้ำ %s:\n%s\nที่มีอยู่: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */ [SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 kullanımdan kalktı, scan_depth=2 kullanılıyor.",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Oyuna backport uygulandı: %s",
/* 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 reddedildi:\n%s\nCluster boyutu çok küçük (%llu < %u).\nimage_sector geçersiz kılması kaydedildi: %u.\nTekrar mount etmeyi 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] = "Image mount reddedildi:\n%s\nCluster boyutu çok küçük (%llu < %u).\nImage içinde daha büyük cluster veya config içinde daha düşük sector size 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Yüklendi: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Image mount 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 cluster boyutu (%llu), %s için aygıt sektör boyutundan (%u) küçük",
/* English: Mounted image is unreadable or damaged */ [SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Mount edilen image okunamıyor veya hasarlı",
/* English: Image cache full (%u entries), cannot track mount %s */ [SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Image önbelleği dolu (%u kayıt), mount %s izlenemiyor",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Image önbelleği dolu (%u kayıt), mount 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json eksik veya geçersiz:\n%s",
/* English: Found %d new games. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "%d yeni oyun bulundu. Çalıştırılıyor...",
/* 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+: config yeniden yüklendi.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Yinelenen %s yok sayıldı:\n%s\nmevcut: %s",
};
+28
View File
@@ -0,0 +1,28 @@
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 exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */ [SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 đã lỗi thời, 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "Đã áp dụng backport cho game: %s",
/* 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 mount image:\n%s\nKích thước cluster quá nhỏ (%llu < %u).\nĐã lưu ghi đè image_sector: %u.\nHãy thử mount 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 mount image:\n%s\nKích thước cluster quá nhỏ (%llu < %u).\nDùng cluster lớn hơn trong image hoặc sector size thấp hơn trong config.",
/* 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)\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: Installed: %s */ [SM_L10N_INSTALLED] = "Đã cài đặt: %s",
/* English: Image mount failed: 0x%08X (%s)\n%s */ [SM_L10N_IMAGE_MOUNT_FAILED] = "Mount image thất bại: 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] = "Image đã mount không đọc được hoặc bị hỏng",
/* English: Image cache full (%u entries), cannot track mount %s */ [SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Cache image đầy (%u mục), không thể theo dõi mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Cache image đầy (%u mục), đang hoàn tác mount",
/* 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: 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. Executing... */ [SM_L10N_FOUND_NEW_GAMES] = "Tìm thấy %d game mới. Đang thực thi...",
/* English: Library Synchronized.\nFound %d games. */ [SM_L10N_LIBRARY_SYNCED] = "Thư viện đã đồng bộ.\nTìm thấy %d game.",
/* English: ShadowMount+: config reloaded. */ [SM_L10N_CONFIG_RELOADED] = "ShadowMount+: đã tải lại config.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "Đã bỏ qua bản trùng %s:\n%s\nđã có: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_zh_cn[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "前往调试设置",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "游戏已应用 backport: %s",
/* 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请在镜像中使用更大的簇大小,或在 config 中降低 sector size。",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "注册失败: %s (%s)\nAppInstallTitleDir 不可用",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "已安装: %s",
/* 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) 小于 %s 的设备扇区大小 (%u)",
/* 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 项),无法跟踪 mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "镜像缓存已满(%u 项),正在回滚 mount",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json 缺失或无效:\n%s",
/* English: Found %d new games. Executing... */ [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+: config 已重新加载。",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "已忽略重复项 %s:\n%s\n现有: %s",
};
+28
View File
@@ -0,0 +1,28 @@
static const char *const g_zh_tw[SM_L10N_COUNT] = {
/* English: Go to Debug Settings */ [SM_L10N_ACTION_DEBUG_SETTINGS] = "前往除錯設定",
/* English: ShadowMount+ v%s exFAT/UFS/PFS */ [SM_L10N_STARTUP] = "ShadowMount+ v%s exFAT/UFS/PFS",
/* 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: Game backported: %s */ [SM_L10N_GAME_BACKPORTED] = "遊戲已套用 backport: %s",
/* 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請在映像中使用較大的叢集大小,或在 config 中降低 sector size。",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */ [SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "註冊失敗: %s (%s)\nAppInstallTitleDir 無法使用",
/* 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: Installed: %s */ [SM_L10N_INSTALLED] = "已安裝: %s",
/* 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) 小於 %s 的裝置磁區大小 (%u)",
/* 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 筆),無法追蹤 mount %s",
/* English: Image cache full (%u entries), rolling back mount */ [SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "映像快取已滿(%u 筆),正在回復 mount",
/* 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: Missing/invalid param.json:\n%s */ [SM_L10N_MISSING_PARAM] = "param.json 遺失或無效:\n%s",
/* English: Found %d new games. Executing... */ [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+: config 已重新載入。",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */ [SM_L10N_DUPLICATE_IGNORED] = "已忽略重複項 %s:\n%s\n現有: %s",
};
+28
View File
@@ -0,0 +1,28 @@
#ifndef SM_APPDB_H
#define SM_APPDB_H
#include <stdbool.h>
struct AppDbTitleList;
// Close the cached app.db handle and release related resources.
void shutdown_app_db(void);
// Normalize existing snd0info rows during process startup.
bool app_db_run_startup_maintenance(void);
// Update snd0 metadata for a registered title.
int update_snd0info(const char *title_id);
// Normalize an existing snd0Info path for one title.
int normalize_snd0info_for_title(const char *title_id);
// Check whether a title ID exists in a cached app.db title list.
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id);
// Release a title-list snapshot returned by get_app_db_title_list_cached().
void free_app_db_title_list(struct AppDbTitleList *list);
// Drop the cached app.db title list so it is reloaded on next access.
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 the cached PPSA titles that app.db marks as not uninstallable.
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
#endif
+35
View File
@@ -0,0 +1,35 @@
#ifndef SM_CONFIG_MOUNT_H
#define SM_CONFIG_MOUNT_H
#include <stdbool.h>
#include <stdint.h>
typedef struct runtime_config runtime_config_t;
// Ensure runtime configuration is loaded before use.
void ensure_runtime_config_ready(void);
// Load runtime configuration from disk and apply defaults.
bool load_runtime_config(void);
// Reload runtime configuration from disk when config.ini changed.
bool reload_runtime_config_if_changed(bool *reloaded_out);
// Return the current runtime configuration.
const runtime_config_t *runtime_config(void);
// Return the number of configured scan roots.
int get_scan_path_count(void);
// Return a scan root by index, or NULL if out of range.
const char *get_scan_path(int index);
// Return scan depth for a root, including managed container-root expansion.
uint32_t get_scan_depth_for_root(const char *scan_path);
// Resolve a per-image read-only override from the file name.
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
// Resolve a per-image sector-size override from autotune.ini or config.ini.
bool get_image_sector_size_override(const char *filename,
uint32_t *sector_size_out);
// Upsert an autotuned per-image sector-size override.
bool upsert_image_sector_size_autotune(const char *filename,
uint32_t sector_size,
uint32_t *sector_size_out);
// Return true when the global fakelib overlay is disabled for this title.
bool is_global_fakelib_excluded_for_title(const char *title_id);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_FAKELIB_H
#define SM_FAKELIB_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Return true when per-game fakelib backport automation is enabled.
bool sm_fakelib_game_feature_enabled(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);
// 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.
void sm_fakelib_game_shutdown(void);
#endif
+52
View File
@@ -0,0 +1,52 @@
#ifndef SM_FILESYSTEM_H
#define SM_FILESYSTEM_H
#include <dirent.h>
#include <stdbool.h>
#include <stddef.h>
// Check whether a title is present in the installed app set.
bool is_installed(const char *title_id);
// Check whether /user/appmeta/<TITLE_ID>/param.json exists.
bool has_appmeta_data(const char *title_id);
// Check whether a title currently has mounted data.
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);
// 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);
// Recover staged mount links and warm image source mappings on startup.
void cleanup_staged_mount_links(void);
// Remove duplicated managed mount layers left from previous runs.
void cleanup_duplicate_title_mounts(void);
// Remount /system_ex with the expected flags.
int remount_system_ex(void);
// Mount a title source into /system_ex/app/<title_id> via nullfs.
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);
// 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,
bool *overlay_active_out);
// Mount a prepared backport overlay on top of an already mounted title.
// Returns false only when nmount() itself fails.
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);
// Remove stale mount links and optionally restore image-backed mounts.
void cleanup_mount_links(const char *removed_source_root,
bool unmount_system_ex_bind);
// Unmount and remove title links that point under a source root.
void cleanup_mount_links_for_source_unmount(const char *source_root);
// Unmount and remove title links backed by USB sources or USB-backed images.
void cleanup_usb_mount_links_for_suspend(void);
// Recursively copy a directory tree.
int copy_dir(const char *src, const char *dst);
// Copy a single file.
int copy_file(const char *src, const char *dst);
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SM_GAME_CACHE_H
#define SM_GAME_CACHE_H
#include <stdbool.h>
typedef bool (*game_cache_iter_fn)(const char *path, const char *title_id,
const char *title_name,
const char *owning_scan_root, void *ctx);
// Cache resolved metadata for a mounted or discovered game.
void cache_game_entry(const char *path, const char *title_id,
const char *title_name);
// Drop invalid or stale entries from the game cache.
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);
// 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);
// Remove a game cache entry by path.
void clear_cached_game(const char *path);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_GAME_LIFECYCLE_H
#define SM_GAME_LIFECYCLE_H
#include <stdbool.h>
// Start the shared game lifecycle watcher used by runtime game features.
bool start_game_lifecycle_watcher(void);
// Wake the shared watcher so pending lifecycle work is processed immediately.
void wake_game_lifecycle_watcher(void);
// Stop the shared watcher and let lifecycle modules clean up tracked state.
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);
// Ensure the shared watcher is running for runtime game-state tracking.
bool refresh_game_lifecycle_watcher(void);
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef SM_GAMEINFO_H
#define SM_GAMEINFO_H
#include <stdbool.h>
#include <sys/stat.h>
// 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);
// 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);
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef SM_HASH_H
#define SM_HASH_H
#include <stdint.h>
// Return a stable 32-bit FNV-1a hash for a NUL-terminated string.
static inline uint32_t sm_fnv1a32(const char *s) {
uint32_t h = 2166136261u;
while (s && *s) {
h ^= (uint8_t)(*s++);
h *= 16777619u;
}
return h;
}
#endif
+35
View File
@@ -0,0 +1,35 @@
#ifndef SM_IMAGE_H
#define SM_IMAGE_H
#include <stdbool.h>
#include "sm_types.h"
// 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);
// 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.
void cleanup_stale_image_mounts_for_root(const char *root);
// Unmount cached image mounts backed by USB storage during suspend.
bool unmount_usb_image_mounts_for_suspend(void);
// Unmount every cached image mount during shutdown.
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 *unstable_out);
// Build the deterministic runtime mount point for an image source path.
void get_image_mount_point_for_source(const char *file_path,
char mount_point[MAX_PATH]);
// Return true when the filename has a supported image extension.
bool is_supported_image_file_name(const char *name);
// Return true when the source path is a supported image, including nested names.
bool is_supported_image_file_path(const char *full_path, const char *name);
#endif
+34
View File
@@ -0,0 +1,34 @@
#ifndef SM_IMAGE_CACHE_H
#define SM_IMAGE_CACHE_H
#include <stdbool.h>
#include "sm_types.h"
typedef struct {
char path[MAX_PATH];
char mount_point[MAX_PATH];
int unit_id;
attach_backend_t backend;
} image_cache_entry_t;
// Cache a successful image mount and its attached device.
// Returns false when no free cache slot is available.
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.
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);
// 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,
int *unit_out);
// Resolve the original image file path for a cached runtime mount point.
bool resolve_image_source_from_mount_cache(const char *mount_point,
char *path_out,
size_t path_out_size);
#endif
+10
View File
@@ -0,0 +1,10 @@
#ifndef SM_INSTALL_H
#define SM_INSTALL_H
typedef struct scan_candidate scan_candidate_t;
// Install or remount the collected scan candidates.
void process_scan_candidates(const scan_candidate_t *candidates,
int candidate_count);
#endif
+28
View File
@@ -0,0 +1,28 @@
#ifndef SM_INSTALL_QUEUE_H
#define SM_INSTALL_QUEUE_H
#include <stdbool.h>
#include <stdint.h>
typedef struct scan_candidate scan_candidate_t;
// Return whether a title is already waiting for async batch installation.
bool is_title_install_pending(const char *title_id);
// Drop queued/submitted async install state for a title after confirmed removal.
void sm_install_forget_pending_title(const char *title_id);
// Return the next pending-install wake deadline in monotonic microseconds.
uint64_t sm_install_next_wake_us(uint64_t now_us);
// Poll async batch installs for success/timeout completion only.
void sm_install_poll_pending(void);
// Poll async batch installs for success/timeout completion and auto-submit
// queued installs when possible.
void sm_install_service_pending(void);
// Queue a staged title for the next batch install request.
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
bool has_src_snd0);
// Submit all queued titles when no batch install is currently pending.
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);
#endif
+43
View File
@@ -0,0 +1,43 @@
#ifndef SM_L10N_H
#define SM_L10N_H
#include "sm_platform.h"
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_IMAGE_REJECTED_CLUSTER_AUTOTUNED,
SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG,
SM_L10N_REGISTER_FAILED_UNAVAILABLE,
SM_L10N_REGISTER_FAILED_CODE,
SM_L10N_INSTALLING,
SM_L10N_INSTALLED,
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_MISSING_PARAM,
SM_L10N_FOUND_NEW_GAMES,
SM_L10N_LIBRARY_SYNCED,
SM_L10N_CONFIG_RELOADED,
SM_L10N_DUPLICATE_IGNORED,
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);
const char *sm_l10n_lang(void);
const char *sm_l10n_locale(void);
#endif
+46
View File
@@ -0,0 +1,46 @@
#ifndef SM_LIMITS_H
#define SM_LIMITS_H
#define DEFAULT_SCAN_INTERVAL_US 15000000u
#define DEFAULT_SCAN_DEPTH 1u
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
#define SM_LANGUAGE_AUTO (-1)
#define MAX_PENDING 512
#define MAX_IMAGE_MOUNTS 256
#define MAX_IMAGE_MODE_RULES 128
#define MAX_FAKELIB_EXCLUDE_RULES 128
#define PATH_STATE_CAPACITY MAX_PENDING
#define TITLE_STATE_CAPACITY MAX_PENDING
#define STATE_HASH_SIZE 1024u
#define MAX_SCAN_PATHS 256
#define MAX_FAILED_MOUNT_ATTEMPTS 2
#define MAX_REGISTER_ATTEMPTS 2
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
#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
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
#define MIN_SCAN_DEPTH 1u
#define MAX_SCAN_DEPTH 2u
#define MIN_SCAN_INTERVAL_SECONDS 1u
#define MAX_SCAN_INTERVAL_SECONDS 3600u
#define MAX_STABILITY_WAIT_SECONDS 3600u
#define APP_DB_QUERY_BUSY_RETRIES 3
#define APP_DB_UPDATE_BUSY_RETRIES 25
#define APP_DB_PREPARE_BUSY_RETRIES 25
#define APP_DB_BUSY_RETRY_SLEEP_US 200000u
#define APP_DB_BUSY_TIMEOUT_MS 5000
#define MAX_PATH 1024
#define MAX_TITLE_ID 32
#define MAX_TITLE_NAME 256
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
#endif
+43
View File
@@ -0,0 +1,43 @@
#ifndef SM_LOG_H
#define SM_LOG_H
#include <stdbool.h>
#include "sm_l10n.h"
typedef struct sm_error sm_error_t;
// Write a formatted line to the debug log.
void log_debug(const char *fmt, ...);
// Prepare notification assets such as the packaged icon file.
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, ...);
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.
bool sm_error_notified(void);
// Mark the current error as already shown to the user.
void sm_error_mark_notified(void);
// Notify the user about an image mount failure once per error state.
void notify_image_mount_failed(const char *path, int mount_err);
#endif
+20
View File
@@ -0,0 +1,20 @@
#ifndef SM_MANUAL_H
#define SM_MANUAL_H
#include <stdbool.h>
struct AppDbTitleList;
// Visit each manual source path from manual.lst.
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx);
// Mark previously installed manual titles as deleted when they disappear from
// app.db, and remove their source lines from manual.lst.
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready);
// Record that a manual source is installed and associated with a title.
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name);
#endif
+83
View File
@@ -0,0 +1,83 @@
#ifndef SM_MOUNT_DEFS_H
#define SM_MOUNT_DEFS_H
#define IMAGE_MOUNT_READ_ONLY 1
// 1 = use legacy /dev/mdctl backend for .exfat images, 0 = use LVD for all.
#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".
#define LVD_CTRL_PATH "/dev/lvdctl"
#define MD_CTRL_PATH "/dev/mdctl"
#define SCE_LVD_IOC_ATTACH_V0 0xC0286D00
#define SCE_LVD_IOC_ATTACH_V1 0xC0286D09
#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_SECTOR_SIZE_EXFAT 512u
#define LVD_SECTOR_SIZE_UFS 4096u
#define LVD_SECTOR_SIZE_PFS 4096u
#define LVD_SECONDARY_UNIT_SINGLE_IMAGE 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 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 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
#define DEVPFS_BUDGET_GAME "game"
#define DEVPFS_BUDGET_SYSTEM "system"
#define DEVPFS_MKEYMODE_SD "SD"
#define DEVPFS_MKEYMODE_GD "GD"
#define DEVPFS_MKEYMODE_AC "AC"
#define PFS_MOUNT_BUDGET_ID DEVPFS_BUDGET_GAME
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_AC
#define PFS_MOUNT_SIGVERIFY 0
#define PFS_MOUNT_PLAYGO 0
#define PFS_MOUNT_DISC 0
#define PFS_GDDR5_CACHE_IOCTL 0x80209168
// 4x64-bit PFS key encoded as 64 hex chars.
#define PFS_ZERO_EKPFS_KEY_HEX \
"0000000000000000000000000000000000000000000000000000000000000000"
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef SM_MOUNT_DEVICE_H
#define SM_MOUNT_DEVICE_H
#include <stdbool.h>
#include "sm_types.h"
// Return a printable name for an attach backend enum value.
const char *attach_backend_name(attach_backend_t backend);
// Check whether a source path has aged enough to be mounted safely.
bool is_source_stable_for_mount(const char *path, const char *name,
const char *tag);
// Wait until a device node appears or disappears.
bool wait_for_dev_node_state(const char *devname, bool should_exist);
// Resolve backend and unit ID for a mounted image path.
bool resolve_device_from_mount(const char *mount_point,
attach_backend_t *backend_out, int *unit_out);
// Return true if a path is an active 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);
#endif
+42
View File
@@ -0,0 +1,42 @@
#ifndef SM_PATH_STATE_H
#define SM_PATH_STATE_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/stat.h>
// Load cached game info if the param file state still matches.
bool load_cached_game_info(const char *path, const struct stat *param_st,
char *out_id, char *out_name, bool *valid_out);
// Store parsed game info together with the current param file state.
void store_cached_game_info(const char *path, const struct stat *param_st,
bool valid, const char *title_id,
const char *title_name);
// Drop expired entries from path-based retry and metadata state.
void prune_path_state(void);
// Drop expired entries that belong to a specific scan root.
void prune_path_state_for_root(const char *root);
// Return whether scans for a path are temporarily suppressed after failures.
bool is_missing_param_scan_limited(const char *path);
// Record a missing param.json failure for scan backoff.
void record_missing_param_failure(const char *path);
// Clear missing-param backoff state for a path.
void clear_missing_param_entry(const char *path);
// Return whether image mount retries are temporarily suppressed for a path.
bool is_image_mount_limited(const char *path);
// Increment and return the image mount retry counter for a path.
uint8_t bump_image_mount_attempts(const char *path);
// Clear image mount retry state for a path.
void clear_image_mount_attempts(const char *path);
// Return whether backport overlay remounts are suppressed for a path.
bool is_backport_mount_blocked(const char *path);
// Record the first failed backport overlay mount for a path.
void note_backport_mount_failure(const char *path);
// Clear failed backport overlay mount state for a path.
void clear_backport_mount_failure(const char *path);
// Return true once for a missing manual source until the path appears again.
bool note_manual_missing_source_once(const char *path);
// Allow a manual source to be reported missing again after it becomes visible.
void clear_manual_missing_source(const char *path);
#endif
+24
View File
@@ -0,0 +1,24 @@
#ifndef SM_PATH_UTILS_H
#define SM_PATH_UTILS_H
#include <stdbool.h>
#include <stddef.h>
#include "sm_types.h"
// Return the last path component without modifying the input string.
const char *get_filename_component(const char *path);
// Return true when stat() can see the path.
bool path_exists(const char *path);
// Return true when a path lives under the image mount root.
bool is_under_image_mount_base(const char *path);
// Return true when a path is the PFSC container mount root or lives under it.
bool is_pfsc_image_mount_base_or_child(const char *path);
// Return true when path is equal to root or is under it.
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);
// Build "<scan_path>/backports" for a managed scan root.
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
#endif
+47
View File
@@ -0,0 +1,47 @@
#ifndef SM_PATHS_H
#define SM_PATHS_H
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
#define PFSC_IMAGE_MOUNT_BASE "/mnt/shadowmnt/pfsc"
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
#define LOG_DIR "/data/shadowmount"
#define LOG_FILE "/data/shadowmount/debug.log"
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
#define CONFIG_FILE "/data/shadowmount/config.ini"
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
#define APPMETA_BASE "/user/appmeta"
#define APP_BASE "/user/app"
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
#define KILL_FILE "/data/shadowmount/STOP"
#define TOAST_FILE "/data/shadowmount/notify.txt"
#define NOTIFY_ICON_DIR "/user/data/shadowmount"
#define NOTIFY_ICON_FILE "/user/data/shadowmount/smp_icon.png"
#define APP_DB_PATH "/system_data/priv/mms/app.db"
/* Compile-time default scan roots (used when config has no scanpath entries). */
#define SM_DEFAULT_SCAN_PATHS_INITIALIZER \
{ \
/* Internal */ \
"/data/homebrew", "/data/etaHEN/games", \
/* Extended Storage */ \
"/mnt/ext0/homebrew", "/mnt/ext0/etaHEN/games", \
/* M.2 Drive */ \
"/mnt/ext1/homebrew", "/mnt/ext1/etaHEN/games", \
/* USB Subfolders */ \
"/mnt/usb0/homebrew", "/mnt/usb1/homebrew", "/mnt/usb2/homebrew", \
"/mnt/usb3/homebrew", "/mnt/usb4/homebrew", "/mnt/usb5/homebrew", \
"/mnt/usb6/homebrew", "/mnt/usb7/homebrew", \
"/mnt/usb0/etaHEN/games", "/mnt/usb1/etaHEN/games", \
"/mnt/usb2/etaHEN/games", "/mnt/usb3/etaHEN/games", \
"/mnt/usb4/etaHEN/games", "/mnt/usb5/etaHEN/games", \
"/mnt/usb6/etaHEN/games", "/mnt/usb7/etaHEN/games", \
/* USB Root Paths */ \
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3", "/mnt/usb4", \
"/mnt/usb5", "/mnt/usb6", "/mnt/usb7", "/mnt/ext0", "/mnt/ext1", NULL \
}
#endif
+66
View File
@@ -0,0 +1,66 @@
#ifndef SM_PLATFORM_H
#define SM_PLATFORM_H
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/mdioctl.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <time.h>
#include <unistd.h>
#include <ps5/kernel.h>
typedef struct {
uint32_t app_id;
uint64_t unknown1;
char title_id[14];
char unknown2[0x3c];
} app_info_t;
static inline uint32_t sm_firmware_major_version(void) {
uint32_t fw = kernel_get_fw_version();
uint32_t major_bcd = (fw >> 24) & 0xFFu;
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
}
// --- SDK Imports ---
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void);
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 paramId, int32_t *value);
int sceUserServiceInitialize(void *);
void sceUserServiceTerminate(void);
// Standard Notification
typedef struct notify_request {
char unused[45];
char message[3075];
} notify_request_t;
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
#define IOVEC_ENTRY(x) \
{(void *)(x), \
((const char *)(x) == NULL ? 0u : (size_t)(strlen((const char *)(x)) + 1u))}
#define IOVEC_SIZE(x) (sizeof(x) / sizeof(struct iovec))
#endif
+31
View File
@@ -0,0 +1,31 @@
#ifndef SM_RUNTIME_H
#define SM_RUNTIME_H
#include <stdbool.h>
#include <stddef.h>
#include <sys/types.h>
// Install process signal handlers used for graceful shutdown.
void install_signal_handlers(void);
// Return process pid with the given name, or 0 if not found.
pid_t find_pid_by_name(const char *name, bool exclude_self);
// Return true when shutdown was requested by signal or kill file.
bool should_stop_requested(void);
// Request graceful shutdown with a descriptive source string.
void request_shutdown_stop(const char *reason);
// Return true when the payload is paused for system suspend/resume.
bool runtime_sleep_mode_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);
// Serialize mount/link/cache publication against suspend cleanup.
void runtime_mount_state_lock(void);
void runtime_mount_state_unlock(void);
// Request an immediate scan cycle with a descriptive source string.
void request_scan_now(const char *reason);
// Consume a pending immediate scan request and copy its reason into caller storage.
bool consume_scan_now_request(char *reason_out, size_t reason_out_size);
// Sleep in chunks and stop early if shutdown was requested.
bool sleep_with_stop_check(unsigned int total_us);
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef SM_SCAN_H
#define SM_SCAN_H
#include <stdbool.h>
typedef struct scan_candidate scan_candidate_t;
// Unmount and clean up mounts whose backing sources disappeared.
void cleanup_lost_sources_before_scan(void);
// Unmount and clean up mounts whose backing sources disappeared under one root.
void cleanup_lost_sources_for_scan_root(const char *scan_root);
// Immediately unmount runtime mounts backed by USB storage for suspend.
void unmount_usb_sources_for_suspend(void);
// Scan configured roots and collect install candidates.
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out);
// Scan a single configured root and collect install candidates.
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
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);
#endif
+31
View File
@@ -0,0 +1,31 @@
#ifndef SM_SCAN_TREE_H
#define SM_SCAN_TREE_H
#include <stdbool.h>
typedef enum {
SM_SCAN_TREE_DIR_DESCEND = 0,
SM_SCAN_TREE_DIR_SKIP_DESCEND,
SM_SCAN_TREE_DIR_ABORT,
} sm_scan_tree_dir_visit_t;
typedef sm_scan_tree_dir_visit_t (*sm_scan_tree_dir_fn)(const char *dir_path,
unsigned int depth_from_root,
void *ctx);
typedef bool (*sm_scan_tree_image_fn)(const char *image_path,
const char *image_name,
unsigned int depth_from_root,
void *ctx);
typedef struct {
sm_scan_tree_dir_fn on_directory;
sm_scan_tree_image_fn on_image_file;
} sm_scan_tree_callbacks_t;
// Walk a managed scan subtree using the shared scan-depth and path filtering rules.
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
unsigned int depth_from_root,
unsigned int remaining_depth,
const sm_scan_tree_callbacks_t *callbacks, void *ctx);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_SCANNER_H
#define SM_SCANNER_H
#include <stdbool.h>
// Initialize scanner service resources such as wake pipe and file watches.
bool sm_scanner_init(void);
// Wake a blocked scanner wait. Safe to call from signal context.
void sm_scanner_wake(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.
void sm_scanner_run_loop(void);
// Release scanner service resources.
void sm_scanner_shutdown(void);
#endif
+9
View File
@@ -0,0 +1,9 @@
#ifndef SM_SHELLCORE_FLAGS_H
#define SM_SHELLCORE_FLAGS_H
#include <stdbool.h>
bool sm_shellcore_flags_start(void);
void sm_shellcore_flags_stop(void);
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef SM_STABILITY_H
#define SM_STABILITY_H
#include <stdbool.h>
// Check whether a path is old enough since its latest mtime/ctime change.
bool is_path_stable_now(const char *path, double *root_diff_out,
int *stat_errno_out);
// Wait briefly for a path to become stable before giving up.
bool wait_for_stability_fast(const char *path, const char *name);
#endif
+9
View File
@@ -0,0 +1,9 @@
#ifndef SM_TIME_H
#define SM_TIME_H
#include <stdint.h>
// Return a monotonic timestamp in microseconds, or 0 on failure.
uint64_t monotonic_time_us(void);
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef SM_TITLE_STATE_H
#define SM_TITLE_STATE_H
#include <stdbool.h>
#include <stdint.h>
// Return whether registration was already attempted for a title.
bool was_register_attempted(const char *title_id);
// Return the number of registration attempts for a title.
uint8_t get_register_attempts(const char *title_id);
// Record a registration attempt for a title.
void mark_register_attempted(const char *title_id);
// Clear registration attempt state after success or confirmed user removal.
void clear_register_attempts(const char *title_id);
// Notify once about duplicate title IDs from different paths.
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b);
// Clear the one-shot duplicate notification state for a title.
void clear_duplicate_title_notification(const char *title_id);
// Return the number of failed install/remount attempts for a title.
uint8_t get_failed_mount_attempts(const char *title_id);
// Clear failed install/remount attempts for a title.
void clear_failed_mount_attempts(const char *title_id);
// Increment and return failed install/remount attempts for a title.
uint8_t bump_failed_mount_attempts(const char *title_id);
#endif
+150
View File
@@ -0,0 +1,150 @@
#ifndef SM_TYPES_H
#define SM_TYPES_H
#include <stdbool.h>
#include <stdint.h>
#include "sm_limits.h"
typedef struct {
// +0x00: 1 -> mount read-only, 0 -> allow write.
uint32_t ro;
// +0x04: reserved in observed Shell/FSMP callers.
uint32_t reserved0;
// +0x08: logical budget/domain string, usually "game" or "system".
const char *budget_id;
// +0x10: reserved in observed Shell/FSMP callers.
uint32_t reserved1;
// +0x14: bitmask consumed by devpfs mount logic.
uint32_t flags;
// +0x18: optional "maxpkgszingib" value (GiB), 0 means not set.
uint64_t max_pkg_gib;
} devpfs_mount_opt_t;
typedef struct {
// Human-readable profile id for logs.
const char *name;
// Raw option payload mapped to FSMP mount behavior.
devpfs_mount_opt_t opt;
} devpfs_mount_profile_t;
typedef struct {
// Source object class (observed: 1=file, 2=device-like/special source).
uint16_t source_type;
// Layer descriptor flags (observed bit0 = no bitmap file path).
uint16_t flags;
// Must be zero.
uint32_t reserved0;
// Backing file or device path.
const char *path;
// Data start offset in backing object (bytes).
uint64_t offset;
// Data size exposed via this layer (bytes).
uint64_t size;
// Optional bitmap file path.
const char *bitmap_path;
// Bitmap offset in bitmap file (bytes).
uint64_t bitmap_offset;
// Bitmap size (bytes), 0 when bitmap is unused.
uint64_t bitmap_size;
} lvd_ioctl_layer_v0_t;
typedef struct {
// Protocol version for /dev/lvdctl ioctl payload (0 = V0/base attach).
uint32_t io_version;
// Input: usually -1 for auto-assign. Output: created lvd unit id.
int32_t device_id;
// User-visible sector size exported by /dev/lvdN.
uint32_t sector_size;
// Secondary unit/granularity validated against sector_size.
uint32_t secondary_unit;
// Normalized attach flags produced from wrapper raw options.
uint16_t flags;
// LVD image type id (validator accepts 0..0xC).
uint16_t image_type;
// Number of valid entries in layers_ptr.
uint32_t layer_count;
// Total exported virtual size (bytes).
uint64_t device_size;
// Pointer to V0 layer array in user payload.
lvd_ioctl_layer_v0_t *layers_ptr;
} lvd_ioctl_attach_v0_t;
typedef struct {
// Must be zero.
uint32_t reserved0;
// Target lvd unit id to detach.
int32_t device_id;
// Reserved padding required by kernel ABI.
uint8_t reserved[0x20];
} lvd_ioctl_detach_t;
struct AppDbTitleList {
char(*ids)[MAX_TITLE_ID];
int count;
int capacity;
};
typedef struct scan_candidate {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char manual_source_path[MAX_PATH];
bool installed;
bool in_app_db;
bool manual;
} scan_candidate_t;
typedef enum {
ATTACH_BACKEND_NONE = 0,
// /dev/lvdctl -> /dev/lvdN
ATTACH_BACKEND_LVD,
// /dev/mdctl -> /dev/mdN
ATTACH_BACKEND_MD,
} attach_backend_t;
typedef struct runtime_config {
bool debug_enabled;
bool quiet_mode;
bool mount_read_only;
bool force_mount;
bool app_install_all_enabled;
bool app_install_all_forced;
bool backport_fakelib_enabled;
bool global_fakelib_enabled;
bool global_fakelib_mount_first;
bool legacy_recursive_scan_forced;
char global_fakelib_path[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;
int32_t language_id;
attach_backend_t exfat_backend;
attach_backend_t ufs_backend;
uint32_t lvd_sector_exfat;
uint32_t lvd_sector_ufs;
uint32_t lvd_sector_pfs;
uint32_t md_sector_exfat;
uint32_t md_sector_ufs;
} runtime_config_t;
typedef enum {
IMAGE_FS_UNKNOWN = 0,
IMAGE_FS_UFS,
IMAGE_FS_EXFAT,
IMAGE_FS_PFS,
IMAGE_FS_PFSC_CONTAINER,
} image_fs_type_t;
typedef struct sm_error {
int code;
char subsystem[16];
char message[256];
char path[MAX_PATH];
bool valid;
bool notified;
} sm_error_t;
#endif
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
# macOS exFAT image builder — requires macOS 10.6.5+ (exFAT and rsync built-in)
# Usage: mkexfat_macos.sh <input_dir> [output_file]
# (c) @drakmor,@betmoar
set -e
# ── argument validation ───────────────────────────────────────────────────────
if [ -z "$1" ]; then
echo "Usage: $0 <input_dir> [output_file]"
exit 1
fi
INPUT_DIR="$1"
OUTPUT="${2:-test.exfat}"
# newfs_exfat interprets relative paths as device names under /dev/ — resolve to absolute
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
if [ ! -d "$INPUT_DIR" ]; then
echo "Error: input directory not found: $INPUT_DIR"
exit 1
fi
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
exit 1
fi
# ── sizing constants ──────────────────────────────────────────────────────────
CLUSTER_SIZE=32768
LARGE_FILE_THRESHOLD=$((1024 * 1024))
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
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
ENTRY_META_BYTES=256
# Single filesystem traversal: collect all file sizes into a temp file.
# FILE_COUNT and RAW_FILE_BYTES are derived from it in one awk pass.
# DIR_COUNT needs a separate traversal (different -type).
SIZES_FILE=$(mktemp)
MOUNT_POINT=""
LOOP_DEVICE=""
cleanup() {
if [ -n "$MOUNT_POINT" ] && mount | grep -q "on $MOUNT_POINT "; then
umount "$MOUNT_POINT" 2>/dev/null || true
fi
if [ -n "$LOOP_DEVICE" ]; then
hdiutil detach "$LOOP_DEVICE" 2>/dev/null || true
fi
if [ -n "$MOUNT_POINT" ]; then
rmdir "$MOUNT_POINT" 2>/dev/null || true
fi
rm -f "$SIZES_FILE"
}
trap cleanup EXIT INT TERM
find "$INPUT_DIR" -type f -exec stat -f '%z' {} \; > "$SIZES_FILE"
read -r FILE_COUNT RAW_FILE_BYTES <<EOF
$(awk '{c++; s += $1} END {print c+0, s+0}' "$SIZES_FILE")
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")
rm -f "$SIZES_FILE"
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
FAT_BYTES=$((DATA_CLUSTERS * 4))
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then SPARE_BYTES=$SPARE_MIN; fi
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then SPARE_BYTES=$SPARE_MAX; fi
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then TOTAL=$MIN_TOTAL; fi
# Round up to nearest MB
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 "Image size: ${MB}MB"
# ── create, attach, format, mount ────────────────────────────────────────────
MOUNT_POINT=$(mktemp -d)
# mkfile -n creates a sparse file without writing zeros (equivalent to Linux truncate)
mkfile -n "${MB}m" "$OUTPUT"
# Attach as a block device first — newfs_exfat requires real sector geometry via ioctl
# and cannot format a raw file directly
ATTACH_OUTPUT=$(hdiutil attach \
-imagekey diskimage-class=CRawDiskImage \
-nomount "$OUTPUT")
LOOP_DEVICE=$(printf '%s\n' "$ATTACH_OUTPUT" | awk 'NR==1 {print $1; exit}')
if [ -z "$LOOP_DEVICE" ]; then
echo "Error: failed to determine attached device: $OUTPUT"
exit 1
fi
# newfs_exfat -b takes bytes-per-cluster directly
newfs_exfat -b "$CLUSTER_SIZE" "$LOOP_DEVICE"
mount -t exfat "$LOOP_DEVICE" "$MOUNT_POINT"
rsync -r --progress "$INPUT_DIR"/ "$MOUNT_POINT"/
umount "$MOUNT_POINT"
hdiutil detach "$LOOP_DEVICE"
LOOP_DEVICE="" # prevent double-detach in trap
echo "Created $OUTPUT"
+25 -18
View File
@@ -23,14 +23,17 @@ if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
fi
# More accurate sizing for UFS2:
# - file payload rounded to fragment size (-f)
# - file payload rounded to the actual allocation unit (-f)
# - minimal per-directory allocation
# - inode table estimate
# - fixed filesystem metadata headroom
BLOCK_SIZE=32768
FRAG_SIZE=4096
BLOCK_SIZE=65536
FRAG_SIZE=65536
SECTOR_SIZE=4096
MINFREE_PERCENT=0
BYTES_PER_INODE=262144
DEFAULT_BYTES_PER_INODE=$((4 * FRAG_SIZE))
MIN_BYTES_PER_INODE=$FRAG_SIZE
INODE_SIZE=256
INODE_SPARE=2048
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
@@ -43,18 +46,6 @@ DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
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
# UFS profile selection:
# - large-file sets: 64K block
# - small/mixed-file sets: 32K block (safer/denser default)
if [ "$AVG_FILE_BYTES" -ge $((1024 * 1024)) ]; then
BLOCK_SIZE=65536
fi
DATA_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk -v frag="$FRAG_SIZE" '{s += int(($1 + frag - 1) / frag) * frag} END {print s + 0}')
DIR_BYTES=$((DIR_COUNT * FRAG_SIZE))
@@ -77,17 +68,33 @@ fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
IMAGE_BYTES=$((MB * 1024 * 1024))
# Tune inode density for the actual tree size. Keep the documented default
# unless the source contains enough files/directories to require denser inodes.
BYTES_PER_INODE=$DEFAULT_BYTES_PER_INODE
if [ "$INODE_COUNT" -gt 0 ]; then
AUTO_BYTES_PER_INODE=$((IMAGE_BYTES / INODE_COUNT))
AUTO_BYTES_PER_INODE=$(( (AUTO_BYTES_PER_INODE / SECTOR_SIZE) * SECTOR_SIZE ))
if [ "$AUTO_BYTES_PER_INODE" -lt "$MIN_BYTES_PER_INODE" ]; then
AUTO_BYTES_PER_INODE=$MIN_BYTES_PER_INODE
fi
if [ "$AUTO_BYTES_PER_INODE" -lt "$BYTES_PER_INODE" ]; then
BYTES_PER_INODE=$AUTO_BYTES_PER_INODE
fi
fi
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (UFS alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -i $BYTES_PER_INODE (avg file=$AVG_FILE_BYTES bytes)"
echo "Inode target: $INODE_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -S $SECTOR_SIZE -i $BYTES_PER_INODE"
echo "Image size: ${MB}MB"
truncate -s "${MB}M" "$OUTPUT"
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -i "$BYTES_PER_INODE" /dev/${MD}
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -S "$SECTOR_SIZE" -i "$BYTES_PER_INODE" /dev/${MD}
mkdir -p /mnt
mount /dev/${MD} /mnt
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+3
View File
@@ -0,0 +1,3 @@
asm(".global \"C49jelxiaVE\"\n"
".type \"C49jelxiaVE\" @function\n"
"\"C49jelxiaVE\":\n");
+439 -3298
View File
File diff suppressed because it is too large. Load diff
+538
View File
@@ -0,0 +1,538 @@
#include "sm_platform.h"
#include <pthread.h>
#include <sqlite3.h>
#include "sm_runtime.h"
#include "sm_types.h"
#include "sm_appdb.h"
#include "sm_limits.h"
#include "sm_log.h"
#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;
#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;
}
}
void free_app_db_title_list(struct AppDbTitleList *list) {
free(list->ids);
list->ids = NULL;
list->count = 0;
list->capacity = 0;
}
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);
}
static bool ensure_app_db_open(void) {
if (!g_app_db) {
if (sqlite3_open_v2(APP_DB_PATH, &g_app_db, SQLITE_OPEN_READWRITE, NULL) !=
SQLITE_OK) {
log_debug(" [DB] open failed: %s",
(g_app_db ? sqlite3_errmsg(g_app_db) : APP_DB_PATH));
close_app_db();
return false;
}
(void)sqlite3_busy_timeout(g_app_db, APP_DB_BUSY_TIMEOUT_MS);
}
return true;
}
static bool app_db_wait_retry(int rc, int attempt, int max_attempts,
bool close_before_retry) {
if (rc != SQLITE_BUSY && rc != SQLITE_LOCKED)
return false;
if (attempt + 1 >= max_attempts || should_stop_requested())
return false;
if (close_before_retry)
close_app_db();
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
return true;
}
static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
int max_attempts, const char *label) {
for (int attempt = 0; attempt < max_attempts; attempt++) {
if (!ensure_app_db_open())
return -1;
int rc = sqlite3_prepare_v2(g_app_db, sql, -1, stmt_out, NULL);
if (rc == SQLITE_OK)
return SQLITE_OK;
if (app_db_wait_retry(rc, attempt, max_attempts, true))
continue;
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
close_app_db();
return rc;
}
close_app_db();
return -1;
}
static bool app_db_exec_with_retry(const char *sql, int max_attempts,
const char *label, int *changes_out) {
if (changes_out)
*changes_out = 0;
for (int attempt = 0; attempt < max_attempts; attempt++) {
if (!ensure_app_db_open())
return false;
char *err_msg = NULL;
int rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err_msg);
if (rc == SQLITE_OK) {
if (changes_out)
*changes_out = sqlite3_changes(g_app_db);
sqlite3_free(err_msg);
return true;
}
if (app_db_wait_retry(rc, attempt, max_attempts, false)) {
sqlite3_free(err_msg);
continue;
}
if (err_msg) {
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc, err_msg);
} else {
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
}
sqlite3_free(err_msg);
close_app_db();
return false;
}
close_app_db();
return false;
}
static int app_db_query_int(const char *sql, const char *label) {
sqlite3_stmt *stmt = NULL;
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
APP_DB_STARTUP_MAINTENANCE_RETRIES,
label);
if (prep_rc != SQLITE_OK)
return -1;
int result = -1;
for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
result = sqlite3_column_int(stmt, 0);
break;
}
if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false))
continue;
log_debug(" [DB] query failed for %s: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
break;
}
sqlite3_finalize(stmt);
return result;
}
bool app_db_run_startup_maintenance(void) {
bool ok = false;
pthread_mutex_lock(&g_app_db_mutex);
if (!ensure_app_db_open())
goto out;
(void)sqlite3_busy_timeout(g_app_db,
APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS);
const char *backfill_count_sql =
"SELECT COUNT(*) FROM tbl_contentinfo "
"WHERE instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0;";
int pending_backfill =
app_db_query_int(backfill_count_sql, "snd0info normalize backfill check");
if (pending_backfill < 0) {
close_app_db();
goto out;
}
int changes = 0;
if (pending_backfill > 0) {
const char *backfill_sql =
"UPDATE tbl_contentinfo "
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
"'/user/appmeta/'), '/sce_sys', '') "
"WHERE instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0;";
if (!app_db_exec_with_retry(backfill_sql,
APP_DB_STARTUP_MAINTENANCE_RETRIES,
"snd0info startup normalize", &changes))
goto out;
}
log_debug(" [DB] snd0info startup maintenance done rows=%d pending=%d",
changes, pending_backfill);
close_app_db();
ok = true;
out:
pthread_mutex_unlock(&g_app_db_mutex);
return ok;
}
int update_snd0info(const char *title_id) {
int result = -1;
pthread_mutex_lock(&g_app_db_mutex);
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",
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
int rc = sqlite3_step(g_app_db_stmt_update_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 update: 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);
return result;
}
int normalize_snd0info_for_title(const char *title_id) {
int result = -1;
pthread_mutex_lock(&g_app_db_mutex);
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);
return result;
}
static bool append_app_db_title(struct AppDbTitleList *list,
const char *title_id) {
if (!list || !title_id || title_id[0] == '\0')
return true;
if (list->count >= list->capacity) {
int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024;
char(*new_ids)[MAX_TITLE_ID] =
realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids));
if (!new_ids)
return false;
list->ids = new_ids;
list->capacity = new_capacity;
}
(void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID);
list->count++;
return true;
}
static int compare_title_id_str(const void *a, const void *b) {
return strcmp((const char *)a, (const char *)b);
}
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id) {
if (list->count <= 0)
return false;
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
compare_title_id_str) != NULL;
}
static bool step_app_db_title_query(struct AppDbTitleList *list,
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 (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
close_app_db();
return true;
}
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
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);
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) {
free_app_db_title_list(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");
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);
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");
if (loaded)
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
list->count);
return loaded;
}
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);
}
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
const struct AppDbTitleList *src) {
if (!dst || !src)
return false;
free_app_db_title_list(dst);
if (src->count <= 0)
return true;
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
if (!ids)
return false;
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
dst->ids = ids;
dst->count = src->count;
dst->capacity = src->count;
return true;
}
bool get_app_db_title_list_cached(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_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;
}
}
}
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return copied;
}
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;
}
File diff suppressed because it is too large. Load diff
+305
View File
@@ -0,0 +1,305 @@
#include "sm_platform.h"
#include "sm_fakelib.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
#include <pthread.h>
typedef struct {
char source_path[MAX_PATH];
char mount_path[MAX_PATH];
const char *label;
} fakelib_layer_t;
typedef struct {
pid_t pid;
char mount_path[MAX_PATH];
fakelib_layer_t layers[2];
size_t layer_count;
} fakelib_session_t;
static fakelib_session_t g_fakelib_mount;
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
bool sm_fakelib_game_feature_enabled(void) {
return runtime_config()->backport_fakelib_enabled;
}
static bool mount_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path,
const char *label) {
struct iovec overlay_iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path),
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), 0) == 0) {
log_debug(" [FAKELIB] %s libraries mounted for %s: %s -> %s", label,
title_id, source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] %s mount failed for %s (%s -> %s): %s", label,
title_id, source_path, mount_path, strerror(errno));
return false;
}
static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) {
const char *mount_path = layer->mount_path;
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
errno == EINVAL) {
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
layer->source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
mount_path, strerror(errno));
return false;
}
static bool track_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path,
const char *label) {
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
return false;
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
layer->label = label;
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
return true;
}
static bool resolve_sandbox_context(const char *title_id,
char game_source_path[MAX_PATH],
char mount_path[MAX_PATH]) {
game_source_path[0] = '\0';
mount_path[0] = '\0';
char sandbox_id[MAX_TITLE_ID];
DIR *d = opendir("/mnt/sandbox");
if (!d)
return false;
size_t title_len = strlen(title_id);
int best_index = -1;
bool found = false;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.')
continue;
if (strncmp(entry->d_name, title_id, title_len) != 0)
continue;
if (entry->d_name[title_len] != '_')
continue;
const char *suffix = entry->d_name + title_len + 1u;
if (!isdigit((unsigned char)suffix[0]))
continue;
if (strlen(entry->d_name) >= sizeof(sandbox_id))
continue;
errno = 0;
long idx_long = strtol(suffix, NULL, 10);
if (errno == ERANGE || idx_long > INT_MAX)
continue;
int idx = (int)idx_long;
if (idx < best_index)
continue;
best_index = idx;
(void)strlcpy(sandbox_id, entry->d_name, sizeof(sandbox_id));
found = true;
}
closedir(d);
if (!found)
return false;
char source_path[MAX_PATH];
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
sandbox_id);
struct stat st;
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
(void)strlcpy(game_source_path, source_path, MAX_PATH);
char sandbox_root[MAX_PATH];
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
d = opendir(sandbox_root);
if (!d)
return false;
found = false;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.')
continue;
if (strcmp(entry->d_name, "app0") == 0)
continue;
snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
entry->d_name);
if (stat(mount_path, &st) != 0 || !S_ISDIR(st.st_mode))
continue;
found = true;
break;
}
closedir(d);
return found;
}
static bool resolve_global_fakelib_source(const char *title_id,
char source_path[MAX_PATH]) {
source_path[0] = '\0';
const runtime_config_t *cfg = runtime_config();
if (!cfg->global_fakelib_enabled)
return false;
if (is_global_fakelib_excluded_for_title(title_id))
return false;
struct stat st;
if (stat(cfg->global_fakelib_path, &st) != 0) {
if (errno != ENOENT)
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
title_id, cfg->global_fakelib_path, strerror(errno));
return false;
}
if (!S_ISDIR(st.st_mode)) {
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
title_id, cfg->global_fakelib_path);
return false;
}
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
return true;
}
static bool cleanup_fakelib_mount(void) {
if (g_fakelib_mount.layer_count == 0) {
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
while (g_fakelib_mount.layer_count > 0) {
fakelib_layer_t *layer =
&g_fakelib_mount.layers[g_fakelib_mount.layer_count - 1];
if (!unmount_fakelib_overlay(layer))
return false;
memset(layer, 0, sizeof(*layer));
g_fakelib_mount.layer_count--;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
pthread_mutex_lock(&g_fakelib_mutex);
if (!sm_fakelib_game_feature_enabled()) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid == pid) {
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid != pid) {
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
(long)g_fakelib_mount.pid, (long)pid, title_id);
if (!cleanup_fakelib_mount()) {
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
(long)g_fakelib_mount.pid, title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
char game_source_path[MAX_PATH];
char global_source_path[MAX_PATH];
char mount_path[MAX_PATH];
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
bool has_global =
resolve_global_fakelib_source(title_id, global_source_path);
bool has_game = (game_source_path[0] != '\0');
if (!has_global && !has_game) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
g_fakelib_mount.pid = pid;
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
sizeof(g_fakelib_mount.mount_path));
bool global_first = runtime_config()->global_fakelib_mount_first;
bool same_source =
has_global && has_game &&
strcmp(global_source_path, game_source_path) == 0;
if (global_first && has_global && !same_source) {
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
"global")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (has_game) {
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
"game")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (!global_first && has_global && !same_source) {
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
"global")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (has_game)
notify_system_info_l10n(SM_L10N_GAME_BACKPORTED, title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
}
void sm_fakelib_game_on_exit(pid_t pid) {
pthread_mutex_lock(&g_fakelib_mutex);
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
g_fakelib_mount.mount_path);
cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
}
void sm_fakelib_game_shutdown(void) {
pthread_mutex_lock(&g_fakelib_mutex);
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
}
+1163
View File
File diff suppressed because it is too large. Load diff
+198
View File
@@ -0,0 +1,198 @@
#include "sm_platform.h"
#include "sm_game_cache.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
#include "sm_title_state.h"
struct GameCache {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char owning_scan_root[MAX_PATH];
bool valid;
};
static struct GameCache g_game_cache[MAX_PENDING];
static bool resolve_game_cache_owning_scan_root(const char *path,
char owning_scan_root[MAX_PATH]) {
char resolved_source_path[MAX_PATH];
const char *match_path = path;
owning_scan_root[0] = '\0';
if (is_under_image_mount_base(path) &&
resolve_image_source_from_mount_cache(path, resolved_source_path,
sizeof(resolved_source_path))) {
match_path = resolved_source_path;
}
size_t best_match_len = 0;
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (!path_matches_root_or_child(match_path, scan_path))
continue;
size_t scan_path_len = strlen(scan_path);
if (scan_path_len <= best_match_len)
continue;
(void)strlcpy(owning_scan_root, scan_path, MAX_PATH);
best_match_len = scan_path_len;
}
return owning_scan_root[0] != '\0';
}
static void write_game_cache_slot(struct GameCache *entry, const char *path,
const char *title_id,
const char *title_name,
const char *owning_scan_root) {
(void)strlcpy(entry->path, path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(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->valid = true;
}
static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
if (entry->owning_scan_root[0] != '\0')
return true;
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
}
static void clear_game_cache_slot(int index, const char *reason) {
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
return;
if (reason && reason[0] != '\0') {
if (g_game_cache[index].title_id[0] != '\0')
log_debug(" [CACHE] %s: %s (%s)", reason, g_game_cache[index].title_id,
g_game_cache[index].path);
else
log_debug(" [CACHE] %s: %s", reason, g_game_cache[index].path);
}
if (g_game_cache[index].title_id[0] != '\0')
clear_duplicate_title_notification(g_game_cache[index].title_id);
memset(&g_game_cache[index], 0, sizeof(g_game_cache[index]));
}
void cache_game_entry(const char *path, const char *title_id,
const char *title_name) {
char owning_scan_root[MAX_PATH];
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (strcmp(g_game_cache[k].path, path) != 0 &&
strcmp(g_game_cache[k].title_id, title_id) != 0) {
continue;
}
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
owning_scan_root);
return;
}
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid) {
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
owning_scan_root);
return;
}
}
}
void prune_game_cache(void) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (path_exists(g_game_cache[k].path))
continue;
clear_game_cache_slot(k, "source removed");
}
}
void prune_game_cache_for_root(const char *root) {
if (!root || root[0] == '\0') {
prune_game_cache();
return;
}
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");
}
}
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
void *ctx) {
if (!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;
}
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)) {
break;
}
}
}
bool find_cached_game(const char *path, const char *title_id,
const char **existing_path_out) {
if (existing_path_out)
*existing_path_out = NULL;
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;
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;
return true;
}
}
return false;
}
void clear_cached_game(const char *path) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (strcmp(g_game_cache[k].path, path) != 0)
continue;
clear_game_cache_slot(k, "removed from duplicate tracking");
}
}
+642
View File
@@ -0,0 +1,642 @@
#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_fakelib.h"
#include "sm_game_lifecycle.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_runtime.h"
#include "sm_scanner.h"
#include "sm_time.h"
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
typedef struct {
bool active;
pid_t pid;
uint64_t exec_time_us;
uint64_t deadline_us;
} pending_game_launch_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 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 char g_active_game_title_id[MAX_TITLE_ID];
static bool register_game_exit_watch(int kq, pid_t pid);
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();
}
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')
(void)strlcpy(g_active_game_title_id, title_id,
sizeof(g_active_game_title_id));
else
g_active_game_title_id[0] = '\0';
pthread_mutex_unlock(&g_active_game_mutex);
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 is_supported_title_id(const char *title_id) {
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
}
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
if (a == 0)
return b;
if (b == 0)
return a;
return a < b ? a : b;
}
/*
static void bytes_to_hex(const void *src, size_t src_len, char *dst,
size_t dst_size) {
static const char hex_chars[] = "0123456789abcdef";
const uint8_t *bytes = (const uint8_t *)src;
size_t out_pos = 0;
if (!dst || dst_size == 0)
return;
for (size_t i = 0; i < src_len && out_pos + 2 < dst_size; ++i) {
dst[out_pos++] = hex_chars[(bytes[i] >> 4) & 0x0f];
dst[out_pos++] = hex_chars[bytes[i] & 0x0f];
}
dst[out_pos] = '\0';
}
static void log_app_info_response(pid_t pid, int rc, const app_info_t *appinfo) {
if (rc != 0) {
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld failed: rc=0x%08X",
(long)pid, (unsigned)rc);
return;
}
if (!appinfo)
return;
char title_id[MAX_TITLE_ID];
size_t title_len = strnlen(appinfo->title_id, sizeof(appinfo->title_id));
if (title_len >= sizeof(title_id))
title_len = sizeof(title_id) - 1u;
memcpy(title_id, appinfo->title_id, title_len);
title_id[title_len] = '\0';
char unknown2_hex[sizeof(appinfo->unknown2) * 2u + 1u];
bytes_to_hex(appinfo->unknown2, sizeof(appinfo->unknown2), unknown2_hex,
sizeof(unknown2_hex));
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld app_id=0x%08X "
"unknown1=0x%016llX title_id=\"%s\"",
(long)pid, appinfo->app_id,
(unsigned long long)appinfo->unknown1, title_id);
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld unknown2=%s", (long)pid,
unknown2_hex);
}
*/
static bool resolve_game_title_id(pid_t pid, char title_id[MAX_TITLE_ID],
uint32_t *app_id_out) {
app_info_t appinfo;
memset(&appinfo, 0, sizeof(appinfo));
int rc = sceKernelGetAppInfo(pid, &appinfo);
/* log_app_info_response(pid, rc, &appinfo); */
if (rc != 0)
return false;
size_t title_len = strnlen(appinfo.title_id, sizeof(appinfo.title_id));
if (title_len == 0 || title_len >= MAX_TITLE_ID)
return false;
memcpy(title_id, appinfo.title_id, title_len);
title_id[title_len] = '\0';
if (app_id_out)
*app_id_out = appinfo.app_id;
return true;
}
static bool is_process_alive(pid_t pid) {
if (kill(pid, 0) == 0)
return true;
return errno != ESRCH;
}
static bool dispatch_game_launch(int kq, pid_t pid, const char *title_id,
uint32_t app_id) {
if (!register_game_exit_watch(kq, pid)) {
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
title_id, (long)pid);
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);
sm_fakelib_game_on_exec(pid, title_id);
return true;
}
static void clear_pending_game_launch(pending_game_launch_t *entry) {
memset(entry, 0, sizeof(*entry));
}
static void clear_all_pending_game_launches(void) {
memset(g_pending_game_launches, 0, sizeof(g_pending_game_launches));
}
static void close_game_lifecycle_wake_pipe(void) {
if (g_game_lifecycle_wake_pipe[0] >= 0) {
close(g_game_lifecycle_wake_pipe[0]);
g_game_lifecycle_wake_pipe[0] = -1;
}
if (g_game_lifecycle_wake_pipe[1] >= 0) {
close(g_game_lifecycle_wake_pipe[1]);
g_game_lifecycle_wake_pipe[1] = -1;
}
}
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) {
struct kevent events[16];
struct timespec timeout;
memset(&timeout, 0, sizeof(timeout));
while (true) {
int nev = kevent(kq, NULL, 0, events, sizeof(events) / sizeof(events[0]),
&timeout);
if (nev < 0) {
if (errno == EINTR)
continue;
log_debug(" [GAME] stale event drain failed: %s", strerror(errno));
return false;
}
if (nev == 0)
return true;
}
}
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;
g_game_lifecycle_start_ready = true;
pthread_cond_broadcast(&g_game_lifecycle_start_cond);
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
}
static pending_game_launch_t *find_pending_game_launch(pid_t pid) {
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (entry->active && entry->pid == pid)
return entry;
}
return NULL;
}
static pending_game_launch_t *reserve_pending_game_launch(uint64_t now_us) {
pending_game_launch_t *oldest_entry = NULL;
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
return entry;
if (now_us != 0 && entry->deadline_us != 0 && now_us >= entry->deadline_us)
return entry;
if (!oldest_entry ||
(entry->deadline_us != 0 &&
(oldest_entry->deadline_us == 0 ||
entry->deadline_us < oldest_entry->deadline_us))) {
oldest_entry = entry;
}
}
return oldest_entry;
}
static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
uint64_t exec_time_us,
uint64_t now_us) {
pending_game_launch_t *entry = find_pending_game_launch(pid);
if (!entry)
entry = reserve_pending_game_launch(now_us);
if (entry->active && entry->pid != pid) {
log_debug(" [GAME] replacing pending exec pid=%ld with pid=%ld",
(long)entry->pid, (long)pid);
}
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
entry->active = true;
entry->pid = pid;
entry->exec_time_us = base_time_us;
entry->deadline_us =
base_time_us == 0 ? 0 : base_time_us + GAME_APPINFO_LOOKUP_TIMEOUT_US;
return entry;
}
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
uint64_t now_us,
const char *title_id) {
clear_all_pending_game_launches();
queue_pending_game_launch(pid, exec_time_us, now_us);
log_debug(" [GAME] deferring launch retry for %s pid=%ld", title_id,
(long)pid);
}
static uint64_t next_pending_game_wake_us(uint64_t now_us) {
uint64_t next_wake_us = 0;
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
continue;
if (now_us == 0) {
next_wake_us = min_nonzero_u64(next_wake_us, 1);
continue;
}
uint64_t poll_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
uint64_t entry_wake_us = min_nonzero_u64(entry->deadline_us, poll_wake_us);
next_wake_us = min_nonzero_u64(next_wake_us, entry_wake_us);
}
return next_wake_us;
}
static const struct timespec *compute_game_wait_timeout(
struct timespec *timeout_out) {
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));
if (next_wake_us == 0)
return NULL;
uint64_t wait_us = GAME_LIFECYCLE_POLL_INTERVAL_US;
if (now_us != 0 && next_wake_us > now_us)
wait_us = next_wake_us - now_us;
else if (now_us != 0)
wait_us = 0;
timeout_out->tv_sec = (time_t)(wait_us / 1000000ull);
timeout_out->tv_nsec = (long)((wait_us % 1000000ull) * 1000ull);
return timeout_out;
}
static void poll_game_modules(int kq) {
uint64_t now_us = monotonic_time_us();
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
continue;
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
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 (dispatch_game_launch(kq, pid, 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);
break;
} else {
clear_pending_game_launch(entry);
}
} else if (!is_process_alive(entry->pid)) {
clear_pending_game_launch(entry);
}
if (entry->active && now_us != 0 && entry->deadline_us != 0 &&
now_us >= entry->deadline_us) {
log_debug(" [GAME] title_id was not available within %uus for pid=%ld",
(unsigned)GAME_APPINFO_LOOKUP_TIMEOUT_US, (long)entry->pid);
clear_pending_game_launch(entry);
}
}
}
static bool register_game_exit_watch(int kq, pid_t pid) {
struct kevent kev;
EV_SET(&kev, (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_EXIT, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] failed to register exit watch for pid=%ld: %s",
(long)pid, strerror(errno));
return false;
}
return true;
}
static void handle_game_exec(int kq, pid_t pid) {
if (find_pending_game_launch(pid))
return;
uint64_t now_us = monotonic_time_us();
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 (dispatch_game_launch(kq, pid, title_id, app_id))
clear_all_pending_game_launches();
else
defer_confirmed_game_launch_retry(pid, now_us, now_us, title_id);
}
return;
}
if (!is_process_alive(pid))
return;
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);
sm_fakelib_game_on_exit(pid);
if (had_active_title) {
int snd0_updates = normalize_snd0info_for_title(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
snd0_updates, title_id);
}
}
static void restore_suspended_game_if_alive(int kq, pid_t pid) {
if (pid <= 0 || !is_process_alive(pid))
return;
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
if (!resolve_game_title_id(pid, title_id, &app_id))
return;
if (!is_supported_title_id(title_id))
return;
log_debug(" [GAME] resumed: %s pid=%ld app_id=0x%08X", title_id,
(long)pid, app_id);
(void)dispatch_game_launch(kq, pid, title_id, app_id);
}
static void *game_lifecycle_watcher_main(void *arg) {
(void)arg;
pid_t syscore_pid = find_pid_by_name("SceSysCore.elf", false);
if (syscore_pid <= 0) {
log_debug(" [GAME] failed to find SceSysCore.elf");
set_game_lifecycle_start_result(false);
return NULL;
}
int kq = kqueue();
if (kq < 0) {
log_debug(" [GAME] kqueue failed: %s", strerror(errno));
set_game_lifecycle_start_result(false);
return NULL;
}
struct kevent kev;
EV_SET(&kev, (uintptr_t)syscore_pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] proc watch registration failed: %s",
strerror(errno));
close(kq);
set_game_lifecycle_start_result(false);
return NULL;
}
EV_SET(&kev, (uintptr_t)g_game_lifecycle_wake_pipe[0], EVFILT_READ,
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] wake pipe registration failed: %s", strerror(errno));
close(kq);
set_game_lifecycle_start_result(false);
return NULL;
}
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()) {
if (runtime_sleep_mode_active()) {
if (!sleep_cleanup_done) {
suspended_game_pid = atomic_load(&g_active_game_pid);
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
publish_active_game(0, NULL);
sleep_cleanup_done = true;
}
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(g_game_lifecycle_wake_pipe[0], &readfds);
int rc = select(g_game_lifecycle_wake_pipe[0] + 1, &readfds, NULL, NULL,
NULL);
if (rc < 0) {
if (errno == EINTR)
continue;
log_debug(" [GAME] sleep wait failed: %s", strerror(errno));
break;
}
drain_game_lifecycle_wake_pipe();
continue;
}
if (sleep_cleanup_done) {
if (!discard_game_lifecycle_events_nowait(kq))
break;
sleep_cleanup_done = false;
restore_suspended_game_if_alive(kq, suspended_game_pid);
suspended_game_pid = 0;
}
struct kevent event;
struct timespec timeout;
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
int nev = kevent(kq, NULL, 0, &event, 1, timeout_ptr);
if (nev < 0) {
if (errno == EINTR)
continue;
if (g_game_lifecycle_stop_requested)
break;
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
break;
}
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.fflags & NOTE_TRACKERR) != 0) {
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
} else {
if ((event.fflags & NOTE_EXEC) != 0)
handle_game_exec(kq, (pid_t)event.ident);
if ((event.fflags & NOTE_EXIT) != 0)
handle_game_exit((pid_t)event.ident);
}
}
if (runtime_sleep_mode_active())
continue;
poll_game_modules(kq);
}
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
publish_active_game(0, NULL);
close(kq);
log_debug(" [GAME] lifecycle watcher stopped");
return NULL;
}
bool start_game_lifecycle_watcher(void) {
if (g_game_lifecycle_thread_started)
return true;
if (pipe(g_game_lifecycle_wake_pipe) != 0) {
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])) {
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
strerror(errno));
close_game_lifecycle_wake_pipe();
return false;
}
g_game_lifecycle_stop_requested = 0;
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
g_game_lifecycle_start_ready = false;
g_game_lifecycle_start_success = false;
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
int rc =
pthread_create(&g_game_lifecycle_thread, NULL, game_lifecycle_watcher_main,
NULL);
if (rc != 0) {
log_debug(" [GAME] watcher start failed: %s", strerror(rc));
close_game_lifecycle_wake_pipe();
return false;
}
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
while (!g_game_lifecycle_start_ready)
pthread_cond_wait(&g_game_lifecycle_start_cond, &g_game_lifecycle_start_mutex);
bool start_success = g_game_lifecycle_start_success;
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
if (!start_success) {
(void)pthread_join(g_game_lifecycle_thread, NULL);
close_game_lifecycle_wake_pipe();
return false;
}
g_game_lifecycle_thread_started = true;
return true;
}
void wake_game_lifecycle_watcher(void) {
if (g_game_lifecycle_wake_pipe[1] < 0)
return;
char wake = 'x';
ssize_t written = write(g_game_lifecycle_wake_pipe[1], &wake, sizeof(wake));
if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
log_debug(" [GAME] wake pipe write failed: %s", strerror(errno));
}
}
void stop_game_lifecycle_watcher(void) {
if (!g_game_lifecycle_thread_started)
return;
g_game_lifecycle_stop_requested = 1;
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);
close_game_lifecycle_wake_pipe();
}
bool sm_game_lifecycle_has_active_game(void) {
return atomic_load(&g_active_game_pid) > 0;
}
bool refresh_game_lifecycle_watcher(void) {
if (!g_game_lifecycle_thread_started)
return start_game_lifecycle_watcher();
wake_game_lifecycle_watcher();
return true;
}
+113
View File
@@ -0,0 +1,113 @@
#include "sm_platform.h"
#include "sm_gameinfo.h"
#include "sm_limits.h"
#include "sm_path_state.h"
// --- Game Metadata Parsing (param.json) ---
static int extract_json_string(const char *json, const char *key, char *out,
size_t out_size) {
char search[64];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p)
return -1;
p = strchr(p + strlen(search), ':');
if (!p)
return -2;
while (*++p && isspace(*p)) {
/* skip */
}
if (*p != '"')
return -3;
p++;
size_t i = 0;
while (i < out_size - 1 && p[i] && p[i] != '"') {
out[i] = p[i];
i++;
}
out[i] = '\0';
return 0;
}
bool get_game_info(const char *base_path, const struct stat *param_st,
char *out_id, char *out_name) {
out_id[0] = '\0';
out_name[0] = '\0';
if (!S_ISREG(param_st->st_mode))
return false;
bool cached_valid = false;
if (load_cached_game_info(base_path, param_st, out_id, out_name, &cached_valid))
return cached_valid;
if (param_st->st_size <= 0 || param_st->st_size > MAX_PARAM_JSON_SIZE) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
char path[MAX_PATH];
int written = snprintf(path, sizeof(path), "%s/sce_sys/param.json",
base_path);
if (written < 0 || (size_t)written >= sizeof(path)) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
FILE *f = fopen(path, "rb");
if (!f) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
size_t len = (size_t)param_st->st_size;
char *buf = (char *)malloc(len + 1);
if (!buf) {
fclose(f);
return false;
}
bool read_ok = (fread(buf, 1, len, f) == len);
fclose(f);
if (!read_ok) {
free(buf);
return false;
}
buf[len] = '\0';
bool valid = false;
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) {
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,
MAX_TITLE_NAME) != 0)
extract_json_string(buf, "titleName", out_name, MAX_TITLE_NAME);
if (out_name[0] == '\0')
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
valid = true;
}
free(buf);
store_cached_game_info(base_path, param_st, valid, out_id, out_name);
return valid;
}
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out) {
int dir_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
if (dir_fd < 0)
return false;
struct stat st;
if (fstatat(dir_fd, "sce_sys", &st, 0) == 0 && S_ISDIR(st.st_mode) &&
fstatat(dir_fd, "sce_sys/param.json", &st, 0) == 0 &&
S_ISREG(st.st_mode)) {
if (param_st_out)
*param_st_out = st;
close(dir_fd);
return true;
}
close(dir_fd);
return false;
}
+1377
View File
File diff suppressed because it is too large. Load diff
+142
View File
@@ -0,0 +1,142 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_image_cache.h"
#include "sm_limits.h"
struct ImageCache {
char path[MAX_PATH];
char mount_point[MAX_PATH];
int unit_id;
attach_backend_t backend;
bool valid;
};
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
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)
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);
if (entry_index >= 0) {
struct ImageCache *entry = &g_image_cache[entry_index];
(void)strlcpy(entry->path, path, sizeof(entry->path));
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
return entry_index;
}
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
if (!g_image_cache[k].valid) {
(void)strlcpy(g_image_cache[k].path, path, sizeof(g_image_cache[k].path));
(void)strlcpy(g_image_cache[k].mount_point, 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].valid = true;
return k;
}
}
return -1;
}
bool cache_image_source_mapping(const char *path, const char *mount_point) {
pthread_mutex_lock(&g_image_cache_mutex);
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
pthread_mutex_unlock(&g_image_cache_mutex);
return ok;
}
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
attach_backend_t backend) {
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;
}
struct ImageCache *entry = &g_image_cache[entry_index];
entry->unit_id = unit_id;
entry->backend = backend;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
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);
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;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
void invalidate_image_cache_entry(int index) {
pthread_mutex_lock(&g_image_cache_mutex);
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
pthread_mutex_unlock(&g_image_cache_mutex);
return;
}
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
pthread_mutex_unlock(&g_image_cache_mutex);
}
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);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
const struct ImageCache *entry = &g_image_cache[entry_index];
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
*backend_out = entry->backend;
*unit_out = entry->unit_id;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
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);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
const struct ImageCache *entry = &g_image_cache[entry_index];
path_out[0] = '\0';
(void)strlcpy(path_out, entry->path, path_out_size);
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
+462
View File
@@ -0,0 +1,462 @@
#include "sm_platform.h"
#include "sm_config_mount.h"
#include "sm_runtime.h"
#include "sm_install.h"
#include "sm_install_queue.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_log.h"
#include "sm_filesystem.h"
#include "sm_limits.h"
#include "sm_path_utils.h"
#include "sm_appdb.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_paths.h"
#include "sm_manual.h"
#include <dlfcn.h>
typedef int (*app_install_title_dir_fn_t)(const char *title_id,
const char *install_path,
void *reserved);
static const char *const k_app_inst_util_sprx_path =
"/system/common/lib/libSceAppInstUtil.sprx";
static const char *const k_app_install_title_dir_symbol =
"sceAppInstUtilAppInstallTitleDir";
static app_install_title_dir_fn_t g_app_install_title_dir_fn = NULL;
static bool g_app_install_title_dir_resolve_attempted = false;
static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
if (g_app_install_title_dir_fn)
return g_app_install_title_dir_fn;
if (g_app_install_title_dir_resolve_attempted)
return NULL;
if (sm_firmware_major_version() >= 12u) {
g_app_install_title_dir_resolve_attempted = true;
return NULL;
}
g_app_install_title_dir_resolve_attempted = true;
void *app_inst_util_handle = dlopen(k_app_inst_util_sprx_path, RTLD_LAZY);
if (!app_inst_util_handle) {
const char *error = dlerror();
log_debug(" [REG] dlopen(%s) failed: %s", k_app_inst_util_sprx_path,
error ? error : "unknown");
return NULL;
}
dlerror();
g_app_install_title_dir_fn = (app_install_title_dir_fn_t)dlsym(
app_inst_util_handle, k_app_install_title_dir_symbol);
if (g_app_install_title_dir_fn)
return g_app_install_title_dir_fn;
const char *error = dlerror();
log_debug(" [REG] %s lookup in %s failed: %s",
k_app_install_title_dir_symbol, k_app_inst_util_sprx_path,
error ? error : "unknown");
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;
if (strcasecmp(name, "param.json") == 0 || strcasecmp(name, "param.sfo") == 0)
return true;
const char *ext = strrchr(name, '.');
if (!ext)
return false;
return (strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".dds") == 0 ||
strcasecmp(ext, ".at9") == 0);
}
static bool copy_sce_sys_to_appmeta(const char *src_sce_sys,
const char *user_appmeta_dir) {
DIR *d = opendir(src_sce_sys);
if (!d)
return false;
bool ok = true;
struct dirent *e;
char src_path[MAX_PATH];
char dst_path[MAX_PATH];
struct stat st;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
if (!is_appmeta_file(e->d_name))
continue;
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, e->d_name);
if (stat(src_path, &st) != 0 || !S_ISREG(st.st_mode))
continue;
snprintf(dst_path, sizeof(dst_path), "%s/%s", user_appmeta_dir, e->d_name);
if (copy_file(src_path, dst_path) != 0) {
ok = false;
break;
}
}
if (closedir(d) != 0)
ok = false;
return ok;
}
static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
const char *dst_base,
const char *relative_path) {
char src_path[MAX_PATH];
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)
return true;
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
if (access(dst_path, F_OK) == 0)
return true;
if (copy_file(src_path, dst_path) == 0)
return true;
log_debug(" [COPY] Failed to copy trophy metadata: %s -> %s", src_path,
dst_path);
return false;
}
static bool update_trophy_metadata(const char *title_id,
const char *src_sce_sys) {
char dst_base[MAX_PATH];
char dst_dir[MAX_PATH];
bool ok = true;
snprintf(dst_base, sizeof(dst_base), "/system_data/priv/appmeta/%s",
title_id);
mkdir("/system_data/priv/appmeta", 0755);
mkdir(dst_base, 0755);
snprintf(dst_dir, sizeof(dst_dir), "%s/trophy2", dst_base);
mkdir(dst_dir, 0755);
snprintf(dst_dir, sizeof(dst_dir), "%s/uds", dst_base);
mkdir(dst_dir, 0755);
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
"trophy2/npbind.dat"))
ok = false;
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
"uds/npbind.dat"))
ok = false;
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base, "param.json"))
ok = false;
return ok;
}
// --- Install/Remount Action ---
static bool mount_and_install(const char *src_path, const char *title_id,
const char *title_name, bool is_remount,
bool should_register,
bool use_app_install_all,
bool *has_src_snd0_out) {
char user_appmeta_dir[MAX_PATH];
char user_app_dir[MAX_PATH];
char user_sce_sys[MAX_PATH];
char src_sce_sys[MAX_PATH];
char src_snd0[MAX_PATH];
char image_source_path[MAX_PATH];
bool has_image_source = false;
bool appmeta_missing = false;
bool metadata_restaged = false;
bool restage_staging = false;
bool restage_appmeta = false;
bool has_src_snd0 = false;
snprintf(user_appmeta_dir, sizeof(user_appmeta_dir), "%s/%s", APPMETA_BASE,
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));
if (!has_image_source) {
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
src_path);
}
}
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
appmeta_missing = !has_appmeta_data(title_id);
if (is_remount && appmeta_missing) {
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
}
restage_staging = (!is_remount || should_register);
restage_appmeta = (!is_remount || appmeta_missing);
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
// COPY FILES
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
has_src_snd0 = path_exists(src_snd0);
if (!restage_staging && !restage_appmeta)
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
if (restage_staging) {
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
mkdir(user_sce_sys, 0777);
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys,
user_sce_sys);
return false;
}
char icon_src[MAX_PATH];
char icon_dst[MAX_PATH];
snprintf(icon_src, sizeof(icon_src), "%s/icon0.png", src_sce_sys);
snprintf(icon_dst, sizeof(icon_dst), "%s/icon0.png", user_app_dir);
if (copy_file(icon_src, icon_dst) != 0) {
log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src,
icon_dst);
return false;
}
}
if (restage_appmeta) {
mkdir(APPMETA_BASE, 0777);
mkdir(user_appmeta_dir, 0777);
if (!copy_sce_sys_to_appmeta(src_sce_sys, user_appmeta_dir)) {
log_debug(" [COPY] Failed to copy appmeta files: %s -> %s", src_sce_sys,
user_appmeta_dir);
return false;
}
metadata_restaged = true;
}
if (!update_trophy_metadata(title_id, src_sce_sys))
return false;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (!mount_title_nullfs(title_id, src_path)) {
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
src_path);
return false;
}
// WRITE TRACKER
char lnk_path[MAX_PATH];
runtime_mount_state_lock();
if (runtime_sleep_mode_active()) {
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
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)) {
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
char img_lnk_path[MAX_PATH];
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)) {
(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] image source cache update failed: %s -> %s",
src_path, image_source_path);
}
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
strerror(errno));
}
bool sleep_started = runtime_sleep_mode_active();
runtime_mount_state_unlock();
if (sleep_started || runtime_sleep_mode_active())
return true;
if (!should_register) {
if (metadata_restaged && has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info force-updated after appmeta refresh rows=%d",
snd0_updates);
}
log_debug(" [REG] Skip (already present in app.db)");
return true;
}
if (use_app_install_all) {
if (has_src_snd0_out)
*has_src_snd0_out = has_src_snd0;
log_debug(" [REG] Prepared for batch install: %s (%s)", title_name,
title_id);
return true;
}
// 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_l10n(SM_L10N_REGISTER_FAILED_UNAVAILABLE, title_name,
title_id);
return false;
}
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);
}
} 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);
}
// Silent on restore/remount to avoid spam
} else {
log_debug(" [REG] FAIL: 0x%x", res);
notify_system_l10n(SM_L10N_REGISTER_FAILED_CODE, title_name, title_id,
(uint32_t)res);
return false;
}
return true;
}
// --- Execution (per discovered candidate) ---
void process_scan_candidates(const scan_candidate_t *candidates,
int candidate_count) {
if (runtime_sleep_mode_active())
return;
sm_install_poll_pending();
for (int i = 0; i < candidate_count; i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
const scan_candidate_t *c = &candidates[i];
bool has_src_snd0 = false;
bool use_app_install_all =
runtime_config()->app_install_all_enabled && !c->in_app_db;
if (c->installed) {
log_debug(" [ACTION] Remounting: %s", c->title_name);
} else {
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
if (!use_app_install_all) {
notify_system_info_l10n(SM_L10N_INSTALLING, c->title_name,
c->title_id);
}
}
bool mounted = mount_and_install(c->path, c->title_id, c->title_name,
c->installed, !c->in_app_db,
use_app_install_all, &has_src_snd0);
if (runtime_sleep_mode_active())
return;
if (mounted) {
if (c->manual && !use_app_install_all) {
sm_manual_note_installed(c->manual_source_path, c->title_id,
c->title_name);
}
if (use_app_install_all) {
if (!sm_install_queue_candidate(c, has_src_snd0)) {
log_debug(" [REG] Failed to queue staged install: %s (%s)",
c->title_name, c->title_id);
if (should_stop_requested() || runtime_sleep_mode_active())
return;
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
continue;
}
clear_failed_mount_attempts(c->title_id);
} else {
clear_failed_mount_attempts(c->title_id);
cache_game_entry(c->path, c->title_id, c->title_name);
}
} else {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
}
}
if (!sm_install_submit_queued())
sm_install_note_submit_failure();
}
+468
View File
@@ -0,0 +1,468 @@
#include "sm_platform.h"
#include "sm_install_queue.h"
#include "sm_types.h"
#include "sm_appdb.h"
#include "sm_config_mount.h"
#include "sm_game_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_manual.h"
#include "sm_runtime.h"
#include "sm_time.h"
#include "sm_title_state.h"
#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull)
#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull)
#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull)
typedef enum {
INSTALL_TRACK_NONE = 0,
INSTALL_TRACK_QUEUED,
INSTALL_TRACK_SUBMITTED,
} install_track_state_t;
typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char source_path[MAX_PATH];
char manual_source_path[MAX_PATH];
uint64_t requested_at_us;
bool has_src_snd0;
bool manual;
install_track_state_t state;
} pending_install_entry_t;
static pending_install_entry_t g_pending_installs[MAX_PENDING];
static uint64_t g_pending_install_poll_due_us = 0;
static uint64_t g_queued_install_submit_due_us = 0;
static int g_tracked_install_count = 0;
static int g_submitted_install_count = 0;
static bool g_queued_install_batch_announced = false;
static bool g_queued_install_submit_failure_notified = false;
static int count_queued_installs(void);
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry);
static void drop_queued_install_entry(pending_install_entry_t *entry);
static bool install_queue_active(void) {
return runtime_config()->app_install_all_enabled || 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')
return NULL;
for (int i = 0; i < MAX_PENDING; i++) {
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
continue;
if (strcmp(g_pending_installs[i].title_id, title_id) == 0)
return &g_pending_installs[i];
}
return NULL;
}
static pending_install_entry_t *reserve_pending_install_entry(
const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
return entry;
for (int i = 0; i < MAX_PENDING; i++) {
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
return &g_pending_installs[i];
}
return NULL;
}
static uint64_t next_pending_install_timeout_us(void) {
if (g_submitted_install_count <= 0)
return 0;
uint64_t next_deadline = 0;
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0)
continue;
uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US;
if (next_deadline == 0 || deadline_us < next_deadline)
next_deadline = deadline_us;
}
return next_deadline;
}
static void schedule_pending_install_poll(uint64_t now_us) {
if (g_submitted_install_count <= 0) {
g_pending_install_poll_due_us = 0;
return;
}
g_pending_install_poll_due_us =
now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
}
static void schedule_queued_install_submit(uint64_t now_us) {
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
g_queued_install_submit_due_us = 0;
return;
}
g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us;
}
static void schedule_queued_install_submit_retry(uint64_t now_us) {
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
g_queued_install_submit_due_us = 0;
return;
}
g_queued_install_submit_due_us =
now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US;
}
static void clear_pending_install_entry(pending_install_entry_t *entry) {
if (!entry)
return;
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
g_submitted_install_count--;
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
g_tracked_install_count--;
memset(entry, 0, sizeof(*entry));
if (g_submitted_install_count <= 0)
g_pending_install_poll_due_us = 0;
if (g_tracked_install_count <= 0) {
g_queued_install_submit_due_us = 0;
g_queued_install_batch_announced = false;
g_queued_install_submit_failure_notified = false;
}
}
bool is_title_install_pending(const char *title_id) {
return find_pending_install_entry(title_id) != NULL;
}
void sm_install_forget_pending_title(const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
clear_pending_install_entry(entry);
}
static void append_batch_install_line(char *message, size_t message_size,
const char *title_name,
const char *title_id, int *shown_count) {
if (!message || message_size == 0 || !title_name || !title_id || !shown_count)
return;
size_t used = strlen(message);
if (used >= message_size)
return;
int written = snprintf(message + used, message_size - used, "\n%s (%s)",
title_name, title_id);
if (written < 0 || (size_t)written >= message_size - used)
return;
(*shown_count)++;
}
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
bool has_src_snd0) {
if (!candidate || candidate->title_id[0] == '\0' ||
candidate->title_name[0] == '\0' || candidate->path[0] == '\0') {
return false;
}
if (!runtime_config()->app_install_all_enabled)
return false;
bool was_queue_empty = (count_queued_installs() <= 0);
pending_install_entry_t *entry =
reserve_pending_install_entry(candidate->title_id);
if (!entry) {
log_debug(" [REG] install queue full, cannot queue %s (%s)",
candidate->title_name, candidate->title_id);
return false;
}
install_track_state_t previous_state = entry->state;
if (previous_state == INSTALL_TRACK_SUBMITTED)
return true;
memset(entry, 0, sizeof(*entry));
(void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id));
(void)strlcpy(entry->title_name, candidate->title_name,
sizeof(entry->title_name));
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
(void)strlcpy(entry->manual_source_path, candidate->manual_source_path,
sizeof(entry->manual_source_path));
entry->manual = candidate->manual;
entry->has_src_snd0 = has_src_snd0;
entry->state = INSTALL_TRACK_QUEUED;
if (previous_state == INSTALL_TRACK_NONE)
g_tracked_install_count++;
if (was_queue_empty) {
g_queued_install_batch_announced = false;
g_queued_install_submit_failure_notified = false;
}
schedule_queued_install_submit(monotonic_time_us());
return true;
}
static void finalize_pending_install_success(pending_install_entry_t *entry) {
if (!entry)
return;
log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id);
notify_game_installed_rich(entry->title_id);
if (entry->has_src_snd0) {
int snd0_updates = update_snd0info(entry->title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
if (entry->manual)
sm_manual_note_installed(entry->manual_source_path, entry->title_id,
entry->title_name);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
clear_pending_install_entry(entry);
}
static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
if (!entry)
return;
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
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);
}
static void poll_pending_installs(void) {
if (g_submitted_install_count <= 0)
return;
invalidate_app_db_title_cache();
struct AppDbTitleList app_db_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
uint64_t now_us = monotonic_time_us();
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable while polling installs");
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_SUBMITTED)
continue;
if (app_db_titles_ready &&
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
finalize_pending_install_success(entry);
continue;
}
if (now_us != 0 && entry->requested_at_us != 0 &&
now_us >= entry->requested_at_us &&
now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) {
finalize_pending_install_timeout(entry);
}
}
schedule_pending_install_poll(now_us);
if (g_submitted_install_count == 0)
schedule_queued_install_submit(now_us);
free_app_db_title_list(&app_db_titles);
}
uint64_t sm_install_next_wake_us(uint64_t now_us) {
if (runtime_sleep_mode_active())
return 0;
if (!install_queue_active())
return 0;
if (g_submitted_install_count <= 0)
return g_queued_install_submit_due_us;
uint64_t next_wake_us = next_pending_install_timeout_us();
if (g_pending_install_poll_due_us != 0 &&
(next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) {
next_wake_us = g_pending_install_poll_due_us;
}
if (g_queued_install_submit_due_us != 0 &&
(next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) {
next_wake_us = g_queued_install_submit_due_us;
}
if (next_wake_us == 0)
next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
return next_wake_us;
}
void sm_install_poll_pending(void) {
if (runtime_sleep_mode_active())
return;
poll_pending_installs();
}
void sm_install_service_pending(void) {
if (runtime_sleep_mode_active())
return;
if (!install_queue_active())
return;
sm_install_poll_pending();
if (count_queued_installs() > 0 && !sm_install_submit_queued())
sm_install_note_submit_failure();
}
static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name,
entry->title_id);
}
}
static int count_queued_installs(void) {
int queued_count = g_tracked_install_count - g_submitted_install_count;
return queued_count > 0 ? queued_count : 0;
}
static void log_queued_install_batch(void) {
int queued_count = count_queued_installs();
if (queued_count <= 0)
return;
log_debug(" [REG] Batch install request contains %d title(s):", queued_count);
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
log_debug(" - %s (%s)", entry->title_name, entry->title_id);
}
}
static void notify_queued_install_batch(void) {
int queued_count = count_queued_installs();
if (queued_count <= 0 || g_queued_install_batch_announced)
return;
char message[3075];
int shown_count = 0;
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];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
append_batch_install_line(message, sizeof(message), entry->title_name,
entry->title_id, &shown_count);
}
if (shown_count < queued_count) {
size_t used = strlen(message);
if (used < sizeof(message)) {
(void)snprintf(message + used, sizeof(message) - used,
sm_l10n_get(SM_L10N_BATCH_INSTALL_AND_MORE),
queued_count - shown_count);
}
}
notify_system_info("%s", message);
g_queued_install_batch_announced = true;
}
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) {
if (!entry)
return 0;
uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id);
log_batch_submit_retry_limit(entry);
return failed_attempts;
}
static void drop_queued_install_entry(pending_install_entry_t *entry) {
if (!entry || entry->state != INSTALL_TRACK_QUEUED)
return;
log_debug(" [REG] Dropping queued install after retry limit: %s (%s)",
entry->title_name, entry->title_id);
clear_pending_install_entry(entry);
}
bool sm_install_submit_queued(void) {
if (runtime_sleep_mode_active())
return true;
int queued_count = count_queued_installs();
if (queued_count <= 0)
return true;
if (g_submitted_install_count > 0)
return true;
uint64_t now_us = monotonic_time_us();
if (g_queued_install_submit_due_us != 0 && now_us != 0 &&
now_us < g_queued_install_submit_due_us) {
return true;
}
log_queued_install_batch();
notify_queued_install_batch();
int res = sceAppInstUtilAppInstallAll();
if (res != 0) {
log_debug(" [REG] Batch install request failed: 0x%x", res);
if (!g_queued_install_submit_failure_notified) {
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);
return false;
}
g_queued_install_submit_due_us = 0;
g_queued_install_submit_failure_notified = false;
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
entry->state = INSTALL_TRACK_SUBMITTED;
entry->requested_at_us = now_us;
g_submitted_install_count++;
clear_failed_mount_attempts(entry->title_id);
}
invalidate_app_db_title_cache();
schedule_pending_install_poll(now_us);
return true;
}
void sm_install_note_submit_failure(void) {
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS)
drop_queued_install_entry(entry);
}
}
+242
View File
@@ -0,0 +1,242 @@
#include "sm_l10n.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
#define SCE_SYSTEM_SERVICE_PARAM_ID_LANG 1
typedef struct {
const char *country;
const char *lang;
const char *locale;
} sm_region_t;
static const sm_region_t g_regions[] = {
{"jp", "ja", "ja-JP"}, {"us", "en", "en-US"},
{"fr", "fr", "fr-FR"}, {"es", "es", "es-ES"},
{"de", "de", "de-DE"}, {"it", "it", "it-IT"},
{"nl", "nl", "nl-NL"}, {"pt", "pt", "pt-PT"},
{"ru", "ru", "ru-RU"}, {"kr", "ko", "ko-KR"},
{"tw", "zh", "zh-TW"}, {"cn", "zh", "zh-CN"},
{"fi", "fi", "fi-FI"}, {"se", "sv", "sv-SE"},
{"dk", "da", "da-DK"}, {"no", "no", "no-NO"},
{"pl", "pl", "pl-PL"}, {"br", "pt", "pt-BR"},
{"gb", "en", "en-GB"}, {"tr", "tr", "tr-TR"},
{"mx", "es", "es-MX"}, {"sa", "ar", "ar-SA"},
{"ca", "fr", "fr-CA"}, {"cz", "cs", "cs-CZ"},
{"hu", "hu", "hu-HU"}, {"gr", "el", "el-GR"},
{"ro", "ro", "ro-RO"}, {"th", "th", "th-TH"},
{"vn", "vi", "vi-VN"}, {"id", "id", "id-ID"},
};
#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/vi_vn.inc"
#include "lang/zh_cn.inc"
#include "lang/zh_tw.inc"
static int g_active_lang = 1;
static bool g_l10n_initialized = false;
static size_t region_count(void) {
return sizeof(g_regions) / sizeof(g_regions[0]);
}
static bool is_valid_language_id(int32_t language_id) {
return language_id >= 0 && language_id < (int32_t)region_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;
}
static const char *const *active_catalog(void) {
switch (g_active_lang) {
case 0:
return g_ja_jp;
case 2:
return g_fr_fr;
case 3:
return g_es_es;
case 4:
return g_de_de;
case 5:
return g_it_it;
case 6:
return g_nl_nl;
case 7:
return g_pt_pt;
case 8:
return g_ru_ru;
case 9:
return g_ko_kr;
case 10:
return g_zh_tw;
case 11:
return g_zh_cn;
case 12:
return g_fi_fi;
case 13:
return g_sv_se;
case 14:
return g_da_dk;
case 15:
return g_no_no;
case 16:
return g_pl_pl;
case 17:
return g_pt_br;
case 18:
return g_en_gb;
case 19:
return g_tr_tr;
case 20:
return g_es_mx;
case 21:
return g_ar_sa;
case 22:
return g_fr_ca;
case 23:
return g_cs_cz;
case 24:
return g_hu_hu;
case 25:
return g_el_gr;
case 26:
return g_ro_ro;
case 27:
return g_th_th;
case 28:
return g_vi_vn;
case 29:
return g_id_id;
default:
return g_en_us;
}
}
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 < 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_language_id(sys_lang)) {
active_lang = sys_lang;
} else if (auto_language) {
active_lang = 1;
}
if (!is_valid_language_id(active_lang))
active_lang = 1;
bool changed = !g_l10n_initialized || g_active_lang != active_lang;
g_active_lang = active_lang;
if (changed || cfg->debug_enabled) {
const sm_region_t *region = &g_regions[g_active_lang];
if (auto_language) {
log_debug(" [L10N] language=auto active=%d country=%s lang=%s locale=%s",
g_active_lang, region->country, region->lang, region->locale);
} else {
log_debug(" [L10N] language=config active=%d country=%s lang=%s locale=%s",
g_active_lang, region->country, region->lang, region->locale);
}
}
g_l10n_initialized = true;
}
const char *sm_l10n_get(sm_l10n_key_t key) {
if (!g_l10n_initialized)
sm_l10n_init();
if (key < 0 || key >= SM_L10N_COUNT)
return "";
const char *const *catalog = active_catalog();
if (catalog[key])
return catalog[key];
return g_en_us[key] ? g_en_us[key] : "";
}
const char *sm_l10n_lang(void) {
if (!g_l10n_initialized)
sm_l10n_init();
return g_regions[g_active_lang].lang;
}
const char *sm_l10n_locale(void) {
if (!g_l10n_initialized)
sm_l10n_init();
return g_regions[g_active_lang].locale;
}
+469
View File
@@ -0,0 +1,469 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_types.h"
#include "sm_paths.h"
static sm_error_t g_last_error;
static bool g_notifications_initialized = false;
static pthread_mutex_t g_notifications_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
static FILE *g_log_file = NULL;
extern unsigned char smp_icon_png[];
extern unsigned int smp_icon_png_len;
#define SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM 0xFE
int sceNotificationSend(int userId, bool isLogged, const char *payload);
int sceNotificationSendById(int userid, bool logged_in, const char *useCaseId,
const char *message);
static void notify_system_plain_message(const char *message) {
notify_request_t req;
memset(&req, 0, sizeof(req));
(void)strlcpy(req.message, message, sizeof(req.message));
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
log_debug("NOTIFY: %s", req.message);
}
static bool ensure_notification_icon_present(void) {
struct stat st;
if (stat(NOTIFY_ICON_FILE, &st) == 0 && st.st_size > 0)
return true;
(void)mkdir("/user/data", 0777);
(void)mkdir(NOTIFY_ICON_DIR, 0777);
FILE *fp = fopen(NOTIFY_ICON_FILE, "wb");
if (!fp) {
log_debug(" [NOTIFY] icon create failed: %s (%s)", NOTIFY_ICON_FILE,
strerror(errno));
return false;
}
size_t written = fwrite(smp_icon_png, 1, smp_icon_png_len, fp);
int saved_errno = 0;
if (written != smp_icon_png_len)
saved_errno = ferror(fp) ? errno : EIO;
if (fflush(fp) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(fp) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [NOTIFY] icon write failed: %s (%s)", NOTIFY_ICON_FILE,
strerror(errno));
(void)unlink(NOTIFY_ICON_FILE);
return false;
}
log_debug(" [NOTIFY] icon created: %s", NOTIFY_ICON_FILE);
return true;
}
void sm_notifications_init(void) {
sm_l10n_init();
pthread_mutex_lock(&g_notifications_mutex);
if (!g_notifications_initialized)
g_notifications_initialized = ensure_notification_icon_present();
pthread_mutex_unlock(&g_notifications_mutex);
}
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
size_t used = strlen(dst);
if (used >= dst_size)
return;
for (; *src != '\0' && used + 1 < dst_size; ++src) {
const char *escape = NULL;
char single[2] = {0};
switch (*src) {
case '\\':
escape = "\\\\";
break;
case '"':
escape = "\\\"";
break;
case '\n':
escape = "\\n";
break;
case '\r':
escape = "\\r";
break;
case '\t':
escape = "\\t";
break;
default:
single[0] = *src;
escape = single;
break;
}
size_t escape_len = strlen(escape);
if (used + escape_len >= dst_size)
break;
memcpy(dst + used, escape, escape_len);
used += escape_len;
dst[used] = '\0';
}
}
static bool build_notification_timestamp(time_t timestamp, char out[32]) {
struct tm tm_utc;
if (gmtime_r(&timestamp, &tm_utc) == NULL)
return false;
return strftime(out, 32, "%Y-%m-%dT%H:%M:%S.000Z", &tm_utc) != 0;
}
static bool build_notification_metadata(char *created_at,
char *notification_id,
size_t notification_id_size) {
time_t now;
now = time(NULL);
if (!build_notification_timestamp(now, created_at))
return false;
snprintf(notification_id, notification_id_size, "%u",
(unsigned)((uint32_t)now ^ (uint32_t)getpid()));
return true;
}
static bool send_rich_notification(const char *message) {
char escaped_message[4096];
char escaped_version[128];
char payload[8192];
char created_at[32];
char notification_id[32];
if (!message || message[0] == '\0')
return false;
sm_notifications_init();
pthread_mutex_lock(&g_notifications_mutex);
bool notifications_ready = g_notifications_initialized;
pthread_mutex_unlock(&g_notifications_mutex);
if (!notifications_ready)
return false;
escaped_message[0] = '\0';
escaped_version[0] = '\0';
append_json_escaped(escaped_message, sizeof(escaped_message), message);
append_json_escaped(escaped_version, sizeof(escaped_version),
SHADOWMOUNT_VERSION);
char escaped_action[256];
escaped_action[0] = '\0';
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;
}
int len = snprintf(
payload, sizeof(payload),
"{\n"
" \"rawData\": {\n"
" \"viewTemplateType\": \"InteractiveToastTemplateB\",\n"
" \"channelType\": \"ServiceFeedback\",\n"
" \"bundleName\": \"ShadowMountPlusWelcome\",\n"
" \"useCaseId\": \"IDC\",\n"
" \"soundEffect\": \"none\",\n"
" \"toastOverwriteType\": \"InQueue\",\n"
" \"isImmediate\": true,\n"
" \"priority\": 100,\n"
" \"viewData\": {\n"
" \"icon\": {\n"
" \"type\": \"Url\",\n"
" \"parameters\": {\n"
" \"url\": \"" NOTIFY_ICON_FILE "\"\n"
" }\n"
" },\n"
" \"message\": {\n"
" \"body\": \"%s\"\n"
" },\n"
" \"subMessage\": {\n"
" \"body\": \"ShadowMountPlus %s\"\n"
" },\n"
" \"actions\": [\n"
" {\n"
" \"actionName\": \"%s\",\n"
" \"actionType\": \"DeepLink\",\n"
" \"defaultFocus\": true,\n"
" \"parameters\": {\n"
" \"actionUrl\": \"pssettings:play?function=debug_settings\"\n"
" }\n"
" }\n"
" ]\n"
" },\n"
" \"platformViews\": {\n"
" \"previewDisabled\": {\n"
" \"viewData\": {\n"
" \"icon\": {\n"
" \"type\": \"Predefined\",\n"
" \"parameters\": {\n"
" \"icon\": \"community\"\n"
" }\n"
" },\n"
" \"message\": {\n"
" \"body\": \"%s\"\n"
" }\n"
" }\n"
" }\n"
" }\n"
" },\n"
" \"createdDateTime\": \"%s\",\n"
" \"localNotificationId\": \"%s\"\n"
"}",
escaped_message, escaped_version, escaped_action, escaped_message, created_at,
notification_id);
if (len < 0 || (size_t)len >= sizeof(payload))
return false;
int rc = sceNotificationSend(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
payload);
return rc == 0;
}
static bool send_game_installed_rich_notification(const char *title_id) {
char escaped_title_id[128];
char params[256];
if (!title_id || title_id[0] == '\0')
return false;
escaped_title_id[0] = '\0';
append_json_escaped(escaped_title_id, sizeof(escaped_title_id), title_id);
int len = snprintf(params, sizeof(params),
"{\"npTitleId\":\"%s_00\"}", escaped_title_id);
if (len < 0 || (size_t)len >= sizeof(params))
return false;
int rc = sceNotificationSendById(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
"NUC240", params);
return rc == 0;
}
static void notify_system_plain_v(const char *fmt, va_list args) {
char message[3075];
vsnprintf(message, sizeof(message), fmt, 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;
(void)mkdir(LOG_DIR, 0777);
g_log_file = fopen(LOG_FILE, "a");
if (!g_log_file)
return NULL;
return g_log_file;
}
static void log_to_file_locked(const char *fmt, va_list args) {
FILE *fp = ensure_log_file_open_locked();
if (!fp)
return;
va_list args_file;
time_t rawtime;
struct tm tm_local;
char buffer[80];
time(&rawtime);
(void)localtime_r(&rawtime, &tm_local);
(void)strftime(buffer, sizeof(buffer), "%H:%M:%S", &tm_local);
va_copy(args_file, args);
fprintf(fp, "[%s] ", buffer);
vfprintf(fp, fmt, args_file);
fputc('\n', fp);
fflush(fp);
va_end(args_file);
}
void log_debug(const char *fmt, ...) {
if (runtime_config()->debug_enabled == false)
return;
va_list args;
va_list args_copy;
va_start(args, fmt);
va_copy(args_copy, args);
pthread_mutex_lock(&g_log_mutex);
vfprintf(stdout, fmt, args);
fputc('\n', stdout);
fflush(stdout);
log_to_file_locked(fmt, args_copy);
pthread_mutex_unlock(&g_log_mutex);
va_end(args_copy);
va_end(args);
}
void sm_log_shutdown(void) {
pthread_mutex_lock(&g_log_mutex);
if (g_log_file) {
fclose(g_log_file);
g_log_file = NULL;
}
pthread_mutex_unlock(&g_log_mutex);
}
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
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_end(args);
if (!send_rich_notification(message)) {
notify_system_plain_message(message);
return;
}
log_debug("NOTIFY: %s", message);
}
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, key);
vsnprintf(message, sizeof(message), sm_l10n_get(key), args);
va_end(args);
if (!send_rich_notification(message)) {
notify_system_plain_message(message);
return;
}
log_debug("NOTIFY: %s", message);
}
void notify_game_installed_rich(const char *title_id) {
if (!send_game_installed_rich_notification(title_id)) {
notify_system_info_l10n(SM_L10N_INSTALLED,
(title_id && title_id[0] != '\0') ? title_id
: "Unknown");
return;
}
log_debug("NOTIFY: installed game %s", title_id ? title_id : "unknown");
}
void notify_system_info(const char *fmt, ...) {
if (runtime_config()->quiet_mode)
return;
va_list args;
va_start(args, fmt);
notify_system_plain_v(fmt, args);
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, ...) {
memset(&g_last_error, 0, sizeof(g_last_error));
g_last_error.valid = true;
g_last_error.code = code;
if (subsystem && subsystem[0] != '\0')
(void)strlcpy(g_last_error.subsystem, subsystem,
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);
vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
va_end(args);
}
}
void sm_error_set_l10n(const char *subsystem, int code, const char *path,
sm_l10n_key_t key, ...) {
memset(&g_last_error, 0, sizeof(g_last_error));
g_last_error.valid = true;
g_last_error.code = code;
if (subsystem && subsystem[0] != '\0')
(void)strlcpy(g_last_error.subsystem, subsystem,
sizeof(g_last_error.subsystem));
if (path && path[0] != '\0')
(void)strlcpy(g_last_error.path, path, sizeof(g_last_error.path));
va_list args;
va_start(args, key);
vsnprintf(g_last_error.message, sizeof(g_last_error.message),
sm_l10n_get(key), args);
va_end(args);
}
const sm_error_t *sm_last_error(void) {
return &g_last_error;
}
bool sm_error_notified(void) {
return g_last_error.notified;
}
void sm_error_mark_notified(void) {
g_last_error.notified = true;
}
void notify_system(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
notify_system_plain_v(fmt, args);
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 =
(g_last_error.path[0] != '\0') ? g_last_error.path : path;
notify_system("%s\n%s", g_last_error.message, error_path);
g_last_error.notified = true;
return;
}
notify_system_l10n(SM_L10N_IMAGE_MOUNT_FAILED, (uint32_t)mount_err,
strerror(mount_err), path);
}
+420
View File
@@ -0,0 +1,420 @@
#include "sm_platform.h"
#include "sm_manual.h"
#include "sm_appdb.h"
#include "sm_install_queue.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_paths.h"
#include "sm_title_state.h"
#define MANUAL_STATUS_CAPACITY 1024
typedef enum {
MANUAL_STATUS_UNKNOWN = 0,
MANUAL_STATUS_INSTALLED,
MANUAL_STATUS_DELETED,
} manual_status_t;
typedef struct {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
manual_status_t status;
bool remove_from_list;
} manual_status_entry_t;
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
static void trim_manual_line(char *line) {
if (!line)
return;
char *start = line;
while (*start != '\0' && isspace((unsigned char)*start))
start++;
if (start != line)
memmove(line, start, strlen(start) + 1u);
size_t len = strlen(line);
while (len > 0 && isspace((unsigned char)line[len - 1u])) {
line[len - 1u] = '\0';
len--;
}
while (len > 1 && line[len - 1u] == '/') {
line[len - 1u] = '\0';
len--;
}
}
static void drain_manual_file_line(FILE *f) {
int ch;
while ((ch = fgetc(f)) != EOF && ch != '\n') {
}
}
static void copy_manual_file_line_remainder(FILE *in, FILE *out,
int *saved_errno) {
int ch;
while ((ch = fgetc(in)) != EOF) {
if (saved_errno && *saved_errno == 0 && fputc(ch, out) == EOF)
*saved_errno = errno;
if (ch == '\n')
break;
}
}
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx) {
if (!visit)
return false;
FILE *f = fopen(MANUAL_LIST_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] open failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
return false;
}
char line[MAX_PATH + 64];
bool completed = true;
while (fgets(line, sizeof(line), f) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
trim_manual_line(line);
if (line[0] == '\0' || line[0] == '#') {
if (truncated)
drain_manual_file_line(f);
continue;
}
if (truncated || strlen(line) >= MAX_PATH) {
log_debug(" [MANUAL] path too long, ignored: %.80s", line);
if (truncated)
drain_manual_file_line(f);
continue;
}
if (!visit(line, ctx)) {
completed = false;
break;
}
}
if (ferror(f)) {
log_debug(" [MANUAL] read failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
completed = false;
}
fclose(f);
return completed;
}
static const char *manual_status_name(manual_status_t status) {
switch (status) {
case MANUAL_STATUS_INSTALLED:
return "installed";
case MANUAL_STATUS_DELETED:
return "deleted";
default:
return "unknown";
}
}
static manual_status_t parse_manual_status_name(const char *name) {
if (!name)
return MANUAL_STATUS_UNKNOWN;
if (strcasecmp(name, "installed") == 0)
return MANUAL_STATUS_INSTALLED;
if (strcasecmp(name, "deleted") == 0)
return MANUAL_STATUS_DELETED;
return MANUAL_STATUS_UNKNOWN;
}
static bool parse_manual_status_line(char *line, manual_status_entry_t *entry) {
char *status = strtok(line, "\t");
char *title_id = strtok(NULL, "\t");
char *path = strtok(NULL, "\t");
char *title_name = strtok(NULL, "\n");
if (!status || !title_id || !path)
return false;
trim_manual_line(status);
trim_manual_line(title_id);
trim_manual_line(path);
if (title_name)
trim_manual_line(title_name);
manual_status_t parsed_status = parse_manual_status_name(status);
if (parsed_status == MANUAL_STATUS_UNKNOWN || title_id[0] == '\0' ||
path[0] == '\0') {
return false;
}
memset(entry, 0, sizeof(*entry));
entry->status = parsed_status;
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(void)strlcpy(entry->path, path, sizeof(entry->path));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
return true;
}
static int load_manual_status(void) {
FILE *f = fopen(MANUAL_STATUS_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] status open failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
return 0;
}
int count = 0;
char line[MAX_PATH + MAX_TITLE_NAME + MAX_TITLE_ID + 64];
while (fgets(line, sizeof(line), f) != NULL) {
if (count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status limit reached (%d)",
MANUAL_STATUS_CAPACITY);
break;
}
manual_status_entry_t entry;
if (!parse_manual_status_line(line, &entry))
continue;
g_manual_status_entries[count++] = entry;
}
if (ferror(f))
log_debug(" [MANUAL] status read failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
fclose(f);
return count;
}
static bool write_manual_status(int entry_count) {
mkdir(LOG_DIR, 0777);
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_STATUS_FILE);
FILE *f = fopen(tmp_path, "w");
if (!f) {
log_debug(" [MANUAL] status write open failed for %s: %s", tmp_path,
strerror(errno));
return false;
}
int saved_errno = 0;
for (int i = 0; i < entry_count; i++) {
const manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status == MANUAL_STATUS_UNKNOWN || entry->path[0] == '\0' ||
entry->title_id[0] == '\0') {
continue;
}
if (fprintf(f, "%s\t%s\t%s\t%s\n", manual_status_name(entry->status),
entry->title_id, entry->path, entry->title_name) < 0) {
saved_errno = errno;
break;
}
}
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(" [MANUAL] status write failed for %s: %s", tmp_path,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (rename(tmp_path, MANUAL_STATUS_FILE) != 0) {
log_debug(" [MANUAL] status rename failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
static int find_manual_status_entry(int entry_count, const char *path,
const char *title_id) {
for (int i = 0; i < entry_count; i++) {
if (path && path[0] != '\0' &&
strcmp(g_manual_status_entries[i].path, path) == 0) {
return i;
}
if (title_id && title_id[0] != '\0' &&
strcmp(g_manual_status_entries[i].title_id, title_id) == 0) {
return i;
}
}
return -1;
}
static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
FILE *in = fopen(MANUAL_LIST_FILE, "r");
if (!in) {
if (errno == ENOENT)
return true;
log_debug(" [MANUAL] list rewrite open failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
return false;
}
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_LIST_FILE);
FILE *out = fopen(tmp_path, "w");
if (!out) {
log_debug(" [MANUAL] list rewrite temp open failed for %s: %s", tmp_path,
strerror(errno));
fclose(in);
return false;
}
bool changed = false;
int saved_errno = 0;
char line[MAX_PATH + 64];
while (fgets(line, sizeof(line), in) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
char normalized[MAX_PATH + 64];
(void)strlcpy(normalized, line, sizeof(normalized));
trim_manual_line(normalized);
if (!truncated && normalized[0] != '\0' && normalized[0] != '#') {
bool remove_line = false;
for (int i = 0; i < entry_count; i++) {
if (g_manual_status_entries[i].remove_from_list &&
strcmp(g_manual_status_entries[i].path, normalized) == 0) {
remove_line = true;
break;
}
}
if (remove_line) {
changed = true;
if (truncated)
drain_manual_file_line(in);
continue;
}
}
if (fputs(line, out) == EOF) {
saved_errno = errno;
break;
}
if (truncated)
copy_manual_file_line_remainder(in, out, &saved_errno);
}
if (ferror(in) && saved_errno == 0)
saved_errno = errno;
if (fflush(out) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(out) != 0 && saved_errno == 0)
saved_errno = errno;
fclose(in);
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] list rewrite failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (!changed) {
(void)unlink(tmp_path);
return true;
}
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
log_debug(" [MANUAL] list rewrite rename failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
bool sm_manual_reconcile_deleted_titles(
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready) {
if (!app_db_titles_ready || !app_db_titles)
return true;
int entry_count = load_manual_status();
if (entry_count <= 0)
return true;
bool changed = false;
for (int i = 0; i < entry_count; i++) {
manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status != MANUAL_STATUS_INSTALLED ||
entry->title_id[0] == '\0') {
continue;
}
if (app_db_title_list_contains(app_db_titles, entry->title_id))
continue;
entry->status = MANUAL_STATUS_DELETED;
entry->remove_from_list = true;
sm_install_forget_pending_title(entry->title_id);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
changed = true;
log_debug(" [MANUAL] title removed from app.db, disabling manual entry: "
"%s (%s)",
entry->title_name, entry->title_id);
}
if (!changed)
return true;
if (!rewrite_manual_list_without_deleted_entries(entry_count))
return false;
return write_manual_status(entry_count);
}
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name) {
if (!manual_source_path || manual_source_path[0] == '\0' || !title_id ||
title_id[0] == '\0') {
return;
}
int entry_count = load_manual_status();
int index =
find_manual_status_entry(entry_count, manual_source_path, title_id);
if (index < 0) {
if (entry_count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status full, cannot record %s (%s)",
manual_source_path, title_id);
return;
}
index = entry_count++;
memset(&g_manual_status_entries[index], 0,
sizeof(g_manual_status_entries[index]));
}
manual_status_entry_t *entry = &g_manual_status_entries[index];
manual_status_t previous_status = entry->status;
bool unchanged = previous_status == MANUAL_STATUS_INSTALLED &&
strcmp(entry->path, manual_source_path) == 0 &&
strcmp(entry->title_id, title_id) == 0 &&
(!title_name || title_name[0] == '\0' ||
strcmp(entry->title_name, title_name) == 0);
if (unchanged)
return;
entry->status = MANUAL_STATUS_INSTALLED;
(void)strlcpy(entry->path, manual_source_path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
if (!write_manual_status(entry_count))
return;
if (previous_status != MANUAL_STATUS_INSTALLED) {
log_debug(" [MANUAL] installed status recorded: %s (%s) from %s",
entry->title_name, entry->title_id, entry->path);
}
}
+248
View File
@@ -0,0 +1,248 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_mount_device.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
#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) {
if (backend == ATTACH_BACKEND_LVD)
return "LVD";
if (backend == ATTACH_BACKEND_MD)
return "MD";
return "UNKNOWN";
}
// --- 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)
return true;
sceKernelUsleep(LVD_NODE_WAIT_US);
}
return false;
}
bool is_source_stable_for_mount(const char *path, const char *name,
const char *tag) {
double age = 0.0;
int st_err = 0;
if (is_path_stable_now(path, &age, &st_err))
return true;
if (st_err != 0)
return false;
log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age);
return false;
}
// --- Mounted Device Resolution (/dev/lvdN, /dev/mdN) ---
static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
int *unit_out) {
size_t prefix_len = strlen(prefix);
if (strncmp(dev_path, prefix, prefix_len) != 0)
return false;
char *end = NULL;
long unit = strtol(dev_path + prefix_len, &end, 10);
if (end == dev_path + prefix_len || *end != '\0' || unit < 0 ||
unit > INT_MAX)
return false;
*unit_out = (int)unit;
return true;
}
bool resolve_device_from_mount(const char *mount_point,
attach_backend_t *backend_out, int *unit_out) {
*backend_out = ATTACH_BACKEND_NONE;
*unit_out = -1;
if (resolve_device_from_mount_cache(mount_point, backend_out, unit_out))
return true;
struct statfs sfs;
if (statfs(mount_point, &sfs) != 0)
return false;
if (strcmp(sfs.f_mntonname, mount_point) != 0)
return false;
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/lvd", unit_out)) {
*backend_out = ATTACH_BACKEND_LVD;
return true;
}
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/md", unit_out)) {
*backend_out = ATTACH_BACKEND_MD;
return true;
}
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
if (mntcount <= 0 || !mntbuf)
return false;
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, mount_point) != 0)
continue;
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/lvd", unit_out)) {
*backend_out = ATTACH_BACKEND_LVD;
return true;
}
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/md", unit_out)) {
*backend_out = ATTACH_BACKEND_MD;
return true;
}
}
return false;
}
static bool is_path_mountpoint(const char *path) {
struct statfs sfs;
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
}
bool is_active_image_mount_point(const char *path) {
return is_path_mountpoint(path);
}
bool wait_for_lvd_release(void) {
for (unsigned int waited_us = 0;; waited_us += LVD_RELEASE_WAIT_POLL_US) {
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
bool mounted = false;
for (int i = 0; i < mntcount && mntbuf; i++) {
if (strcmp(mntbuf[i].f_mntfromname, "/dev/lvd2") != 0)
continue;
mounted = true;
break;
}
if (!mounted) {
if (waited_us != 0)
log_debug(" [IMG][LVD] /dev/lvd2 released");
return true;
}
if (waited_us == 0) {
log_debug(" [IMG][LVD] waiting for /dev/lvd2 to be released...");
for (int i = 0; i < mntcount && mntbuf; i++) {
if (strncmp(mntbuf[i].f_mntfromname, "/dev/lvd", 8) != 0)
continue;
log_debug(" [IMG][LVD] mounted: from=%s path=%s type=%s "
"bsize=%llu iosize=%llu blocks=%llu bfree=%llu "
"bavail=%llu files=%llu ffree=%llu flags=0x%lX",
mntbuf[i].f_mntfromname, mntbuf[i].f_mntonname,
mntbuf[i].f_fstypename,
(unsigned long long)(uint64_t)mntbuf[i].f_bsize,
(unsigned long long)(uint64_t)mntbuf[i].f_iosize,
(unsigned long long)(uint64_t)mntbuf[i].f_blocks,
(unsigned long long)(uint64_t)mntbuf[i].f_bfree,
(unsigned long long)(uint64_t)mntbuf[i].f_bavail,
(unsigned long long)(uint64_t)mntbuf[i].f_files,
(unsigned long long)(uint64_t)mntbuf[i].f_ffree,
(unsigned long)mntbuf[i].f_flags);
}
}
if (should_stop_requested())
return false;
if (waited_us >= LVD_RELEASE_WAIT_MAX_US) {
log_debug(" [IMG][LVD] /dev/lvd2 wait timeout reached (%u ms), "
"continuing startup", LVD_RELEASE_WAIT_MAX_US / 1000u);
return true;
}
sceKernelUsleep(LVD_RELEASE_WAIT_POLL_US);
}
}
// --- Device Detach Helpers ---
static bool detach_lvd_unit(int unit_id) {
if (unit_id < 0)
return true;
int fd = open(LVD_CTRL_PATH, O_RDWR);
if (fd < 0) {
log_debug(" [IMG][%s] open %s for detach failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
strerror(errno));
return false;
}
lvd_ioctl_detach_t req;
memset(&req, 0, sizeof(req));
req.device_id = unit_id;
bool ok = true;
if (ioctl(fd, SCE_LVD_IOC_DETACH, &req) != 0) {
log_debug(" [IMG][%s] detach %d failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id, strerror(errno));
ok = false;
}
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;
}
static bool detach_md_unit(int unit_id) {
if (unit_id < 0)
return true;
int fd = open(MD_CTRL_PATH, O_RDWR);
if (fd < 0) {
log_debug(" [IMG][%s] open %s for detach failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
strerror(errno));
return false;
}
struct md_ioctl req;
memset(&req, 0, sizeof(req));
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));
}
}
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;
}
bool detach_attached_unit(attach_backend_t backend, int unit_id) {
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;
}
+306
View File
@@ -0,0 +1,306 @@
#include "sm_platform.h"
#include "sm_path_state.h"
#include "sm_filesystem.h"
#include "sm_hash.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
struct PathStateEntry {
char path[MAX_PATH];
uint8_t missing_param_attempts;
uint8_t image_mount_attempts;
bool missing_param_limit_logged;
bool image_mount_limit_logged;
bool backport_mount_blocked;
bool manual_missing_source_logged;
bool game_info_cached;
bool game_info_valid;
time_t game_info_mtime;
off_t game_info_size;
ino_t game_info_ino;
char game_title_id[MAX_TITLE_ID];
char game_title_name[MAX_TITLE_NAME];
bool valid;
};
static struct PathStateEntry g_path_state[PATH_STATE_CAPACITY];
static uint16_t g_path_state_hash[STATE_HASH_SIZE];
static void rebuild_path_state_hash(void) {
memset(g_path_state_hash, 0, sizeof(g_path_state_hash));
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
uint32_t slot = sm_fnv1a32(g_path_state[k].path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_path_state_hash[slot] == 0) {
g_path_state_hash[slot] = (uint16_t)(k + 1);
break;
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
}
}
static struct PathStateEntry *find_path_state(const char *path) {
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
uint16_t idx = g_path_state_hash[slot];
if (idx == 0)
return NULL;
struct PathStateEntry *entry = &g_path_state[idx - 1u];
if (entry->valid && strcmp(entry->path, path) == 0)
return entry;
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
return NULL;
}
static struct PathStateEntry *create_path_state(const char *path) {
int slot_k = -1;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid) {
slot_k = k;
break;
}
}
if (slot_k < 0) {
int evict_k = -1;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (!path_exists(g_path_state[k].path) &&
!g_path_state[k].manual_missing_source_logged) {
evict_k = k;
break;
}
}
if (evict_k < 0) {
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (g_path_state[k].missing_param_attempts == 0 &&
g_path_state[k].image_mount_attempts == 0 &&
!g_path_state[k].backport_mount_blocked &&
!g_path_state[k].manual_missing_source_logged &&
!g_path_state[k].game_info_cached) {
evict_k = k;
break;
}
}
}
if (evict_k < 0)
evict_k = 0;
memset(&g_path_state[evict_k], 0, sizeof(g_path_state[evict_k]));
rebuild_path_state_hash();
slot_k = evict_k;
}
memset(&g_path_state[slot_k], 0, sizeof(g_path_state[slot_k]));
g_path_state[slot_k].valid = true;
(void)strlcpy(g_path_state[slot_k].path, path,
sizeof(g_path_state[slot_k].path));
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_path_state_hash[slot] == 0) {
g_path_state_hash[slot] = (uint16_t)(slot_k + 1);
return &g_path_state[slot_k];
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
g_path_state[slot_k].valid = false;
g_path_state[slot_k].path[0] = '\0';
rebuild_path_state_hash();
return NULL;
}
static struct PathStateEntry *get_or_create_path_state(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry : create_path_state(path);
}
bool load_cached_game_info(const char *path, const struct stat *param_st,
char *out_id, char *out_name, bool *valid_out) {
*valid_out = false;
struct PathStateEntry *entry = find_path_state(path);
if (!entry || !entry->game_info_cached ||
entry->game_info_mtime != param_st->st_mtime ||
entry->game_info_size != param_st->st_size ||
entry->game_info_ino != param_st->st_ino) {
return false;
}
*valid_out = entry->game_info_valid;
if (!entry->game_info_valid)
return true;
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
(void)strlcpy(out_name, entry->game_title_name, MAX_TITLE_NAME);
return true;
}
void store_cached_game_info(const char *path, const struct stat *param_st,
bool valid, const char *title_id,
const char *title_name) {
struct PathStateEntry *entry = get_or_create_path_state(path);
entry->game_info_cached = true;
entry->game_info_valid = valid;
entry->game_info_mtime = param_st->st_mtime;
entry->game_info_size = param_st->st_size;
entry->game_info_ino = param_st->st_ino;
(void)strlcpy(entry->game_title_id, valid ? title_id : "", MAX_TITLE_ID);
(void)strlcpy(entry->game_title_name, valid ? title_name : "", MAX_TITLE_NAME);
}
void prune_path_state(void) {
bool changed = false;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
if (changed)
rebuild_path_state_hash();
}
void prune_path_state_for_root(const char *root) {
if (!root || root[0] == '\0') {
prune_path_state();
return;
}
bool changed = false;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
if (!path_matches_root_or_child(g_path_state[k].path, root))
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
if (changed)
rebuild_path_state_hash();
}
bool is_missing_param_scan_limited(const char *path) {
if (!is_under_image_mount_base(path))
return false;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return false;
return entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS;
}
void record_missing_param_failure(const char *path) {
if (!is_under_image_mount_base(path))
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (entry->missing_param_attempts < UINT8_MAX)
entry->missing_param_attempts++;
log_debug(" [SCAN] missing/invalid param.json: %s", path);
if (entry->missing_param_attempts == 1)
notify_system_l10n(SM_L10N_MISSING_PARAM, path);
if (entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS &&
!entry->missing_param_limit_logged) {
log_debug(" [SCAN] attempt limit reached (%u), skipping path: %s",
(unsigned)MAX_MISSING_PARAM_SCAN_ATTEMPTS, path);
entry->missing_param_limit_logged = true;
}
}
void clear_missing_param_entry(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->missing_param_attempts = 0;
entry->missing_param_limit_logged = false;
}
bool is_image_mount_limited(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS
: false;
}
uint8_t bump_image_mount_attempts(const char *path) {
struct PathStateEntry *entry = get_or_create_path_state(path);
if (entry->image_mount_attempts < UINT8_MAX)
entry->image_mount_attempts++;
if (entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS &&
!entry->image_mount_limit_logged) {
log_debug(" [IMG] retry limit reached (%u/%u), skipping image: %s",
(unsigned)entry->image_mount_attempts,
(unsigned)MAX_IMAGE_MOUNT_ATTEMPTS, path);
entry->image_mount_limit_logged = true;
}
return entry->image_mount_attempts;
}
void clear_image_mount_attempts(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->image_mount_attempts = 0;
entry->image_mount_limit_logged = false;
}
bool is_backport_mount_blocked(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry->backport_mount_blocked : false;
}
void note_backport_mount_failure(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry)
return;
if (entry->backport_mount_blocked)
return;
entry->backport_mount_blocked = true;
}
void clear_backport_mount_failure(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->backport_mount_blocked = false;
}
bool note_manual_missing_source_once(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry || entry->manual_missing_source_logged)
return false;
entry->manual_missing_source_logged = true;
return true;
}
void clear_manual_missing_source(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->manual_missing_source_logged = false;
}
+60
View File
@@ -0,0 +1,60 @@
#include "sm_platform.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
const char *get_filename_component(const char *path) {
const char *base = strrchr(path, '/');
if (!base)
base = strrchr(path, '\\');
return base ? base + 1 : path;
}
bool path_exists(const char *path) {
struct stat st;
return (path && path[0] != '\0' && stat(path, &st) == 0);
}
bool is_under_image_mount_base(const char *path) {
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
path[image_prefix_len] == '/');
}
bool is_pfsc_image_mount_base_or_child(const char *path) {
return path_matches_root_or_child(path, PFSC_IMAGE_MOUNT_BASE);
}
bool path_matches_root_or_child(const char *path, const char *root) {
if (!path || !root || root[0] == '\0')
return false;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
return false;
return path[root_len] == '\0' || path[root_len] == '/';
}
bool is_usb_storage_path(const char *path) {
static const char *usb_roots[] = {
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3",
"/mnt/usb4", "/mnt/usb5", "/mnt/usb6", "/mnt/usb7",
"/mnt/ext0",
};
if (!path || path[0] == '\0')
return false;
for (size_t i = 0; i < sizeof(usb_roots) / sizeof(usb_roots[0]); i++) {
if (path_matches_root_or_child(path, usb_roots[i]))
return true;
}
return false;
}
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
if (is_under_image_mount_base(scan_path))
return false;
int written = snprintf(out, MAX_PATH, "%s/%s", scan_path,
DEFAULT_BACKPORTS_DIR_NAME);
return written >= 0 && (size_t)written < MAX_PATH;
}
+903
View File
@@ -0,0 +1,903 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_scan_tree.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_gameinfo.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_mount_device.h"
#include "sm_filesystem.h"
#include "sm_appdb.h"
#include "sm_paths.h"
#include "sm_path_state.h"
#include "sm_path_utils.h"
#include "sm_stability.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_image.h"
#include "sm_install_queue.h"
#include "sm_manual.h"
typedef struct {
char discovered_param_roots[MAX_PENDING][MAX_PATH];
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
char blocked_ppsa_uninstall_titles[MAX_PENDING][MAX_TITLE_ID];
bool checked_appmeta_present[MAX_PENDING];
int checked_appmeta_count;
int blocked_ppsa_uninstall_count;
} scan_workspace_t;
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
// state on the stack each cycle.
static scan_workspace_t g_scan_workspace;
static void reset_scan_workspace(void) {
g_scan_workspace.checked_appmeta_count = 0;
g_scan_workspace.blocked_ppsa_uninstall_count = 0;
}
static bool blocked_ppsa_uninstall_requested(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return false;
for (int i = 0; i < g_scan_workspace.blocked_ppsa_uninstall_count; i++) {
if (strcmp(g_scan_workspace.blocked_ppsa_uninstall_titles[i], title_id) == 0)
return true;
}
return false;
}
static void remember_blocked_ppsa_uninstall(const char *title_id) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return;
if (g_scan_workspace.blocked_ppsa_uninstall_count >= MAX_PENDING)
return;
int slot = g_scan_workspace.blocked_ppsa_uninstall_count++;
(void)strlcpy(g_scan_workspace.blocked_ppsa_uninstall_titles[slot], title_id,
sizeof(g_scan_workspace.blocked_ppsa_uninstall_titles[slot]));
}
static bool is_blocked_ppsa_title(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready, const char *title_id) {
return blocked_ppsa_titles_ready &&
app_db_title_list_contains(blocked_ppsa_titles, title_id);
}
static bool request_blocked_ppsa_uninstall(const char *title_id,
const char *source_path) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return false;
remember_blocked_ppsa_uninstall(title_id);
int res = sceAppInstUtilAppUnInstall(title_id);
if (res == 0) {
sm_install_forget_pending_title(title_id);
invalidate_app_db_title_cache();
if (source_path && source_path[0] != '\0') {
log_debug(" [REG] requested blocked PPSA uninstall: %s source=%s",
title_id, source_path);
} else {
log_debug(" [REG] requested blocked PPSA uninstall: %s", title_id);
}
return true;
}
log_debug(" [REG] blocked PPSA uninstall failed: %s code=0x%08X", title_id,
(uint32_t)res);
return false;
}
static bool get_appmeta_present_for_scan_cycle(const char *title_id) {
for (int i = 0; i < g_scan_workspace.checked_appmeta_count; i++) {
if (strcmp(g_scan_workspace.checked_appmeta_titles[i], title_id) == 0)
return g_scan_workspace.checked_appmeta_present[i];
}
bool present = has_appmeta_data(title_id);
if (g_scan_workspace.checked_appmeta_count < MAX_PENDING) {
int slot = g_scan_workspace.checked_appmeta_count++;
(void)strlcpy(g_scan_workspace.checked_appmeta_titles[slot], title_id,
sizeof(g_scan_workspace.checked_appmeta_titles[slot]));
g_scan_workspace.checked_appmeta_present[slot] = present;
}
return present;
}
static bool is_under_discovered_param_root(
const char *path, char discovered_param_roots[][MAX_PATH],
int discovered_count) {
for (int i = 0; i < discovered_count; i++) {
const char *root = discovered_param_roots[i];
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] == '\0' || path[root_len] == '/')
return true;
}
return false;
}
// --- Candidate Discovery ---
typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
} directory_candidate_info_t;
typedef enum {
DIRECTORY_CANDIDATE_DESCEND = 0,
DIRECTORY_CANDIDATE_SKIP_DESCEND,
DIRECTORY_CANDIDATE_READY,
} directory_candidate_probe_t;
typedef enum {
EXISTING_DIRECTORY_CONTINUE = 0,
EXISTING_DIRECTORY_HANDLED,
EXISTING_DIRECTORY_PREFER_CURRENT,
EXISTING_DIRECTORY_PREFER_CACHED,
} existing_directory_result_t;
typedef struct {
const struct AppDbTitleList *titles;
const struct AppDbTitleList *blocked_ppsa_titles;
bool titles_ready;
bool blocked_ppsa_titles_ready;
} scan_app_db_context_t;
static directory_candidate_probe_t probe_directory_candidate(
const char *full_path, char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
directory_candidate_info_t *info_out) {
struct stat param_st;
if (is_pfsc_image_mount_base_or_child(full_path))
return DIRECTORY_CANDIDATE_DESCEND;
if (!allow_known_param_root &&
is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (is_under_image_mount_base(full_path) && !is_active_image_mount_point(full_path)) {
log_debug(" [SKIP] inactive mount path: %s", full_path);
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (!directory_has_param_json(full_path, &param_st)) {
if (is_missing_param_scan_limited(full_path)) {
log_debug(" [SKIP] param.json retry limit reached: %s", full_path);
} else {
record_missing_param_failure(full_path);
}
return DIRECTORY_CANDIDATE_DESCEND;
}
if (!get_game_info(full_path, &param_st, info_out->title_id,
info_out->title_name)) {
record_missing_param_failure(full_path);
log_debug(" [SKIP] game info unavailable: %s", full_path);
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (*discovered_param_root_count < MAX_PENDING &&
!is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
MAX_PATH);
(*discovered_param_root_count)++;
}
clear_missing_param_entry(full_path);
return DIRECTORY_CANDIDATE_READY;
}
static int find_scan_candidate_index_by_title_id(scan_candidate_t *candidates,
int candidate_count,
const char *title_id) {
for (int i = 0; i < candidate_count; i++) {
if (strcmp(candidates[i].title_id, title_id) == 0)
return i;
}
return -1;
}
static void mark_scan_candidate_manual(scan_candidate_t *candidate,
const char *manual_source_path) {
if (!candidate || !manual_source_path || manual_source_path[0] == '\0')
return;
candidate->manual = true;
(void)strlcpy(candidate->manual_source_path, manual_source_path,
sizeof(candidate->manual_source_path));
}
static void remove_scan_candidate_at(scan_candidate_t *candidates,
int *candidate_count, int index) {
int trailing_count = *candidate_count - index - 1;
if (trailing_count > 0) {
memmove(&candidates[index], &candidates[index + 1],
(size_t)trailing_count * sizeof(candidates[0]));
}
(*candidate_count)--;
}
static void notify_duplicate_scan_candidate(const char *title_id,
const char *ignored_path,
const char *existing_path) {
notify_duplicate_title_once(title_id, ignored_path, existing_path);
}
static existing_directory_result_t handle_existing_directory_candidate(
const char *full_path, const scan_app_db_context_t *app_db,
const directory_candidate_info_t *info,
bool *installed_out, bool *in_app_db_out,
char preferred_existing_path_out[MAX_PATH]) {
char tracked_path[MAX_PATH];
preferred_existing_path_out[0] = '\0';
bool has_tracked_path =
read_mount_link(info->title_id, tracked_path, sizeof(tracked_path));
bool link_matches_source =
has_tracked_path && strcmp(tracked_path, full_path) == 0;
if (is_blocked_ppsa_title(app_db->blocked_ppsa_titles,
app_db->blocked_ppsa_titles_ready,
info->title_id)) {
request_blocked_ppsa_uninstall(info->title_id, full_path);
return EXISTING_DIRECTORY_HANDLED;
}
if (!app_db->titles_ready) {
if (link_matches_source && is_data_mounted(info->title_id)) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
return EXISTING_DIRECTORY_HANDLED;
}
bool in_app_db = app_db_title_list_contains(app_db->titles, info->title_id);
bool installed = in_app_db && is_installed(info->title_id);
bool appmeta_present =
installed ? get_appmeta_present_for_scan_cycle(info->title_id) : false;
link_matches_source = installed && link_matches_source;
bool source_valid = false;
if (link_matches_source) {
source_valid = is_data_mounted(info->title_id);
if (!source_valid && mount_title_nullfs(info->title_id, full_path))
source_valid = is_data_mounted(info->title_id);
}
if (source_valid && appmeta_present) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
if (installed && appmeta_present && has_tracked_path &&
strcmp(tracked_path, full_path) != 0 &&
is_data_mounted(info->title_id)) {
(void)strlcpy(preferred_existing_path_out, tracked_path, MAX_PATH);
cache_game_entry(tracked_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CACHED;
}
if (!in_app_db && is_title_install_pending(info->title_id))
return EXISTING_DIRECTORY_HANDLED;
if (!in_app_db && was_register_attempted(info->title_id)) {
return EXISTING_DIRECTORY_HANDLED;
}
*installed_out = installed;
*in_app_db_out = in_app_db;
return EXISTING_DIRECTORY_CONTINUE;
}
static bool enqueue_directory_candidate(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const directory_candidate_info_t *info, bool installed,
bool in_app_db, const char *manual_source_path,
bool *unstable_found_out) {
char metadata_path[MAX_PATH];
uint8_t failed_attempts = get_failed_mount_attempts(info->title_id);
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [SKIP] mount/register retry limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts, (unsigned)MAX_FAILED_MOUNT_ATTEMPTS,
info->title_name, info->title_id);
return true;
}
int written =
snprintf(metadata_path, sizeof(metadata_path), "%s/sce_sys", full_path);
if (written < 0 || (size_t)written >= sizeof(metadata_path)) {
log_debug(" [SKIP] metadata path too long: %s (%s)", info->title_name,
full_path);
return true;
}
if (!wait_for_stability_fast(metadata_path, info->title_name)) {
if (unstable_found_out)
*unstable_found_out = true;
log_debug(" [SKIP] source not stable yet: %s (%s)", info->title_name,
full_path);
return true;
}
if (*candidate_count >= max_candidates) {
log_debug(" [SKIP] candidate queue full (%d): %s (%s)", max_candidates,
info->title_name, info->title_id);
return true;
}
(void)strlcpy(candidates[*candidate_count].path, full_path,
sizeof(candidates[*candidate_count].path));
(void)strlcpy(candidates[*candidate_count].title_id, info->title_id,
sizeof(candidates[*candidate_count].title_id));
(void)strlcpy(candidates[*candidate_count].title_name, info->title_name,
sizeof(candidates[*candidate_count].title_name));
candidates[*candidate_count].manual_source_path[0] = '\0';
candidates[*candidate_count].installed = installed;
candidates[*candidate_count].in_app_db = in_app_db;
candidates[*candidate_count].manual = false;
mark_scan_candidate_manual(&candidates[*candidate_count], manual_source_path);
(*candidate_count)++;
return true;
}
static bool try_collect_candidate_for_directory(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
const char *manual_source_path, bool *unstable_found_out) {
directory_candidate_info_t info;
directory_candidate_probe_t probe_result =
probe_directory_candidate(full_path, discovered_param_roots,
discovered_param_root_count,
allow_known_param_root, &info);
if (probe_result == DIRECTORY_CANDIDATE_SKIP_DESCEND)
return true;
if (probe_result == DIRECTORY_CANDIDATE_DESCEND)
return false;
int duplicate_candidate_index = find_scan_candidate_index_by_title_id(
candidates, *candidate_count, info.title_id);
const char *duplicate_candidate_path =
duplicate_candidate_index >= 0 ? candidates[duplicate_candidate_index].path
: NULL;
bool duplicate_candidate_same_path =
duplicate_candidate_path && strcmp(duplicate_candidate_path, full_path) == 0;
bool installed = false;
bool in_app_db = false;
char preferred_existing_path[MAX_PATH];
existing_directory_result_t existing_result =
handle_existing_directory_candidate(full_path, app_db, &info,
&installed, &in_app_db,
preferred_existing_path);
if (existing_result == EXISTING_DIRECTORY_PREFER_CURRENT) {
if (manual_source_path)
sm_manual_note_installed(manual_source_path, info.title_id,
info.title_name);
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, duplicate_candidate_path,
full_path);
remove_scan_candidate_at(candidates, candidate_count,
duplicate_candidate_index);
}
return true;
}
if (existing_result == EXISTING_DIRECTORY_PREFER_CACHED) {
notify_duplicate_scan_candidate(info.title_id, full_path,
preferred_existing_path);
if (duplicate_candidate_index >= 0)
remove_scan_candidate_at(candidates, candidate_count,
duplicate_candidate_index);
return true;
}
if (existing_result == EXISTING_DIRECTORY_HANDLED) {
if (duplicate_candidate_index >= 0 && !duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
if (duplicate_candidate_index >= 0)
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
return enqueue_directory_candidate(full_path, candidates, max_candidates,
candidate_count, &info, installed,
in_app_db, manual_source_path,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
const char *manual_source_path;
bool *unstable_found_out;
} collect_candidates_walk_ctx_t;
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out);
static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
if (depth_from_root == 0u)
return SM_SCAN_TREE_DIR_DESCEND;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
if (try_collect_candidate_for_directory(
dir_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, false, ctx->manual_source_path,
ctx->unstable_found_out)) {
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
}
return SM_SCAN_TREE_DIR_DESCEND;
}
static bool collect_candidate_image_visit(const char *image_path,
const char *image_name,
unsigned int depth_from_root,
void *ctx_ptr) {
(void)depth_from_root;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
if (!maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out))
return true;
if (ctx->manual_source_path && is_pfsc_image_mount_base_or_child(image_path)) {
char mount_point[MAX_PATH];
get_image_mount_point_for_source(image_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, ctx->manual_source_path, ctx->candidates,
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
ctx->discovered_param_roots, ctx->discovered_param_root_count,
ctx->unstable_found_out);
}
return true;
}
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
bool pfsc_container_root = is_pfsc_image_mount_base_or_child(scan_path);
bool try_root_candidate = !pfsc_container_root;
if (try_root_candidate && is_under_image_mount_base(scan_path)) {
struct stat param_st;
try_root_candidate = directory_has_param_json(scan_path, &param_st);
}
if (try_root_candidate) {
if (try_collect_candidate_for_directory(
scan_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count, true,
manual_source_path, unstable_found_out)) {
return;
}
}
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = manual_source_path,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = pfsc_container_root ? collect_candidate_image_visit
: NULL,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_path(
const char *manual_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
struct stat st;
if (stat(manual_path, &st) != 0) {
if (note_manual_missing_source_once(manual_path))
log_debug(" [MANUAL] source unavailable: %s", manual_path);
return;
}
clear_manual_missing_source(manual_path);
const char *name = get_filename_component(manual_path);
if (S_ISREG(st.st_mode) &&
is_supported_image_file_path(manual_path, name)) {
if (!maybe_mount_image_file(manual_path, name, unstable_found_out))
return;
char mount_point[MAX_PATH];
get_image_mount_point_for_source(manual_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
return;
}
struct stat param_st;
if (!S_ISDIR(st.st_mode) || !directory_has_param_json(manual_path, &param_st)) {
log_debug(" [MANUAL] expected game directory or image file: %s",
manual_path);
return;
}
collect_scan_candidates_from_manual_root(
manual_path, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
bool *unstable_found_out;
int path_count;
} manual_scan_ctx_t;
static bool collect_manual_path_visit(const char *manual_path, void *ctx_ptr) {
manual_scan_ctx_t *ctx = (manual_scan_ctx_t *)ctx_ptr;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (*ctx->candidate_count >= ctx->max_candidates) {
log_debug(" [MANUAL] candidate queue full (%d), remaining sources deferred",
ctx->max_candidates);
return false;
}
ctx->path_count++;
collect_scan_candidates_from_manual_path(
manual_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, ctx->unstable_found_out);
return true;
}
static bool build_backport_path_for_scan_root(const char *title_id,
const char *scan_path,
char backport_path[MAX_PATH]) {
if (!title_id || title_id[0] == '\0' || !scan_path || scan_path[0] == '\0')
return false;
char backport_root[MAX_PATH];
if (!build_backports_root_path(scan_path, backport_root))
return false;
int written = snprintf(backport_path, MAX_PATH, "%s/%s", backport_root,
title_id);
return written > 0 && (size_t)written < MAX_PATH;
}
static bool backport_path_is_dir(const char *backport_path) {
struct stat st;
return backport_path && stat(backport_path, &st) == 0 &&
S_ISDIR(st.st_mode);
}
static bool resolve_backport_path(const char *title_id,
const char *owning_scan_path,
char backport_path[MAX_PATH]) {
if (!title_id || title_id[0] == '\0')
return false;
backport_path[0] = '\0';
if (build_backport_path_for_scan_root(title_id, owning_scan_path,
backport_path) &&
backport_path_is_dir(backport_path)) {
return true;
}
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (owning_scan_path && strcmp(scan_path, owning_scan_path) == 0)
continue;
char candidate_path[MAX_PATH];
if (!build_backport_path_for_scan_root(title_id, scan_path, candidate_path))
continue;
if (!backport_path_is_dir(candidate_path))
continue;
(void)strlcpy(backport_path, candidate_path, MAX_PATH);
return true;
}
backport_path[0] = '\0';
return false;
}
typedef struct {
bool *unstable_found_out;
} backport_overlay_ctx_t;
static bool mount_backport_overlay_for_cached_game(const char *source_path,
const char *title_id,
const char *title_name,
const char *owning_scan_root,
void *ctx_ptr) {
(void)title_name;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
backport_overlay_ctx_t *ctx = (backport_overlay_ctx_t *)ctx_ptr;
char backport_path[MAX_PATH];
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
return true;
}
if (is_backport_mount_blocked(backport_path))
return true;
bool overlay_active = false;
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
&overlay_active) ||
overlay_active) {
return true;
}
if (!wait_for_stability_fast(backport_path, "BKP")) {
if (ctx->unstable_found_out)
*ctx->unstable_found_out = true;
return true;
}
overlay_active = false;
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
&overlay_active) ||
overlay_active) {
return true;
}
char system_ex_path[MAX_PATH];
snprintf(system_ex_path, sizeof(system_ex_path), "/system_ex/app/%s",
title_id);
if (!mount_backport_overlay(system_ex_path, backport_path, title_id)) {
note_backport_mount_failure(backport_path);
return true;
}
clear_backport_mount_failure(backport_path);
return true;
}
void mount_backport_overlays(bool *unstable_found_out) {
backport_overlay_ctx_t ctx = {
.unstable_found_out = unstable_found_out,
};
for_each_cached_game_entry(NULL, mount_backport_overlay_for_cached_game, &ctx);
}
// --- Unified Scan Pass (images + game candidates) ---
void cleanup_lost_sources_before_scan(void) {
// 1) Drop stale game cache entries for deleted sources.
prune_game_cache();
// 2) Drop stale/broken mount links and unmount stale /system_ex stacks.
cleanup_mount_links(NULL, true);
// 3) Unmount stale image mounts for deleted image files.
cleanup_stale_image_mounts();
// 4) Drop stale path-state entries.
prune_path_state();
}
void cleanup_lost_sources_for_scan_root(const char *scan_root) {
prune_game_cache_for_root(scan_root);
cleanup_mount_links(scan_root, true);
cleanup_stale_image_mounts_for_root(scan_root);
prune_path_state_for_root(scan_root);
}
void unmount_usb_sources_for_suspend(void) {
log_debug("[SLEEP] USB-backed mount cleanup begin");
cleanup_usb_mount_links_for_suspend();
if (!unmount_usb_image_mounts_for_suspend()) {
log_debug("[SLEEP] some USB-backed image mounts remain busy");
}
cleanup_mount_dirs();
log_debug("[SLEEP] USB-backed mount cleanup done");
}
static void collect_scan_candidates_from_root(
const char *scan_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = NULL,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = collect_candidate_image_visit,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_list(
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
(void)sm_manual_reconcile_deleted_titles(app_db->titles,
app_db->titles_ready);
manual_scan_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.unstable_found_out = unstable_found_out,
.path_count = 0,
};
if (!sm_manual_for_each_path(collect_manual_path_visit, &ctx) ||
ctx.path_count <= 0) {
return;
}
log_debug(" [MANUAL] scanned %d manual source(s)", ctx.path_count);
}
static void uninstall_blocked_ppsa_titles(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready) {
if (!blocked_ppsa_titles_ready || !blocked_ppsa_titles)
return;
int requested = 0;
for (int i = 0; i < blocked_ppsa_titles->count &&
!should_stop_requested() && !runtime_sleep_mode_active();
i++) {
const char *title_id = blocked_ppsa_titles->ids[i];
if (request_blocked_ppsa_uninstall(title_id, NULL))
requested++;
}
if (requested > 0)
log_debug(" [REG] requested %d blocked PPSA uninstall(s)", requested);
}
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
collect_scan_candidates_from_root(scan_root, candidates, max_candidates,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
if (total_found_out)
*total_found_out = discovered_param_root_count;
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
for (int i = 0; i < get_scan_path_count(); i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
break;
collect_scan_candidates_from_root(get_scan_path(i), candidates,
max_candidates,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
}
collect_scan_candidates_from_manual_list(
candidates, max_candidates, &candidate_count, &app_db,
g_scan_workspace.discovered_param_roots, &discovered_param_root_count,
unstable_found_out);
uninstall_blocked_ppsa_titles(&blocked_ppsa_titles,
blocked_ppsa_titles_ready);
if (total_found_out)
*total_found_out = discovered_param_root_count;
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
+124
View File
@@ -0,0 +1,124 @@
#include "sm_platform.h"
#include "sm_scan_tree.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
#include "sm_runtime.h"
static void classify_scan_tree_entry(const char *full_path, unsigned char d_type,
bool *is_dir_out, bool *is_regular_out) {
bool is_dir = false;
bool is_regular = false;
if (d_type == DT_DIR) {
is_dir = true;
} else if (d_type == DT_REG) {
is_regular = true;
} else if (d_type == DT_UNKNOWN) {
struct stat st;
if (lstat(full_path, &st) == 0) {
is_dir = S_ISDIR(st.st_mode);
is_regular = S_ISREG(st.st_mode);
}
}
*is_dir_out = is_dir;
*is_regular_out = is_regular;
}
static bool is_distinct_configured_scan_root(const char *current_scan_root,
const char *path) {
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (strcmp(scan_path, path) != 0)
continue;
if (strcmp(current_scan_root, path) == 0)
return false;
return true;
}
return false;
}
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
unsigned int depth_from_root,
unsigned int remaining_depth,
const sm_scan_tree_callbacks_t *callbacks, void *ctx) {
if (should_stop_requested() || runtime_sleep_mode_active())
return true;
if (depth_from_root > 0u &&
is_distinct_configured_scan_root(scan_root, dir_path)) {
return true;
}
sm_scan_tree_dir_visit_t dir_visit = SM_SCAN_TREE_DIR_DESCEND;
if (callbacks->on_directory) {
dir_visit = callbacks->on_directory(dir_path, depth_from_root, ctx);
if (dir_visit == SM_SCAN_TREE_DIR_ABORT)
return false;
if (dir_visit == SM_SCAN_TREE_DIR_SKIP_DESCEND)
return true;
}
if (remaining_depth == 0u)
return true;
DIR *d = opendir(dir_path);
if (!d)
return true;
bool scan_root_is_pfsc_mount_base =
is_pfsc_image_mount_base_or_child(scan_root);
bool skip_backports_root =
(depth_from_root == 0u && !is_under_image_mount_base(scan_root));
bool allow_image_file_visits =
callbacks->on_image_file &&
(!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE) ||
scan_root_is_pfsc_mount_base);
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested() || runtime_sleep_mode_active()) {
closedir(d);
return true;
}
if (entry->d_name[0] == '.')
continue;
if (skip_backports_root &&
strcmp(entry->d_name, DEFAULT_BACKPORTS_DIR_NAME) == 0) {
continue;
}
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
bool is_dir = false;
bool is_regular = false;
classify_scan_tree_entry(full_path, entry->d_type, &is_dir, &is_regular);
if (allow_image_file_visits && is_regular &&
is_supported_image_file_path(full_path, entry->d_name)) {
if (!callbacks->on_image_file(full_path, entry->d_name, depth_from_root + 1u,
ctx)) {
closedir(d);
return false;
}
}
if (!is_dir)
continue;
if (!sm_scan_tree_walk(scan_root, full_path, depth_from_root + 1u,
remaining_depth - 1u, callbacks, ctx)) {
closedir(d);
return false;
}
}
closedir(d);
return true;
}
+1669
View File
File diff suppressed because it is too large. Load diff
+801
View File
@@ -0,0 +1,801 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_shellcore_flags.h"
#define SHELLCORE_FLAG_WAITMODE_OR 2u
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
#define SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS 0x0000000000200000ULL
typedef intptr_t sm_kernel_event_flag_t;
int sceKernelOpenEventFlag(sm_kernel_event_flag_t *ef, const char *name);
int sceKernelPollEventFlag(sm_kernel_event_flag_t ef, uint64_t bit_pattern,
unsigned int wait_mode, uint64_t *result_pattern);
int sceKernelCloseEventFlag(sm_kernel_event_flag_t ef);
typedef struct {
const char *name;
sm_kernel_event_flag_t handle;
uint64_t last_pattern;
int last_rc;
bool required;
bool is_open;
bool has_last_pattern;
bool has_last_rc;
} shellcore_flag_monitor_t;
typedef struct {
uint64_t mask;
const char *name;
} shellcore_flag_bit_desc_t;
static const shellcore_flag_bit_desc_t g_lnc_util_system_status_bits[] = {
{0x0000000000000001ULL, "EXTRA_AUDIO_CPU_BUDGET_AVAILABLE"},
{0x0000000000000002ULL, "SHELLUI_FG_GAME_BG_CPU_MODE"},
};
static shellcore_flag_monitor_t g_shellcore_flags[] = {
#if 0
{.name = "SceShellCoreUtilAppFocus", .handle = -1, .required = true},
#endif
{.name = "SceSystemStateMgrInfo", .handle = -1, .required = true},
{.name = "SceLncUtilSystemStatus", .handle = -1, .required = false},
{.name = "SceSystemStateMgrStatus", .handle = -1},
/* {.name = "SceSysCoreSuspend", .handle = -1},
{.name = "SceShellCoreUtilPowerControl", .handle = -1},
{.name = "SceShellCoreUtilRunLevel", .handle = -1},
{.name = "SceShellCoreUtilUIStatus", .handle = -1},
{.name = "SceAutoMountUsbMass", .handle = -1},
{.name = "SceVshctlInternalUserFlag", .handle = -1},
{.name = "SceBootStatusFlags", .handle = -1},
{.name = "SceSysCoreReboot", .handle = -1},
*/
};
static pthread_t g_shellcore_flag_thread;
static pthread_mutex_t g_shellcore_flag_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_shellcore_flag_cond = PTHREAD_COND_INITIALIZER;
static bool g_shellcore_flag_thread_started = false;
static bool g_shellcore_flag_start_ready = false;
static bool g_shellcore_flag_start_success = false;
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
{0x0000000000000004ULL, "BD_DRIVE_READY"},
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
{0x0000000000000020ULL, "TICK_0x20"},
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
{0x0000000000000200ULL, "REBOOT_REQUEST"},
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
{SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS,
"SHELLUI_SHUTDOWN_IN_PROGRESS"},
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
};
#if 0
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
{0x0000000000040000ULL, "POWER_TRANSITION"},
{0x0000000000080000ULL, "POWER_LOCK"},
{0x0000000000100000ULL, "SOCIAL_SCREEN_STATUS"},
{0x0000000000200000ULL, "REQUEST_CAMERA_CALIBRATION"},
{0x0000000002000000ULL, "USER_INPUT_PULSE"},
{0x0000000004000000ULL, "IMPOSE_OVERLAY"},
{0x0000000008000000ULL, "AUTO_POWER_DOWN_TIMER_RESET"},
{0x0000000010000000ULL, "REMOTE_PLAY_STATUS"},
{0x0000000020000000ULL, "SHARE_PLAY_STATUS"},
{0x0000000040000000ULL, "GAME_LIVE_STREAMING"},
{0x0000000080000000ULL, "BGM_CPU_BUDGET"},
{0x0000000100000000ULL, "PRENOTIFY_REMOTE_PLAY"},
{0x0000000200000000ULL, "PRENOTIFY_SHARE_PLAY"},
{0x0000000400000000ULL, "PRENOTIFY_GAME_LIVE_STREAMING"},
{0x0000000800000000ULL, "REMOTE_PLAY_CPU_BUDGET"},
{0x0000001000000000ULL, "SHARE_PLAY_CPU_BUDGET"},
{0x0000100000000000ULL, "EXT_STORAGE_APP_MOVE_IN_PROGRESS"},
{0x0000200000000000ULL, "STATE_0x2000000000000"},
};
static const shellcore_flag_bit_desc_t g_shellcore_ui_status_bits[] = {
{0x0000000000000001ULL, "LOW_BIT_0x1"},
{0x0000000000000010ULL, "LOW_BIT_0x10"},
{0x0000000000018000ULL, "BASIC_PRODUCT_SHAPE_FIELD"},
{0x0000000000020000ULL, "EXT_HDD_MOUNT_UNBLOCK"},
{0x0000000400000000ULL, "HIGH32_BIT2"},
};
static const shellcore_flag_bit_desc_t g_vshctl_internal_user_bits[] = {
{0x0000000000000001ULL, "INTERNAL_CRONOS_USER"},
};
static const shellcore_flag_bit_desc_t g_boot_status_bits[] = {
{0x0000000000000002ULL, "BIT_0x2"},
{0x0000000000000040ULL, "BIT_0x40"},
{0x0000000000000080ULL, "BIT_0x80"},
{0x0000000000000200ULL, "BIT_0x200"},
{0x0000000000004000ULL, "BIT_0x4000"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
};
static const shellcore_flag_bit_desc_t g_syscore_reboot_bits[] = {
{0x0000000000000040ULL, "BIT_0x40"},
{0x0000000000004000ULL, "BIT_0x4000"},
{0x0000000100000000ULL, "REBOOT_FLAG"},
};
static const shellcore_flag_bit_desc_t g_syscore_suspend_bits[] = {
{0x0000000000000001ULL, "SUSPEND_ACK"},
{0x0000000000000002ULL, "SUSPEND_REQ_BIT1"},
{0x0000000000000004ULL, "SUSPEND_REQ_BIT2"},
{0x0000000000000008ULL, "SUSPEND_REQ_BIT3"},
};
#endif
enum {
SYSTEM_STATE_MGR_STATE_INVALID = 0u,
SYSTEM_STATE_MGR_STATE_INITIALIZING = 10u,
SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING = 100u,
SYSTEM_STATE_MGR_STATE_POWER_SAVING = 200u,
SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING = 300u,
SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY = 500u,
SYSTEM_STATE_MGR_STATE_WORKING = 1000u,
};
enum {
SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE = 0u,
SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF = 1u,
};
static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
const shellcore_flag_monitor_t *flag, size_t *count_out) {
if (count_out)
*count_out = 0;
if (!flag || !flag->name)
return NULL;
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_lnc_util_system_status_bits) /
sizeof(g_lnc_util_system_status_bits[0]);
}
return g_lnc_util_system_status_bits;
}
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_system_state_mgr_status_bits) /
sizeof(g_system_state_mgr_status_bits[0]);
}
return g_system_state_mgr_status_bits;
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
if (count_out)
*count_out = sizeof(g_power_control_bits) / sizeof(g_power_control_bits[0]);
return g_power_control_bits;
}
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_shellcore_ui_status_bits) /
sizeof(g_shellcore_ui_status_bits[0]);
}
return g_shellcore_ui_status_bits;
}
if (strcmp(flag->name, "SceVshctlInternalUserFlag") == 0) {
if (count_out) {
*count_out = sizeof(g_vshctl_internal_user_bits) /
sizeof(g_vshctl_internal_user_bits[0]);
}
return g_vshctl_internal_user_bits;
}
if (strcmp(flag->name, "SceBootStatusFlags") == 0) {
if (count_out)
*count_out = sizeof(g_boot_status_bits) / sizeof(g_boot_status_bits[0]);
return g_boot_status_bits;
}
if (strcmp(flag->name, "SceSysCoreReboot") == 0) {
if (count_out)
*count_out = sizeof(g_syscore_reboot_bits) / sizeof(g_syscore_reboot_bits[0]);
return g_syscore_reboot_bits;
}
if (strcmp(flag->name, "SceSysCoreSuspend") == 0) {
if (count_out)
*count_out = sizeof(g_syscore_suspend_bits) /
sizeof(g_syscore_suspend_bits[0]);
return g_syscore_suspend_bits;
}
#endif
return NULL;
}
static const char *get_system_state_mgr_state_name(unsigned state) {
switch (state) {
case SYSTEM_STATE_MGR_STATE_INVALID:
return "INVALID";
case SYSTEM_STATE_MGR_STATE_INITIALIZING:
return "INITIALIZING";
case SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING:
return "SHUTDOWN_ON_GOING";
case SYSTEM_STATE_MGR_STATE_POWER_SAVING:
return "POWER_SAVING";
case SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING:
return "SUSPEND_ON_GOING";
case SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY:
return "MAIN_ON_STANDBY";
case SYSTEM_STATE_MGR_STATE_WORKING:
return "WORKING";
default:
return "UNKNOWN";
}
}
static const char *get_system_state_mgr_reboot_cause_name(unsigned cause) {
switch (cause) {
case SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE:
return "NONE";
case SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF:
return "CFF";
default:
return "UNKNOWN";
}
}
static void append_shellcore_flag_token(char *dst, size_t dst_size,
const char *token) {
size_t used;
if (!dst || dst_size == 0 || !token || token[0] == '\0')
return;
used = strnlen(dst, dst_size);
if (used >= dst_size - 1)
return;
(void)snprintf(dst + used, dst_size - used, "%s%s", used == 0 ? "" : "|",
token);
}
static bool format_shellcore_flag_special_known_masks(
const shellcore_flag_monitor_t *flag, char *dst, size_t dst_size) {
if (!flag || !flag->name || !dst || dst_size == 0)
return false;
#if 0
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
append_shellcore_flag_token(dst, dst_size, "RUN_LEVEL_NUMERIC");
append_shellcore_flag_token(dst, dst_size, "POLL_MASK=0xFFFFFFF");
append_shellcore_flag_token(dst, dst_size, "INITIAL=10");
append_shellcore_flag_token(dst, dst_size, "READY=100");
return true;
}
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
append_shellcore_flag_token(dst, dst_size, "USB_MOUNT_BITS=0xFFFFFFF");
return true;
}
#endif
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
append_shellcore_flag_token(dst, dst_size,
"CURRENT_STATE=bits[0..15]");
append_shellcore_flag_token(dst, dst_size,
"TRIGGER_CODE=bits[32..47]");
append_shellcore_flag_token(dst, dst_size,
"REBOOT_CAUSE=bits[48..63]");
append_shellcore_flag_token(dst, dst_size, "STATE_INVALID=0");
append_shellcore_flag_token(dst, dst_size, "STATE_INITIALIZING=10");
append_shellcore_flag_token(dst, dst_size,
"STATE_SHUTDOWN_ON_GOING=100");
append_shellcore_flag_token(dst, dst_size, "STATE_POWER_SAVING=200");
append_shellcore_flag_token(dst, dst_size,
"STATE_SUSPEND_ON_GOING=300");
append_shellcore_flag_token(dst, dst_size,
"STATE_MAIN_ON_STANDBY=500");
append_shellcore_flag_token(dst, dst_size, "STATE_WORKING=1000");
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_NONE=0");
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_CFF=1");
append_shellcore_flag_token(dst, dst_size, "INITIAL=0x10000000A");
return true;
}
return false;
}
static void format_shellcore_flag_known_masks(const shellcore_flag_monitor_t *flag,
char *dst, size_t dst_size) {
size_t bit_count = 0;
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
if (!dst || dst_size == 0)
return;
dst[0] = '\0';
(void)format_shellcore_flag_special_known_masks(flag, dst, dst_size);
for (size_t i = 0; i < bit_count; ++i) {
char token[96];
(void)snprintf(token, sizeof(token), "%s=0x%llX", bits[i].name,
(unsigned long long)bits[i].mask);
append_shellcore_flag_token(dst, dst_size, token);
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
append_shellcore_flag_token(dst, dst_size, "APP_ID");
}
#endif
}
static bool format_shellcore_flag_special_value(const shellcore_flag_monitor_t *flag,
uint64_t pattern, char *dst,
size_t dst_size) {
if (!flag || !flag->name || !dst || dst_size == 0)
return false;
#if 0
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
char token[96];
(void)snprintf(token, sizeof(token), "RUN_LEVEL=%u",
(unsigned)(pattern & 0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
if ((pattern & 0x0FFFFFFFULL) == 10u) {
append_shellcore_flag_token(dst, dst_size, "INITIAL");
} else if ((pattern & 0x0FFFFFFFULL) == 100u) {
append_shellcore_flag_token(dst, dst_size, "READY");
}
if ((pattern & ~0x0FFFFFFFULL) != 0) {
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
}
return true;
}
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
uint64_t usb_bits = pattern & 0x0FFFFFFFULL;
for (unsigned i = 0; i < 28; ++i) {
if ((usb_bits & (1ULL << i)) == 0)
continue;
char token[32];
(void)snprintf(token, sizeof(token), "USB%u", i);
append_shellcore_flag_token(dst, dst_size, token);
}
if ((pattern & ~0x0FFFFFFFULL) != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
return true;
}
#endif
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
unsigned current_state = (unsigned)(pattern & 0xFFFFu);
unsigned mid16 = (unsigned)((pattern >> 16) & 0xFFFFu);
unsigned trigger_code = (unsigned)((pattern >> 32) & 0xFFFFu);
unsigned reboot_cause = (unsigned)((pattern >> 48) & 0xFFFFu);
char token[128];
(void)snprintf(token, sizeof(token), "STATE=%s(0x%04X)",
get_system_state_mgr_state_name(current_state), current_state);
append_shellcore_flag_token(dst, dst_size, token);
(void)snprintf(token, sizeof(token), "TRIGGER=0x%04X", trigger_code);
append_shellcore_flag_token(dst, dst_size, token);
(void)snprintf(token, sizeof(token), "REBOOT_CAUSE=%s(0x%04X)",
get_system_state_mgr_reboot_cause_name(reboot_cause),
reboot_cause);
append_shellcore_flag_token(dst, dst_size, token);
if (mid16 != 0) {
(void)snprintf(token, sizeof(token), "MID16=0x%04X", mid16);
append_shellcore_flag_token(dst, dst_size, token);
}
return true;
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
unsigned low32 = (unsigned)(pattern & 0xFFFFFFFFu);
unsigned high32 = (unsigned)(pattern >> 32);
char token[96];
if (low32 != 0) {
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", low32);
append_shellcore_flag_token(dst, dst_size, token);
}
if (high32 != 0) {
(void)snprintf(token, sizeof(token), "HIGH32=0x%08X", high32);
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
return true;
}
#endif
#if 0
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
unsigned shape_field = (unsigned)((pattern >> 15) & 0x3u);
if (shape_field != 0) {
char token[96];
(void)snprintf(token, sizeof(token), "BASIC_PRODUCT_SHAPE=%u",
shape_field - 1u);
append_shellcore_flag_token(dst, dst_size, token);
}
return false;
}
#endif
return false;
}
static void format_shellcore_flag_set_bits(const shellcore_flag_monitor_t *flag,
uint64_t pattern, char *dst,
size_t dst_size) {
size_t bit_count = 0;
uint64_t known_mask = 0;
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
if (!dst || dst_size == 0)
return;
dst[0] = '\0';
if (format_shellcore_flag_special_value(flag, pattern, dst, dst_size) &&
dst[0] != '\0') {
return;
}
for (size_t i = 0; i < bit_count; ++i) {
known_mask |= bits[i].mask;
if ((pattern & bits[i].mask) == 0)
continue;
char token[96];
(void)snprintf(token, sizeof(token), "%s", bits[i].name);
append_shellcore_flag_token(dst, dst_size, token);
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0 &&
(pattern >> 32) == 0 && pattern != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", (unsigned)pattern);
append_shellcore_flag_token(dst, dst_size, token);
}
#endif
if ((pattern & ~known_mask) != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~known_mask));
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
}
static void set_shellcore_flag_start_result(bool success) {
pthread_mutex_lock(&g_shellcore_flag_mutex);
g_shellcore_flag_start_success = success;
g_shellcore_flag_start_ready = true;
pthread_cond_broadcast(&g_shellcore_flag_cond);
pthread_mutex_unlock(&g_shellcore_flag_mutex);
}
static void enter_sleep_mode_and_cleanup(const char *reason) {
if (request_runtime_sleep_mode(true, reason)) {
runtime_mount_state_lock();
unmount_usb_sources_for_suspend();
runtime_mount_state_unlock();
}
}
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
if (!flag)
return;
flag->handle = -1;
flag->is_open = false;
flag->has_last_pattern = false;
flag->has_last_rc = false;
flag->last_pattern = 0;
flag->last_rc = 0;
}
static void log_shellcore_flag_pattern(const shellcore_flag_monitor_t *flag,
uint64_t pattern) {
char decoded[512];
if (!flag)
return;
format_shellcore_flag_set_bits(flag, pattern, decoded, sizeof(decoded));
if (!flag->has_last_pattern) {
log_debug(" [SHELLFLAG] %s initial=0x%016llX low32=0x%08X high32=0x%08X "
"decoded=%s",
flag->name, (unsigned long long)pattern, (unsigned)(pattern),
(unsigned)(pattern >> 32), decoded);
return;
}
log_debug(" [SHELLFLAG] %s value=0x%016llX delta=0x%016llX low32=0x%08X "
"high32=0x%08X decoded=%s",
flag->name, (unsigned long long)pattern,
(unsigned long long)(flag->last_pattern ^ pattern),
(unsigned)(pattern), (unsigned)(pattern >> 32), decoded);
}
static void close_shellcore_flag(shellcore_flag_monitor_t *flag) {
if (!flag || !flag->is_open)
return;
int rc = sceKernelCloseEventFlag(flag->handle);
if (rc < 0) {
log_debug(" [SHELLFLAG] close failed: %s handle=0x%016llX rc=0x%08X",
flag->name, (unsigned long long)flag->handle, (unsigned)rc);
}
reset_shellcore_flag_state(flag);
}
static void close_shellcore_flags(void) {
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
close_shellcore_flag(&g_shellcore_flags[i]);
}
}
static bool open_shellcore_flags(size_t *opened_count_out) {
size_t opened_count = 0;
size_t required_count = 0;
size_t required_opened = 0;
if (opened_count_out)
*opened_count_out = 0;
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
shellcore_flag_monitor_t *flag = &g_shellcore_flags[i];
sm_kernel_event_flag_t handle = -1;
if (flag->required)
required_count++;
int rc = sceKernelOpenEventFlag(&handle, flag->name);
if (rc < 0) {
log_debug(" [SHELLFLAG] open failed: %s rc=0x%08X", flag->name,
(unsigned)rc);
reset_shellcore_flag_state(flag);
continue;
}
flag->handle = handle;
flag->is_open = true;
if (flag->required)
required_opened++;
log_debug(" [SHELLFLAG] opened %s handle=0x%016llX", flag->name,
(unsigned long long)flag->handle);
{
char known_masks[1024];
format_shellcore_flag_known_masks(flag, known_masks, sizeof(known_masks));
if (known_masks[0] != '\0') {
log_debug(" [SHELLFLAG] known %s masks: %s", flag->name, known_masks);
}
}
opened_count++;
}
if (opened_count_out)
*opened_count_out = opened_count;
return required_opened == required_count;
}
static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
uint64_t result_pattern = 0;
int rc;
#if 0
bool changed = false;
#endif
bool entered_shutdown_on_going = false;
bool entered_main_on_standby = false;
bool entered_suspend_on_going = false;
bool entered_resume_working = false;
bool entered_shellui_shutdown_in_progress = false;
bool is_system_state_mgr_info = false;
unsigned current_state = 0;
unsigned previous_state = 0;
if (!flag || !flag->is_open)
return;
rc = sceKernelPollEventFlag(flag->handle, SHELLCORE_FLAG_ALL_BITS,
SHELLCORE_FLAG_WAITMODE_OR, &result_pattern);
if (rc < 0) {
if (!flag->has_last_rc || flag->last_rc != rc) {
log_debug(" [SHELLFLAG] %s poll rc=0x%08X", flag->name,
(unsigned)rc);
}
flag->last_rc = rc;
flag->has_last_rc = true;
return;
}
if (!flag->has_last_rc || flag->last_rc != 0 || !flag->has_last_pattern ||
flag->last_pattern != result_pattern) {
log_shellcore_flag_pattern(flag, result_pattern);
#if 0
changed = flag->has_last_pattern && flag->last_pattern != result_pattern;
#endif
}
is_system_state_mgr_info = strcmp(flag->name, "SceSystemStateMgrInfo") == 0;
if (is_system_state_mgr_info) {
current_state = (unsigned)(result_pattern & 0xFFFFu);
previous_state =
flag->has_last_pattern ? (unsigned)(flag->last_pattern & 0xFFFFu) : 0;
entered_shutdown_on_going =
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
entered_main_on_standby =
current_state == SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY);
entered_suspend_on_going =
current_state == SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING);
entered_resume_working =
flag->has_last_pattern &&
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
} else if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
entered_shellui_shutdown_in_progress =
(result_pattern &
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) != 0 &&
(!flag->has_last_pattern ||
(flag->last_pattern &
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) == 0);
}
flag->last_pattern = result_pattern;
flag->has_last_pattern = true;
flag->last_rc = 0;
flag->has_last_rc = true;
#if 0
if (changed && strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
sm_kstuff_note_app_focus((uint32_t)result_pattern);
wake_game_lifecycle_watcher();
}
#endif
if (entered_shutdown_on_going) {
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
}
if (entered_main_on_standby || entered_suspend_on_going) {
const char *sleep_reason =
entered_main_on_standby ? "SceSystemStateMgrInfo=MAIN_ON_STANDBY"
: "SceSystemStateMgrInfo=SUSPEND_ON_GOING";
enter_sleep_mode_and_cleanup(sleep_reason);
}
if (entered_shellui_shutdown_in_progress) {
enter_sleep_mode_and_cleanup(
"SceSystemStateMgrStatus=SHELLUI_SHUTDOWN_IN_PROGRESS");
}
if (entered_resume_working) {
request_scan_now("SceSystemStateMgrInfo=WORKING");
request_runtime_sleep_mode(false, "SceSystemStateMgrInfo=WORKING");
}
}
static void *shellcore_flag_thread_main(void *arg) {
(void)arg;
size_t opened_count = 0;
if (!open_shellcore_flags(&opened_count)) {
if (opened_count != 0)
close_shellcore_flags();
set_shellcore_flag_start_result(false);
return NULL;
}
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
poll_shellcore_flag(&g_shellcore_flags[i]);
}
log_debug(" [SHELLFLAG] monitor started (%zu/%zu flags opened)", opened_count,
sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]));
set_shellcore_flag_start_result(true);
while (!g_shellcore_flag_stop_requested && !should_stop_requested()) {
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
poll_shellcore_flag(&g_shellcore_flags[i]);
}
sceKernelUsleep(SHELLCORE_FLAG_POLL_INTERVAL_US);
}
close_shellcore_flags();
return NULL;
}
bool sm_shellcore_flags_start(void) {
int rc;
if (g_shellcore_flag_thread_started)
return g_shellcore_flag_start_success;
g_shellcore_flag_stop_requested = 0;
g_shellcore_flag_start_ready = false;
g_shellcore_flag_start_success = false;
close_shellcore_flags();
rc = pthread_create(&g_shellcore_flag_thread, NULL, shellcore_flag_thread_main,
NULL);
if (rc != 0) {
log_debug(" [SHELLFLAG] pthread_create failed: rc=%d", rc);
return false;
}
pthread_mutex_lock(&g_shellcore_flag_mutex);
while (!g_shellcore_flag_start_ready)
pthread_cond_wait(&g_shellcore_flag_cond, &g_shellcore_flag_mutex);
pthread_mutex_unlock(&g_shellcore_flag_mutex);
if (!g_shellcore_flag_start_success) {
pthread_join(g_shellcore_flag_thread, NULL);
close_shellcore_flags();
return false;
}
g_shellcore_flag_thread_started = true;
return true;
}
void sm_shellcore_flags_stop(void) {
if (!g_shellcore_flag_thread_started)
return;
g_shellcore_flag_stop_requested = 1;
pthread_join(g_shellcore_flag_thread, NULL);
g_shellcore_flag_thread_started = false;
g_shellcore_flag_start_ready = false;
g_shellcore_flag_start_success = false;
close_shellcore_flags();
}
+49
View File
@@ -0,0 +1,49 @@
#include "sm_platform.h"
#include "sm_stability.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
static time_t get_path_last_change_time(const struct stat *st) {
if (!st)
return 0;
return (st->st_ctime > st->st_mtime) ? st->st_ctime : st->st_mtime;
}
bool is_path_stable_now(const char *path, double *root_diff_out,
int *stat_errno_out) {
struct stat st;
time_t now = time(NULL);
ensure_runtime_config_ready();
if (stat_errno_out)
*stat_errno_out = 0;
if (stat(path, &st) != 0) {
if (root_diff_out)
*root_diff_out = -1.0;
if (stat_errno_out)
*stat_errno_out = errno;
return false;
}
double root_diff = difftime(now, get_path_last_change_time(&st));
if (root_diff_out)
*root_diff_out = root_diff;
if (root_diff < 0.0)
return true;
return root_diff > (double)runtime_config()->stability_wait_seconds;
}
bool wait_for_stability_fast(const char *path, const char *name) {
double diff = 0.0;
int st_err = 0;
if (is_path_stable_now(path, &diff, &st_err))
return true;
if (st_err != 0)
log_debug(" [WAIT] %s stat failed for %s: %s", name, path,
strerror(st_err));
else
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
return false;
}
+12
View File
@@ -0,0 +1,12 @@
#include "sm_platform.h"
#include "sm_time.h"
uint64_t monotonic_time_us(void) {
struct timespec ts;
memset(&ts, 0, sizeof(ts));
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
return 0;
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)ts.tv_nsec / 1000ull;
}
+163
View File
@@ -0,0 +1,163 @@
#include "sm_platform.h"
#include "sm_title_state.h"
#include "sm_hash.h"
#include "sm_limits.h"
#include "sm_log.h"
struct TitleStateEntry {
char title_id[MAX_TITLE_ID];
uint8_t mount_reg_attempts;
uint8_t register_attempts;
bool duplicate_notified_once;
bool valid;
};
static struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY];
static uint16_t g_title_state_hash[STATE_HASH_SIZE];
static void rebuild_title_state_hash(void) {
memset(g_title_state_hash, 0, sizeof(g_title_state_hash));
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid || g_title_state[k].title_id[0] == '\0')
continue;
uint32_t slot =
sm_fnv1a32(g_title_state[k].title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_title_state_hash[slot] == 0) {
g_title_state_hash[slot] = (uint16_t)(k + 1);
break;
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
}
}
static struct TitleStateEntry *find_title_state(const char *title_id) {
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
uint16_t idx = g_title_state_hash[slot];
if (idx == 0)
return NULL;
struct TitleStateEntry *entry = &g_title_state[idx - 1u];
if (entry->valid && strcmp(entry->title_id, title_id) == 0)
return entry;
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
return NULL;
}
static struct TitleStateEntry *create_title_state(const char *title_id) {
int slot_k = -1;
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid) {
slot_k = k;
break;
}
}
if (slot_k < 0) {
int evict_k = -1;
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid)
continue;
if (g_title_state[k].mount_reg_attempts == 0 &&
g_title_state[k].register_attempts == 0) {
evict_k = k;
break;
}
}
if (evict_k < 0) {
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid)
continue;
if (g_title_state[k].mount_reg_attempts == 0) {
evict_k = k;
break;
}
}
}
if (evict_k < 0)
evict_k = 0;
memset(&g_title_state[evict_k], 0, sizeof(g_title_state[evict_k]));
rebuild_title_state_hash();
slot_k = evict_k;
}
memset(&g_title_state[slot_k], 0, sizeof(g_title_state[slot_k]));
g_title_state[slot_k].valid = true;
(void)strlcpy(g_title_state[slot_k].title_id, title_id,
sizeof(g_title_state[slot_k].title_id));
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_title_state_hash[slot] == 0) {
g_title_state_hash[slot] = (uint16_t)(slot_k + 1);
return &g_title_state[slot_k];
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
g_title_state[slot_k].valid = false;
g_title_state[slot_k].title_id[0] = '\0';
rebuild_title_state_hash();
return NULL;
}
static struct TitleStateEntry *get_or_create_title_state(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry : create_title_state(title_id);
}
bool was_register_attempted(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? (entry->register_attempts >= MAX_REGISTER_ATTEMPTS) : false;
}
uint8_t get_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry->register_attempts : 0;
}
void mark_register_attempted(const char *title_id) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->register_attempts < UINT8_MAX)
entry->register_attempts++;
}
void clear_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->register_attempts = 0;
}
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->duplicate_notified_once)
return;
entry->duplicate_notified_once = true;
notify_system_l10n(SM_L10N_DUPLICATE_IGNORED, title_id, path_a, path_b);
}
void clear_duplicate_title_notification(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->duplicate_notified_once = false;
}
uint8_t get_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry->mount_reg_attempts : 0;
}
void clear_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->mount_reg_attempts = 0;
}
uint8_t bump_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->mount_reg_attempts < UINT8_MAX)
entry->mount_reg_attempts++;
return entry->mount_reg_attempts;
}