mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
chore: merge configs root and switch folders
This commit is contained in:
commit
24eab97bc7
24 files changed
+826
-203
No files matched your search
@@ -63,6 +63,7 @@ files:
|
||||
standalone_directories:
|
||||
- "$XDG_DATA_HOME/eden"
|
||||
- "$HOME/.local/share/eden"
|
||||
- "PORTABLE_DIR"
|
||||
data_root:
|
||||
windows:
|
||||
path: "%APPDATA%/eden"
|
||||
|
||||
@@ -19,6 +19,7 @@ mode: standalone
|
||||
standalone_directories:
|
||||
- "$XDG_CONFIG_HOME/Ryujinx"
|
||||
- "$HOME/.config/Ryujinx"
|
||||
- "DefaultPortableDir"
|
||||
data_root:
|
||||
windows:
|
||||
path: "%APPDATA%/Ryujinx"
|
||||
|
||||
+273
-1
@@ -39,7 +39,12 @@ notes: |
|
||||
priority over the global sys_modules/ directory.
|
||||
|
||||
System fonts are mounted from fonts/font/ (/preinst/common/font on real
|
||||
PS4) and fonts/font2/ (/system/common/font2 on real PS4). These are SST
|
||||
PS4) and fonts/font2/ (/system/common/font2 on real PS4)
|
||||
(src/emulator.cpp:639-661). The font library opens each by its dumped name
|
||||
or its PS4 name in fonts/, then fonts/font/, then fonts/font2/
|
||||
(src/core/libraries/font/font_internal.cpp:391-468), and falls back to the
|
||||
Noto fonts it embeds (font_internal.cpp:119-127). The entries below put
|
||||
each in fonts/font/, which both the mount and the library read. These are SST
|
||||
(Sony Standard Text) typeface files in OTF format. The emulator mounts
|
||||
the directories wholesale, and games load individual fonts by name. Without
|
||||
fonts, the emulator warns "No dumped system fonts, expect missing text
|
||||
@@ -239,3 +244,270 @@ files:
|
||||
description: "PS4 XML parser"
|
||||
note: "No HLE implementation. The module is stubbed when absent."
|
||||
source_ref: "src/core/libraries/sysmodule/sysmodule_internal.cpp:251"
|
||||
|
||||
- name: "SST-Light.otf"
|
||||
aliases: ["SST-EU-ROMAN-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-Light.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:419, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-Roman.otf"
|
||||
aliases: ["SST-EU-ROMAN.OTF", "SST-EU-ROMAN-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-Roman.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:420, 422, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-Medium.otf"
|
||||
aliases: ["SST-EU-ROMAN-M.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-Medium.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:421, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-Italic.otf"
|
||||
aliases: ["SST-EU-ROMAN-I.OTF", "SST-ITALIC-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-Italic.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:423, 427, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-Bold.otf"
|
||||
aliases: ["SST-EU-ROMAN-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-Bold.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:424, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-BoldItalic.otf"
|
||||
aliases: ["SST-EU-ROMAN-BI.OTF", "SST-ITALIC-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-BoldItalic.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:425, 429, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-LightItalic.otf"
|
||||
aliases: ["SST-ITALIC-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-LightItalic.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:426, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SST-MediumItalic.otf"
|
||||
aliases: ["SST-ITALIC-M.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SST-MediumItalic.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:428, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTTypewriter-Roman.otf"
|
||||
aliases: ["SST-TYPEWRITER-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTTypewriter-Roman.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:430, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTTypewriter-Bd.otf"
|
||||
aliases: ["SST-TYPEWRITER-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTTypewriter-Bd.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:431, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTJpPro-Regular.otf"
|
||||
aliases: ["SST-JPPRO-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTJpPro-Regular.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:432, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTJpPro-Bold.otf"
|
||||
aliases: ["SST-JPPRO-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTJpPro-Bold.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:433, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "DFHEI5-SONY.ttf"
|
||||
aliases: ["SST-CNGB-HEI-R.TTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/DFHEI5-SONY.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:434, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTAribStdB24-Regular.ttf"
|
||||
aliases: ["SST-ARIB-STD-B24-R.TTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTAribStdB24-Regular.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:435, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTArabic-Roman.otf"
|
||||
aliases: ["SST-ARABIC-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTArabic-Roman.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:436, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTArabic-Light.otf"
|
||||
aliases: ["SST-ARABIC-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTArabic-Light.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:437, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTArabic-Medium.otf"
|
||||
aliases: ["SST-ARABIC-M.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTArabic-Medium.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:438, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTArabic-Bold.otf"
|
||||
aliases: ["SST-ARABIC-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTArabic-Bold.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:439, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SCEPS4Yoongd-Light.otf"
|
||||
aliases: ["SCE-EXT-HANGUL-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SCEPS4Yoongd-Light.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:440, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SCEPS4Yoongd-Medium.otf"
|
||||
aliases: ["SCE-EXT-HANGUL-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SCEPS4Yoongd-Medium.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:441, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SCEPS4Yoongd-Bold.otf"
|
||||
aliases: ["SCE-EXT-HANGUL-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SCEPS4Yoongd-Bold.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:442, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "e046323ms.ttf"
|
||||
aliases: ["SSTCC-SERIF-MONO.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/e046323ms.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:443, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "e046323ts.ttf"
|
||||
aliases: ["SSTCC-SERIF.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/e046323ts.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:444, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "n023055ms.ttf"
|
||||
aliases: ["SSTCC-SANSSERIF-MONO.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/n023055ms.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:445, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "n023055ts.ttf"
|
||||
aliases: ["SSTCC-SANSSERIF.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/n023055ts.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:446, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "d013013ds.ttf"
|
||||
aliases: ["SSTCC-CUSUAL.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/d013013ds.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:447, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "k006004ds.ttf"
|
||||
aliases: ["SSTCC-CURSIVE.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/k006004ds.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:448, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "c041056ts.ttf"
|
||||
aliases: ["SSTCC-SMALLCAPITAL.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/c041056ts.ttf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:449, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SCE-RDC-R-JPN.otf"
|
||||
aliases: ["SCE-JP-CATTLEYA-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SCE-RDC-R-JPN.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:450, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SCE-RDC-B-JPN.otf"
|
||||
aliases: ["SCE-JP-CATTLEYA-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SCE-RDC-B-JPN.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:451, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTThai-Light.otf"
|
||||
aliases: ["SST-THAI-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTThai-Light.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:452, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTThai-Roman.otf"
|
||||
aliases: ["SST-THAI-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTThai-Roman.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:453, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTThai-Medium.otf"
|
||||
aliases: ["SST-THAI-M.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTThai-Medium.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:454, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTThai-Bold.otf"
|
||||
aliases: ["SST-THAI-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTThai-Bold.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:455, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTVietnamese-Light.otf"
|
||||
aliases: ["SST-VIETNAMESE-L.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTVietnamese-Light.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:456, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTVietnamese-Roman.otf"
|
||||
aliases: ["SST-VIETNAMESE-R.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTVietnamese-Roman.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:457, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTVietnamese-Medium.otf"
|
||||
aliases: ["SST-VIETNAMESE-M.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTVietnamese-Medium.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:458, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
- name: "SSTVietnamese-Bold.otf"
|
||||
aliases: ["SST-VIETNAMESE-B.OTF"]
|
||||
required: false
|
||||
hle_fallback: true
|
||||
path: "fonts/font/SSTVietnamese-Bold.otf"
|
||||
description: "PS4 system font"
|
||||
source_ref: "src/core/libraries/font/font_internal.cpp:459, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
|
||||
@@ -59,6 +59,7 @@ files:
|
||||
standalone_directories:
|
||||
- "$XDG_DATA_HOME/yuzu"
|
||||
- "$HOME/.local/share/yuzu"
|
||||
- "PORTABLE_DIR"
|
||||
data_root:
|
||||
windows:
|
||||
path: "%APPDATA%/yuzu"
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
|
||||
# the site's build rewrites this line there (scripts/sitebootstrap.py).
|
||||
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
$defaultInstallSha256 = "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc"
|
||||
$defaultInstallSha256 = "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+73
-58
@@ -143,6 +143,7 @@ OMISSION_REASONS = {
|
||||
"no_save_system": "no system runs the core",
|
||||
"outside_bios_folder": "read outside the BIOS folder",
|
||||
"no_roms_directory": "no roms folder",
|
||||
"no_configs_directory": "no configs folder",
|
||||
"no_emulators_directory": "no emulator folder",
|
||||
}
|
||||
# What a file is when it is not firmware, as the manifest's category field
|
||||
@@ -3528,14 +3529,20 @@ def _roms_relation(value: object, field: str) -> tuple[int, PurePosixPath]:
|
||||
return ups, PurePosixPath(*names)
|
||||
|
||||
|
||||
def _roms_root(bios_path: Path, from_bios: str) -> Path | None:
|
||||
"""The roms root of a BIOS folder, on the host's own path type.
|
||||
# The roots a list places files below by their place relative to the BIOS
|
||||
# folder: the roms root (ROCKNIX's /storage/roms) and the frontend's
|
||||
# configs folder (Batocera's /userdata/system/configs).
|
||||
RELATIVE_ROOTS = ("roms", "configs")
|
||||
|
||||
|
||||
def _relative_root(bios_path: Path, from_bios: str, root: str = "roms") -> Path | None:
|
||||
"""A relative root of a BIOS folder, on the host's own path type.
|
||||
|
||||
The BIOS folder's ancestor at the depth the relation climbs, then the
|
||||
named folders below it. None when the joined and normalized path is
|
||||
anything else.
|
||||
"""
|
||||
ups, names = _roms_relation(from_bios, "roms_directory.from_bios")
|
||||
ups, names = _roms_relation(from_bios, f"{root}_directory.from_bios")
|
||||
if ups > len(bios_path.parents):
|
||||
return None
|
||||
ancestor = bios_path if not ups else bios_path.parents[ups - 1]
|
||||
@@ -3671,8 +3678,9 @@ class SavePlace(NamedTuple):
|
||||
# BIOS folder -> where the files read below its frontend's save directory go,
|
||||
# set once the save directory is known (save_directory_for).
|
||||
_SAVE_DIRS: dict[Path, SavePlace] = {}
|
||||
# BIOS folder -> the platform's roms folder, set by place_roms_entries.
|
||||
_ROMS_DIRS: dict[Path, Path] = {}
|
||||
# (BIOS folder, root) -> the platform's roms or configs folder, set by
|
||||
# place_relative_entries.
|
||||
_RELATIVE_DIRS: dict[tuple[Path, str], Path] = {}
|
||||
# (BIOS folder, emulator key) -> the emulator folder, set by
|
||||
# place_emulator_entries.
|
||||
_EMULATOR_DIRS: dict[tuple[Path, str], Path] = {}
|
||||
@@ -3686,11 +3694,11 @@ def _entry_place(root: Path, entry: dict) -> tuple[Path, object]:
|
||||
core's own folder where the frontend sorts saves by core. Any other is
|
||||
relative to the BIOS folder.
|
||||
"""
|
||||
if entry.get("root") == "roms":
|
||||
roms = _ROMS_DIRS.get(root)
|
||||
if roms is None:
|
||||
raise ValueError(f"no roms folder known for {entry.get('dest')}")
|
||||
return roms, entry.get("dest")
|
||||
if entry.get("root") in RELATIVE_ROOTS:
|
||||
folder = _RELATIVE_DIRS.get((root, entry["root"]))
|
||||
if folder is None:
|
||||
raise ValueError(f"no {entry['root']} folder known for {entry.get('dest')}")
|
||||
return folder, entry.get("dest")
|
||||
if entry.get("root") == "emulators":
|
||||
key, _, rest = str(entry.get("dest")).partition("/")
|
||||
folder = _EMULATOR_DIRS.get((root, key))
|
||||
@@ -3731,7 +3739,7 @@ def _entry_key(entry: dict) -> str:
|
||||
if root == "emulator":
|
||||
# A path in one emulator's own folder: two emulators read two files.
|
||||
return f"emulator\0{','.join(entry.get('cores') or [])}\0{dest}"
|
||||
return f"{root}\0{dest}" if root in ("save", "roms", "emulators") else dest
|
||||
return f"{root}\0{dest}" if root in ("save", "emulators", *RELATIVE_ROOTS) else dest
|
||||
|
||||
|
||||
def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
|
||||
@@ -3739,7 +3747,8 @@ def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
|
||||
the emulator's own folder, outside the BIOS tree), and core_folder one
|
||||
folder name of a save entry."""
|
||||
root = entry.get("root")
|
||||
if root not in (None, "save", "roms", "emulators") and not (omitted and root == "emulator"):
|
||||
if root not in (None, "save", "emulators", *RELATIVE_ROOTS) and not (
|
||||
omitted and root == "emulator"):
|
||||
raise ValueError(f"invalid root for {dest}")
|
||||
if root == "emulators" and len(PurePosixPath(dest).parts) < 2:
|
||||
raise ValueError(f"an emulators entry names its folder first: {dest}")
|
||||
@@ -3752,24 +3761,27 @@ def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
|
||||
raise ValueError(f"core_folder must be one folder name for {dest}")
|
||||
|
||||
|
||||
def _check_roms_directory(data: dict) -> None:
|
||||
"""The manifest's roms_directory, which every installable root: roms entry needs."""
|
||||
layout = data.get("roms_directory")
|
||||
def _check_relative_directories(data: dict) -> None:
|
||||
"""The manifest's roms_directory and configs_directory, which every
|
||||
installable root: roms or root: configs entry needs."""
|
||||
installable = [
|
||||
entry for field in ("files", "case_variants")
|
||||
for entry in data.get(field) or [] if isinstance(entry, dict)
|
||||
]
|
||||
if layout is None:
|
||||
if any(entry.get("root") == "roms" for entry in installable):
|
||||
raise ValueError("entries below the roms folder and no roms_directory")
|
||||
return
|
||||
if (
|
||||
not isinstance(layout, dict)
|
||||
or set(layout) != {"bios_folder", "from_bios"}
|
||||
or len(_safe_relative_path(layout["bios_folder"], "roms_directory.bios_folder").parts) != 1
|
||||
):
|
||||
raise ValueError("invalid roms_directory")
|
||||
_roms_relation(layout["from_bios"], "roms_directory.from_bios")
|
||||
for root in RELATIVE_ROOTS:
|
||||
field = f"{root}_directory"
|
||||
layout = data.get(field)
|
||||
if layout is None:
|
||||
if any(entry.get("root") == root for entry in installable):
|
||||
raise ValueError(f"entries below the {root} folder and no {field}")
|
||||
continue
|
||||
if (
|
||||
not isinstance(layout, dict)
|
||||
or set(layout) != {"bios_folder", "from_bios"}
|
||||
or len(_safe_relative_path(layout["bios_folder"], f"{field}.bios_folder").parts) != 1
|
||||
):
|
||||
raise ValueError(f"invalid {field}")
|
||||
_roms_relation(layout["from_bios"], f"{field}.from_bios")
|
||||
|
||||
|
||||
def _check_emulators_directory(data: dict) -> None:
|
||||
@@ -3969,7 +3981,7 @@ def _check_entry(
|
||||
# Below a roms or emulator folder a colon has no use: refused outright.
|
||||
dest = str(_safe_relative_path(
|
||||
entry.get("dest"), f"{where}.dest",
|
||||
colons=entry.get("root") not in ("roms", "emulators"),
|
||||
colons=entry.get("root") not in ("emulators", *RELATIVE_ROOTS),
|
||||
))
|
||||
key = _entry_key({**entry, "dest": dest})
|
||||
if key in seen_destinations:
|
||||
@@ -4109,7 +4121,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
|
||||
for index, entry in enumerate(data_dirs):
|
||||
_check_data_directory(entry, f"data_directories[{index}]")
|
||||
_check_save_directory(data)
|
||||
_check_roms_directory(data)
|
||||
_check_relative_directories(data)
|
||||
_check_emulators_directory(data)
|
||||
|
||||
declared_total_files = data.get("total_files")
|
||||
@@ -5376,39 +5388,41 @@ def place_save_entries(
|
||||
return kept, unplaced
|
||||
|
||||
|
||||
def place_roms_entries(
|
||||
def place_relative_entries(
|
||||
files: list[dict], bios_path: Path, manifest: dict, display: str, say: bool = True,
|
||||
) -> tuple[list[dict], list[dict]]:
|
||||
"""Find the roms folder for the entries a launcher reads below it.
|
||||
"""Find the roms and configs folders for the entries read below them.
|
||||
|
||||
The list gives the roms root relative to the BIOS folder, as the OS's
|
||||
own source places them (ROCKNIX's /storage/roms holds bios/): a BIOS
|
||||
folder of the platform's name puts the roms root there. Any other is a
|
||||
folder this run cannot relate to the roms, and the entries are left out
|
||||
with the reason.
|
||||
The list gives each root relative to the BIOS folder, as the OS's own
|
||||
source places them (ROCKNIX's /storage/roms holds bios/, Batocera's
|
||||
configs folder is ../system/configs): a BIOS folder of the platform's
|
||||
name puts the root there. Any other is a folder this run cannot relate
|
||||
to the root, and the entries are left out with the reason.
|
||||
"""
|
||||
below = [entry for entry in files if entry.get("root") == "roms"]
|
||||
if not below:
|
||||
return files, []
|
||||
layout = manifest.get("roms_directory") or {}
|
||||
roms = (
|
||||
_roms_root(bios_path, layout["from_bios"])
|
||||
if layout and bios_path.name == layout.get("bios_folder") else None
|
||||
)
|
||||
if roms is not None:
|
||||
_ROMS_DIRS[bios_path] = roms
|
||||
return files, []
|
||||
if say:
|
||||
print(f" {_plural(len(below), 'file')} read from {display}'s roms folder left out: "
|
||||
f"{bios_path} is not its {layout.get('bios_folder') or 'BIOS'} folder.")
|
||||
kept = [entry for entry in files if entry.get("root") != "roms"]
|
||||
unplaced = [
|
||||
{"dest": entry["dest"], "name": PurePosixPath(entry["dest"]).name,
|
||||
"system": "", "systems": list(entry.get("systems") or []), "required": False,
|
||||
"reason": "no_roms_directory", "cores": entry.get("cores"), "root": "roms"}
|
||||
for entry in below
|
||||
]
|
||||
return kept, unplaced
|
||||
unplaced: list[dict] = []
|
||||
for root in RELATIVE_ROOTS:
|
||||
below = [entry for entry in files if entry.get("root") == root]
|
||||
if not below:
|
||||
continue
|
||||
layout = manifest.get(f"{root}_directory") or {}
|
||||
folder = (
|
||||
_relative_root(bios_path, layout["from_bios"], root)
|
||||
if layout and bios_path.name == layout.get("bios_folder") else None
|
||||
)
|
||||
if folder is not None:
|
||||
_RELATIVE_DIRS[(bios_path, root)] = folder
|
||||
continue
|
||||
if say:
|
||||
print(f" {_plural(len(below), 'file')} read from {display}'s {root} folder left "
|
||||
f"out: {bios_path} is not its {layout.get('bios_folder') or 'BIOS'} folder.")
|
||||
files = [entry for entry in files if entry.get("root") != root]
|
||||
unplaced += [
|
||||
{"dest": entry["dest"], "name": PurePosixPath(entry["dest"]).name,
|
||||
"system": "", "systems": list(entry.get("systems") or []), "required": False,
|
||||
"reason": f"no_{root}_directory", "cores": entry.get("cores"), "root": root}
|
||||
for entry in below
|
||||
]
|
||||
return files, unplaced
|
||||
|
||||
|
||||
def _config_keys(path: Path) -> set[str]:
|
||||
@@ -16114,7 +16128,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
display = display_name(plat_name, manifest)
|
||||
files, unplaced = place_save_entries(
|
||||
files, bios_path, manifest, args.save_dir, display, say=False)
|
||||
files, beyond = place_roms_entries(files, bios_path, manifest, display, say=not listing)
|
||||
files, beyond = place_relative_entries(
|
||||
files, bios_path, manifest, display, say=not listing)
|
||||
files, elsewhere = place_emulator_entries(
|
||||
files, bios_path, manifest, display, say=not listing)
|
||||
unplaced = [*unplaced, *beyond, *elsewhere]
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
|
||||
# curl or wget stop on a cut transfer before the hash is read: the site's
|
||||
# build rewrites this line there (scripts/sitebootstrap.py).
|
||||
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
DEFAULT_INSTALL_SHA256="28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc"
|
||||
DEFAULT_INSTALL_SHA256="9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1143586,
|
||||
"sha256": "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc",
|
||||
"size": 1144393,
|
||||
"sha256": "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1197211,
|
||||
"sha256": "c66dc01820e1610e7e476b417a44fc3c9007886d574aef81bcdb41441f1e1a8c"
|
||||
"size": 1198018,
|
||||
"sha256": "4729710b43a017c62bd275ec7651d489d56e5646cae6e87c73717e13413c6b5d"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1143586,
|
||||
"sha256": "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc",
|
||||
"size": 1144393,
|
||||
"sha256": "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1143586,
|
||||
"sha256": "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc",
|
||||
"size": 1144393,
|
||||
"sha256": "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6598,3 +6598,7 @@ launchers:
|
||||
\ / KEYS_DIR"
|
||||
fails_when_missing: false
|
||||
source_ref: "package/batocera/emulators/citron/002-adjust-paths.patch:9-10"
|
||||
configs_root:
|
||||
path: /userdata/system/configs
|
||||
from_bios: ../system/configs
|
||||
source_ref: "package/batocera/core/batocera-configgen/configgen/configgen/batoceraPaths.py:14, 19-20, 25"
|
||||
+4
-10
@@ -186,7 +186,6 @@ cores:
|
||||
- superbroswar
|
||||
- supermodel
|
||||
- supermodel-legacy
|
||||
- suyu
|
||||
- swanstation
|
||||
- taradino
|
||||
- tgbdual
|
||||
@@ -4970,7 +4969,6 @@ standalone_cores:
|
||||
- stella
|
||||
- supermodel
|
||||
- supermodel-legacy
|
||||
- suyu
|
||||
- taradino
|
||||
- theforceengine
|
||||
- thextech
|
||||
@@ -6143,14 +6141,6 @@ launchers:
|
||||
depends_on_option: not system.name == 'ti99'
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py:431-432,\
|
||||
\ 435"
|
||||
- emulator: suyu
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: /userdata/saves/switch
|
||||
passes_to: environment XDG_DATA_HOME
|
||||
fails_when_missing: false
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/suyu/suyuGenerator.py:41"
|
||||
- emulator: citron
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
@@ -6160,3 +6150,7 @@ launchers:
|
||||
\ / KEYS_DIR"
|
||||
fails_when_missing: false
|
||||
source_ref: "batocera/package/batocera/emulators/citron/002-adjust-paths.patch:9-10"
|
||||
configs_root:
|
||||
path: /userdata/system/configs
|
||||
from_bios: ../system/configs
|
||||
source_ref: "package/system/knulli-configgen/configgen/configgen/batoceraPaths.py:14, 19-20, 25"
|
||||
+27
-2
@@ -4971,17 +4971,42 @@ launchers:
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: "<AppConfig.GetFullPath(\"citron\")>/user"
|
||||
passes_to: PORTABLE_DIR, the user folder beside citron.exe
|
||||
passes_to: PORTABLE_DIR, the portable folder beside citron.exe
|
||||
fails_when_missing: false
|
||||
source_ref: "emulatorLauncher/Generators/Citron.Generator.cs:25, 29-30"
|
||||
- emulator: eden
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: "<AppConfig.GetFullPath(\"eden\")>/user"
|
||||
passes_to: PORTABLE_DIR, the portable folder beside eden.exe
|
||||
fails_when_missing: false
|
||||
source_ref: "emulatorLauncher/Generators/Eden.Generator.cs:28, 35-36"
|
||||
- emulator: ryujinx
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: "<AppConfig.GetFullPath(\"ryujinx\")>/portable"
|
||||
passes_to: DefaultPortableDir, the portable folder beside Ryujinx.exe
|
||||
fails_when_missing: false
|
||||
depends_on_option: unless saves/switch/ryujinx/portable exists, passed with -r
|
||||
source_ref: "emulatorLauncher/Generators/Ryujinx.Generator.cs:31, 73-75"
|
||||
- emulator: shadps4
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: "<AppConfig.GetFullPath(\"shadps4\")>/user"
|
||||
passes_to: PORTABLE_DIR, the user folder beside shadPS4.exe
|
||||
passes_to: PORTABLE_DIR, the portable folder beside shadPS4.exe
|
||||
fails_when_missing: false
|
||||
source_ref: "emulatorLauncher/Generators/ShadPS4.Generator.cs:29, 158-160"
|
||||
- emulator: yuzu
|
||||
build: standalone
|
||||
kind: passes_path
|
||||
names: []
|
||||
search_dir: "<AppConfig.GetFullPath(\"yuzu\")>/user"
|
||||
passes_to: PORTABLE_DIR, the portable folder beside yuzu.exe
|
||||
fails_when_missing: false
|
||||
source_ref: "emulatorLauncher/Generators/Yuzu.Generator.cs:24, 33-34"
|
||||
- emulator: bizhawk
|
||||
build: standalone
|
||||
kind: names_searched
|
||||
|
||||
@@ -207,7 +207,8 @@
|
||||
"no_save_system",
|
||||
"outside_bios_folder",
|
||||
"no_roms_directory",
|
||||
"no_emulators_directory"
|
||||
"no_emulators_directory",
|
||||
"no_configs_directory"
|
||||
],
|
||||
"description": "Why the file is left out, as a code. enumDescriptions gives the meaning of each code, in the order of enum.",
|
||||
"enumDescriptions": [
|
||||
@@ -222,7 +223,8 @@
|
||||
"The launcher gives each system its own save folder and no system of the platform runs the core.",
|
||||
"The platform runs the emulator from a folder outside the BIOS tree and links none of the BIOS tree where the emulator reads this file (root: emulator).",
|
||||
"The platform's launcher runs the file from its roms folder (root: roms) and the run cannot tell where that folder is: the BIOS folder is not the one the platform keeps in it.",
|
||||
"The platform's launcher runs the file from one of its emulator folders (root: emulators) and the run cannot place it there: the BIOS folder is not the one beside the platform's launcher config, or that config sets the folder."
|
||||
"The platform's launcher runs the file from one of its emulator folders (root: emulators) and the run cannot place it there: the BIOS folder is not the one beside the platform's launcher config, or that config sets the folder.",
|
||||
"The platform's launcher starts the emulator from a folder of its configs folder (root: configs) and the run cannot tell where that folder is: the BIOS folder is not the one the platform keeps beside it."
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
@@ -367,12 +369,13 @@
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. emulator: dest is the path the emulator reads in its own folder, which the platform keeps outside the BIOS tree (reason outside_bios_folder). roms: dest is relative to the platform's roms folder (roms_directory). emulators: dest is the emulator folder's key then the path in it (emulators_directory). Absent for a file of the BIOS tree.",
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. emulator: dest is the path the emulator reads in its own folder, which the platform keeps outside the BIOS tree (reason outside_bios_folder). roms: dest is relative to the platform's roms folder (roms_directory). emulators: dest is the emulator folder's key then the path in it (emulators_directory). configs: dest is relative to the frontend's configs folder (configs_directory). Absent for a file of the BIOS tree.",
|
||||
"enum": [
|
||||
"save",
|
||||
"emulator",
|
||||
"roms",
|
||||
"emulators"
|
||||
"emulators",
|
||||
"configs"
|
||||
]
|
||||
},
|
||||
"core_folder": {
|
||||
@@ -581,6 +584,27 @@
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configs_directory": {
|
||||
"description": "Where the frontend keeps its emulators' configs, present when an entry carries root: configs. from_bios is that folder relative to the BIOS folder, as the platform's own path code builds it (Batocera's batoceraPaths CONFIGS, ../system/configs), with the rules of roms_directory.from_bios. It is followed only from a BIOS folder named bios_folder; any other leaves root: configs entries out (no_configs_directory).",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"bios_folder",
|
||||
"from_bios"
|
||||
],
|
||||
"properties": {
|
||||
"bios_folder": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[^/\\\\]+$"
|
||||
},
|
||||
"from_bios": {
|
||||
"type": "string",
|
||||
"pattern": "^(\\.|\\.\\.(/\\.\\.)?(/[^/\\\\:.][^/\\\\:]*)*)$",
|
||||
"maxLength": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -599,7 +623,7 @@
|
||||
"dest": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
|
||||
"description": "Path below base_destination, below the save directory when root is save, below the roms folder when root is roms, or in an emulator folder when root is emulators (its key first)."
|
||||
"description": "Path below base_destination, below the save directory when root is save, below the roms folder when root is roms, below the configs folder when root is configs, or in an emulator folder when root is emulators (its key first)."
|
||||
},
|
||||
"sha1": {
|
||||
"type": "string",
|
||||
@@ -1082,11 +1106,12 @@
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. roms: dest is relative to the platform's roms folder, which roms_directory locates (a file its launcher runs from there, ROCKNIX's roms/pico-8). emulators: dest is the key of an emulator folder of the platform, then the path in it, located by emulators_directory (RetroBat's emulators/pico8). Absent for a file of the BIOS tree.",
|
||||
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. roms: dest is relative to the platform's roms folder, which roms_directory locates (a file its launcher runs from there, ROCKNIX's roms/pico-8). emulators: dest is the key of an emulator folder of the platform, then the path in it, located by emulators_directory (RetroBat's emulators/pico8). configs: dest is relative to the frontend's configs folder, which configs_directory locates (Batocera starts shadPS4 from configs/shadps4). Absent for a file of the BIOS tree.",
|
||||
"enum": [
|
||||
"save",
|
||||
"roms",
|
||||
"emulators"
|
||||
"emulators",
|
||||
"configs"
|
||||
]
|
||||
},
|
||||
"core_folder": {
|
||||
|
||||
@@ -188,6 +188,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configs_root": {
|
||||
"description": "Where the frontend keeps its emulators' configs, as its own path code builds it (Batocera's batoceraPaths CONFIGS): path as the OS spells it, from_bios relative to the BIOS folder. A launcher that starts an emulator from a folder below it (shadPS4's configs/shadps4) places that emulator's user folder there, and the install manifest writes such a file as root: configs.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"path",
|
||||
"from_bios",
|
||||
"source_ref"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"from_bios": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
|
||||
+11
-1
@@ -2825,7 +2825,8 @@ OUTSIDE_BIOS_FOLDER = "outside_bios_folder"
|
||||
# describes. Each is located by the registry entry <root>_directory.
|
||||
ROMS_ROOT = "roms"
|
||||
EMULATORS_ROOT = "emulators"
|
||||
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT)
|
||||
CONFIGS_ROOT = "configs"
|
||||
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT, CONFIGS_ROOT)
|
||||
|
||||
|
||||
def outer_layout(config: dict, root: str) -> dict | None:
|
||||
@@ -2858,6 +2859,15 @@ def outer_layout(config: dict, root: str) -> dict | None:
|
||||
return {"path": str(path), "install_prefix": "" if inside == "." else inside}
|
||||
if root == EMULATORS_ROOT:
|
||||
return config.get("emulators_directory") or None
|
||||
if root == CONFIGS_ROOT:
|
||||
# The frontend's configs folder, as its path code builds it
|
||||
# (configs_root, Batocera's batoceraPaths CONFIGS).
|
||||
found = config.get("configs_root") or {}
|
||||
base = str(config.get("base_destination") or "")
|
||||
if not (found.get("path") and found.get("from_bios") and base):
|
||||
return None
|
||||
return {"path": str(found["path"]), "from_bios": str(found["from_bios"]),
|
||||
"bios_folder": base}
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+29
-2
@@ -331,6 +331,8 @@ BIOS_TREE = ""
|
||||
ROMS_TREE = "roms"
|
||||
# The emulator folders of a platform whose registry describes them.
|
||||
EMULATORS_TREE = "emulators"
|
||||
# The frontend's configs folder of a platform whose file records it.
|
||||
CONFIGS_TREE = "configs"
|
||||
# The tree a platform's list installs into when it starts above the BIOS
|
||||
# folder: a plain dest, with no prefix.
|
||||
INSTALL_TREE = "install"
|
||||
@@ -433,12 +435,18 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
|
||||
source_ref=str(record.get("source_ref", "")),
|
||||
roms_place=None if folder is not None else (
|
||||
_roms_place(config, folder_in_roms(raw, config))
|
||||
or _configs_place(folder_in_configs(raw, config))
|
||||
or _emulators_place(folder_in_emulators(raw, config))
|
||||
),
|
||||
))
|
||||
return found
|
||||
|
||||
|
||||
def _configs_place(path: str | None) -> Place | None:
|
||||
"""A folder below the configs root, as a place of the configs tree."""
|
||||
return None if path is None else (CONFIGS_TREE, path)
|
||||
|
||||
|
||||
def _emulators_place(path: str | None) -> Place | None:
|
||||
"""A folder an emulator key names, as a place of the emulators tree."""
|
||||
return None if path is None else (EMULATORS_TREE, path)
|
||||
@@ -458,6 +466,18 @@ def _roms_place(config: dict, path: str | None) -> Place | None:
|
||||
return (ROMS_TREE, path)
|
||||
|
||||
|
||||
def folder_in_configs(directory: str, config: dict) -> str | None:
|
||||
"""A directory relative to the frontend's configs folder, None outside it.
|
||||
|
||||
Only a platform whose file records its configs root has one
|
||||
(``configs_root.path``): Batocera's /userdata/system/configs, where its
|
||||
generators start shadPS4 from configs/shadps4.
|
||||
"""
|
||||
from common import CONFIGS_ROOT # noqa: PLC0415
|
||||
|
||||
return folder_in_root(directory, config, CONFIGS_ROOT)
|
||||
|
||||
|
||||
def folder_in_roms(directory: str, config: dict) -> str | None:
|
||||
"""A directory relative to the platform's roms directory, None outside it.
|
||||
|
||||
@@ -465,9 +485,16 @@ def folder_in_roms(directory: str, config: dict) -> str | None:
|
||||
(``roms_root.path``): ROCKNIX's /storage/roms, which holds its BIOS
|
||||
folder and the folders its launchers run programs from.
|
||||
"""
|
||||
from common import ROMS_ROOT, outer_layout # noqa: PLC0415
|
||||
from common import ROMS_ROOT # noqa: PLC0415
|
||||
|
||||
roms = str((outer_layout(config, ROMS_ROOT) or {}).get("path") or "").rstrip("/")
|
||||
return folder_in_root(directory, config, ROMS_ROOT)
|
||||
|
||||
|
||||
def folder_in_root(directory: str, config: dict, root: str) -> str | None:
|
||||
"""A directory relative to an outer root's path, None outside it."""
|
||||
from common import outer_layout # noqa: PLC0415
|
||||
|
||||
roms = str((outer_layout(config, root) or {}).get("path") or "").rstrip("/")
|
||||
directory = directory.strip().rstrip("/")
|
||||
if not roms or not directory.startswith(roms + "/"):
|
||||
return None
|
||||
|
||||
@@ -97,6 +97,32 @@ def path_constants(source: Source) -> dict[str, str]:
|
||||
return constants
|
||||
|
||||
|
||||
def configs_root(cg: Configgen) -> dict[str, str]:
|
||||
"""Where configgen keeps the emulators' configs, relative to the BIOS folder.
|
||||
|
||||
batoceraPaths' CONFIGS (/userdata/system/configs), the folder a generator
|
||||
starts an emulator from (shadPS4's configs/shadps4), with the lines that
|
||||
build it and BIOS.
|
||||
"""
|
||||
import posixpath # noqa: PLC0415
|
||||
|
||||
src = cg.source(f"{cg.generators.rsplit('/', 1)[0]}/batoceraPaths.py")
|
||||
if "CONFIGS" not in cg.paths:
|
||||
src.fail("no CONFIGS path constant")
|
||||
wanted: set[str] = {"CONFIGS", "BIOS"}
|
||||
spans = []
|
||||
for node in reversed(src.tree.body):
|
||||
name = _assigned_name(node)
|
||||
if name in wanted:
|
||||
spans.append((node.lineno, node.end_lineno or node.lineno))
|
||||
wanted |= {n.id for n in ast.walk(node.value) if isinstance(n, ast.Name)} # type: ignore[union-attr]
|
||||
return {
|
||||
"path": cg.paths["CONFIGS"],
|
||||
"from_bios": posixpath.relpath(cg.paths["CONFIGS"], cg.bios),
|
||||
"source_ref": src.ref(spans),
|
||||
}
|
||||
|
||||
|
||||
def _path_value(node: ast.AST | None, constants: dict[str, str]) -> str | None:
|
||||
if isinstance(node, ast.Name):
|
||||
return constants.get(node.id)
|
||||
|
||||
@@ -14,6 +14,7 @@ import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Callable, Iterable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -28,7 +29,7 @@ from .base_scraper import (
|
||||
md5_field,
|
||||
requirement_entry,
|
||||
)
|
||||
from .batocera_launchers import extract_launchers
|
||||
from .batocera_launchers import Configgen, configs_root, extract_launchers
|
||||
|
||||
PLATFORM_NAME = "batocera"
|
||||
|
||||
@@ -168,7 +169,8 @@ def es_systems_libretro_systems(text: str, dropped: set[str]) -> dict[str, list[
|
||||
|
||||
|
||||
def es_systems_by_system(
|
||||
es_systems_text: str, defaults_text: str, dropped: set[str]
|
||||
es_systems_text: str, defaults_text: str, dropped: set[str],
|
||||
built: Callable[[str], bool] | None = None,
|
||||
) -> dict[str, list[str]]:
|
||||
"""system -> the cores es_systems.yml offers it, in the file's order.
|
||||
|
||||
@@ -195,6 +197,10 @@ def es_systems_by_system(
|
||||
default = defaults.get(system)
|
||||
if isinstance(default, dict):
|
||||
emulator, core = default.get("emulator"), default.get("core")
|
||||
if (built is not None and emulator and emulator != "libretro"
|
||||
and str(emulator) not in names and not built(str(emulator))):
|
||||
# A default the build lacks (configgen_cores).
|
||||
emulator = core = None
|
||||
if emulator and emulator != "libretro":
|
||||
names.append(str(emulator))
|
||||
if core:
|
||||
@@ -472,7 +478,8 @@ def platform_systems(
|
||||
|
||||
|
||||
def configgen_cores(
|
||||
defaults_text: str, es_systems_text: str, dropped: set[str]
|
||||
defaults_text: str, es_systems_text: str, dropped: set[str],
|
||||
built: Callable[[str], bool] | None = None,
|
||||
) -> tuple[list[str], list[str], list[str]]:
|
||||
"""Core names, standalone cores and dual-mode cores of a configgen build.
|
||||
|
||||
@@ -481,9 +488,16 @@ def configgen_cores(
|
||||
dual-mode ones those es_systems.yml also offers under libretro (dolphin
|
||||
for GameCube). `dropped` names what es_systems.yml files under an
|
||||
emulator without being one.
|
||||
|
||||
A default es_systems.yml does not offer names an emulator the build may
|
||||
lack: KNULLI's defaults pick suyu for the Switch, and no package of its
|
||||
tree, its batocera submodule or its buildroot builds suyu. `built` says
|
||||
whether the build has a standalone emulator (packages_and_programs); a
|
||||
default it answers no for is not run, nor its core.
|
||||
"""
|
||||
data = yaml_load_upstream(defaults_text)
|
||||
cores, standalone = es_systems_emulators(es_systems_text)
|
||||
offered = set(cores)
|
||||
libretro = es_systems_libretro(es_systems_text)
|
||||
if not cores:
|
||||
raise RuntimeError("es_systems.yml names no emulator")
|
||||
@@ -492,6 +506,9 @@ def configgen_cores(
|
||||
continue
|
||||
emulator = cfg.get("emulator", "")
|
||||
core = cfg.get("core", "")
|
||||
if (built is not None and emulator and emulator != "libretro"
|
||||
and emulator not in offered and not built(emulator)):
|
||||
continue
|
||||
if core:
|
||||
cores.add(core)
|
||||
if emulator and emulator != "libretro":
|
||||
@@ -502,6 +519,57 @@ def configgen_cores(
|
||||
return sorted(cores - dropped), sorted(standalone - dropped), sorted(dual)
|
||||
|
||||
|
||||
BATOCERA_REPO = "batocera-linux/batocera.linux"
|
||||
|
||||
|
||||
def package_names(paths: Iterable[str]) -> set[str]:
|
||||
"""The buildroot packages a tree defines: each folder holding a Config.in."""
|
||||
return {
|
||||
path.rsplit("/", 2)[-2]
|
||||
for path in paths
|
||||
if path.endswith("/Config.in") and path.count("/") >= 1
|
||||
}
|
||||
|
||||
|
||||
def repo_tree(repo: str, ref: str) -> list[str]:
|
||||
"""Every path of a GitHub repository at a revision."""
|
||||
url = f"https://api.github.com/repos/{repo}/git/trees/{ref}?recursive=1"
|
||||
req = urllib.request.Request(url, headers=github_headers())
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
found = json.loads(resp.read().decode("utf-8"))
|
||||
if found.get("truncated"):
|
||||
raise RuntimeError(f"{url}: tree truncated")
|
||||
return [entry["path"] for entry in found.get("tree", [])]
|
||||
|
||||
|
||||
def submodule_revision(repo: str, path: str, ref: str) -> str:
|
||||
"""The commit a repository's submodule at `path` pins at a revision."""
|
||||
url = f"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}"
|
||||
req = urllib.request.Request(url, headers=github_headers())
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
found = json.loads(resp.read().decode("utf-8"))
|
||||
if not isinstance(found, dict) or found.get("type") != "submodule" or not found.get("sha"):
|
||||
raise RuntimeError(f"{url}: {path} is not a submodule")
|
||||
return str(found["sha"])
|
||||
|
||||
|
||||
def packages_and_programs(packages: set[str], launchers: list[dict]) -> Callable[[str], bool]:
|
||||
"""Whether a build has a standalone emulator.
|
||||
|
||||
It does when a package of that name exists, or when its launcher runs a
|
||||
program the user puts in the BIOS folder (Batocera's lexaloffle runs
|
||||
/userdata/bios/pico-8/pico8), which no package builds.
|
||||
"""
|
||||
supplied = {
|
||||
name.strip()
|
||||
for record in launchers
|
||||
if record.get("passes_to") == "exec"
|
||||
and str(record.get("search_dir") or "").startswith("/userdata/bios")
|
||||
for name in str(record.get("emulator", "")).split(",")
|
||||
}
|
||||
return lambda emulator: emulator in packages or emulator in supplied
|
||||
|
||||
|
||||
class Scraper(BaseScraper):
|
||||
"""Scraper for batocera-systems Python dict."""
|
||||
|
||||
@@ -509,12 +577,26 @@ class Scraper(BaseScraper):
|
||||
self.tag = fetch_stable_tag()
|
||||
super().__init__(url=SOURCE_URL.format(tag=self.tag))
|
||||
|
||||
def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]:
|
||||
"""Core names, standalone cores and dual-mode cores at the pinned tag."""
|
||||
def _fetch_cores(
|
||||
self, launchers: list[dict] | None = None
|
||||
) -> tuple[list[str], list[str], list[str]]:
|
||||
"""Core names, standalone cores and dual-mode cores at the pinned tag.
|
||||
|
||||
A default es_systems.yml does not offer counts when a package of
|
||||
Batocera's tree or its buildroot builds it, or when its launcher
|
||||
runs a program from the BIOS folder.
|
||||
"""
|
||||
built = None
|
||||
if launchers is not None:
|
||||
paths = repo_tree(BATOCERA_REPO, self.tag)
|
||||
buildroot = submodule_revision(BATOCERA_REPO, "buildroot", self.tag)
|
||||
paths += repo_tree("batocera-linux/buildroot", buildroot)
|
||||
built = packages_and_programs(package_names(paths), launchers)
|
||||
return configgen_cores(
|
||||
self._fetch_text(CONFIGGEN_DEFAULTS_URL.format(tag=self.tag)),
|
||||
self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)),
|
||||
_not_cores(),
|
||||
built,
|
||||
)
|
||||
|
||||
def _fetch_text(self, url: str) -> str:
|
||||
@@ -548,7 +630,10 @@ class Scraper(BaseScraper):
|
||||
SYSTEM_SLUG_MAP,
|
||||
)
|
||||
|
||||
cores, standalone, dual = self._fetch_cores()
|
||||
launchers = extract_launchers(
|
||||
lambda path: self._fetch_text(f"{_RAW_BASE}/{self.tag}/{path}")
|
||||
)
|
||||
cores, standalone, dual = self._fetch_cores(launchers)
|
||||
libretro_systems = es_systems_libretro_systems(
|
||||
self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)), _not_cores()
|
||||
)
|
||||
@@ -570,8 +655,9 @@ class Scraper(BaseScraper):
|
||||
result["dual_mode_cores"] = dual
|
||||
# The systems each libretro core runs for: its save folders.
|
||||
result["libretro_systems"] = libretro_systems
|
||||
result["launchers"] = extract_launchers(
|
||||
lambda path: self._fetch_text(f"{_RAW_BASE}/{self.tag}/{path}")
|
||||
result["launchers"] = launchers
|
||||
result["configs_root"] = configs_root(
|
||||
Configgen(lambda path: self._fetch_text(f"{_RAW_BASE}/{self.tag}/{path}"))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ try:
|
||||
)
|
||||
from .configgen_launchers import (
|
||||
Launcher,
|
||||
_calls,
|
||||
LauncherShapeError,
|
||||
Source,
|
||||
_assigned_name,
|
||||
@@ -74,7 +73,6 @@ except ImportError:
|
||||
)
|
||||
from configgen_launchers import ( # type: ignore[no-redef]
|
||||
Launcher,
|
||||
_calls,
|
||||
LauncherShapeError,
|
||||
Source,
|
||||
_assigned_name,
|
||||
@@ -1062,39 +1060,6 @@ EXTRACTORS: tuple[Callable[[Configgen], list[Launcher]], ...] = (
|
||||
)
|
||||
|
||||
|
||||
# Generators whose command sets the emulator's XDG data folder, the parent
|
||||
# of every user folder a yuzu-family emulator derives.
|
||||
DATA_HOME_GENERATORS = ("suyu.suyuGenerator",)
|
||||
|
||||
|
||||
def data_homes(cg: Configgen) -> list[Launcher]:
|
||||
"""Command(..., env={"XDG_DATA_HOME": <path>}) in the generators above.
|
||||
|
||||
The path need not lie in /userdata/bios: KNULLI's suyu keeps its data
|
||||
in /userdata/saves/switch, which tells where its keys are not.
|
||||
"""
|
||||
launchers = []
|
||||
for module in DATA_HOME_GENERATORS:
|
||||
src = cg.generator(module)
|
||||
found = [
|
||||
(_path_value(item, cg.paths), key)
|
||||
for call in _calls(src.tree, attr="Command")
|
||||
for keyword in call.keywords
|
||||
if keyword.arg == "env" and isinstance(keyword.value, ast.Dict)
|
||||
for key, item in zip(keyword.value.keys, keyword.value.values)
|
||||
if isinstance(key, ast.Constant) and key.value == "XDG_DATA_HOME"
|
||||
]
|
||||
if not found or found[0][0] is None:
|
||||
src.fail("the command environment sets no XDG_DATA_HOME path")
|
||||
value, node = found[0]
|
||||
launchers.append(Launcher(
|
||||
emulator=cg.emulator(module), build="standalone", kind="passes_path", names=[],
|
||||
search_dir=value, passes_to="environment XDG_DATA_HOME",
|
||||
source_ref=src.ref([(node.lineno, node.end_lineno)]),
|
||||
))
|
||||
return launchers
|
||||
|
||||
|
||||
# KNULLI builds Batocera's emulator packages from this submodule, at the
|
||||
# batocera.linux commit it pins: Citron's path patch is there.
|
||||
BATOCERA_SUBMODULE = "batocera"
|
||||
@@ -1114,7 +1079,6 @@ def extract_launchers(
|
||||
launchers += boot_disks(cg, read)
|
||||
for spec in GENERIC_SITES:
|
||||
launchers += bios_sites(cg, spec)
|
||||
launchers += data_homes(cg)
|
||||
if batocera is not None:
|
||||
prefix = f"{BATOCERA_SUBMODULE}/"
|
||||
launchers += patched_paths(
|
||||
|
||||
@@ -41,11 +41,17 @@ from .batocera_scraper import (
|
||||
es_systems_by_system,
|
||||
es_systems_libretro_systems,
|
||||
extract_systems_dict,
|
||||
package_names,
|
||||
packages_and_programs,
|
||||
platform_systems,
|
||||
repo_tree,
|
||||
requirements_from_systems,
|
||||
submodule_revision,
|
||||
)
|
||||
from .es_emulators import attach_system_cores
|
||||
from .batocera_launchers import configs_root
|
||||
from .knulli_launchers import BATOCERA_SUBMODULE, extract_launchers
|
||||
from .knulli_launchers import configgen as knulli_configgen
|
||||
|
||||
PLATFORM_NAME = "knulli"
|
||||
|
||||
@@ -123,11 +129,27 @@ class Scraper(BaseScraper):
|
||||
defaults = self._fetch_text(CONFIGGEN_DEFAULTS_TEMPLATE.format(ref=self.tag))
|
||||
es_systems = self._fetch_text(ES_SYSTEMS_TEMPLATE.format(ref=self.tag))
|
||||
dropped = _not_cores("batocera") | _not_cores(PLATFORM_NAME)
|
||||
cores, standalone, dual = configgen_cores(defaults, es_systems, dropped)
|
||||
revision = self.submodule_revision()
|
||||
launchers = extract_launchers(
|
||||
lambda path: self._fetch_text(_RAW.format(ref=self.tag) + path),
|
||||
lambda path: self._fetch_text(_BATOCERA_RAW.format(ref=revision) + path),
|
||||
)
|
||||
# The packages KNULLI builds come from its own tree, its batocera
|
||||
# submodule and its buildroot submodule: a default no package of
|
||||
# them builds (suyu) is not run.
|
||||
paths = repo_tree(REPO, self.tag)
|
||||
paths += repo_tree("batocera-linux/batocera.linux", revision)
|
||||
paths += repo_tree(
|
||||
"knulli-cfw/buildroot", submodule_revision(REPO, "buildroot", self.tag)
|
||||
)
|
||||
built = packages_and_programs(package_names(paths), launchers)
|
||||
cores, standalone, dual = configgen_cores(defaults, es_systems, dropped, built)
|
||||
# Each system's own list (psx, es_systems.yml:640-653: libretro
|
||||
# pcsx_rearmed, swanstation, mednafen_psx, then duckstation and
|
||||
# duckstation-legacy).
|
||||
attach_system_cores(systems, es_systems_by_system(es_systems, defaults, dropped))
|
||||
attach_system_cores(
|
||||
systems, es_systems_by_system(es_systems, defaults, dropped, built)
|
||||
)
|
||||
result = {
|
||||
"platform": "KNULLI",
|
||||
"version": self.tag,
|
||||
@@ -146,10 +168,9 @@ class Scraper(BaseScraper):
|
||||
# configgen hands a libretro core /userdata/saves/<system>, as
|
||||
# Batocera's does: the systems each core runs for are its save folders.
|
||||
result["libretro_systems"] = es_systems_libretro_systems(es_systems, dropped)
|
||||
revision = self.submodule_revision()
|
||||
result["launchers"] = extract_launchers(
|
||||
lambda path: self._fetch_text(_RAW.format(ref=self.tag) + path),
|
||||
lambda path: self._fetch_text(_BATOCERA_RAW.format(ref=revision) + path),
|
||||
result["launchers"] = launchers
|
||||
result["configs_root"] = configs_root(
|
||||
knulli_configgen(lambda path: self._fetch_text(_RAW.format(ref=self.tag) + path))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ SOURCE_FILES = (
|
||||
f"{GENERATORS}/DosBoxPure.Generator.cs",
|
||||
f"{GENERATORS}/DosBoxStaging.Generator.cs",
|
||||
f"{GENERATORS}/Duckstation.Generator.cs",
|
||||
f"{GENERATORS}/Eden.Generator.cs",
|
||||
f"{GENERATORS}/Eka2l1.Generator.cs",
|
||||
f"{GENERATORS}/Fbneo.Generator.cs",
|
||||
f"{GENERATORS}/Flycast.Generator.cs",
|
||||
@@ -57,6 +58,7 @@ SOURCE_FILES = (
|
||||
f"{GENERATORS}/Project64.Generator.cs",
|
||||
f"{GENERATORS}/Raine.Generator.cs",
|
||||
f"{GENERATORS}/Rpcs3.Generator.cs",
|
||||
f"{GENERATORS}/Ryujinx.Generator.cs",
|
||||
f"{GENERATORS}/SSF.Generator.cs",
|
||||
f"{GENERATORS}/ScummVm.Generator.cs",
|
||||
f"{GENERATORS}/ShadPS4.Generator.cs",
|
||||
@@ -68,6 +70,7 @@ SOURCE_FILES = (
|
||||
f"{GENERATORS}/Xroar.Generator.cs",
|
||||
f"{GENERATORS}/Yabasanshiro.Generator.cs",
|
||||
f"{GENERATORS}/Ymir.Generator.cs",
|
||||
f"{GENERATORS}/Yuzu.Generator.cs",
|
||||
f"{GENERATORS}/mGBA.Generator.cs",
|
||||
FEATURES,
|
||||
)
|
||||
@@ -2982,44 +2985,63 @@ def xroar(sources: dict[str, Source]) -> list[Record]:
|
||||
return _covered(src, records)
|
||||
|
||||
|
||||
# Generators that create a portable user folder beside the emulator's binary,
|
||||
# and the emulator each one starts: the folder AppConfig.GetFullPath gives.
|
||||
# Generators that start an emulator with a portable data folder beside its
|
||||
# binary: (generator, emulator key, binary, the code's name for the folder).
|
||||
PORTABLE_USERS = (
|
||||
("Citron.Generator.cs", "citron", "citron.exe"),
|
||||
("ShadPS4.Generator.cs", "shadps4", "shadPS4.exe"),
|
||||
("Citron.Generator.cs", "citron", "citron.exe", "PORTABLE_DIR"),
|
||||
("Eden.Generator.cs", "eden", "eden.exe", "PORTABLE_DIR"),
|
||||
("Ryujinx.Generator.cs", "ryujinx", "Ryujinx.exe", "DefaultPortableDir"),
|
||||
("ShadPS4.Generator.cs", "shadps4", "shadPS4.exe", "PORTABLE_DIR"),
|
||||
("Yuzu.Generator.cs", "yuzu", "yuzu.exe", "PORTABLE_DIR"),
|
||||
)
|
||||
_EMULATOR_FOLDER = (
|
||||
r"GetEmulatorPathAndExe\(emulator, out string path, out string exe, \"[^\"]+\"\)"
|
||||
r"|string path = AppConfig\.GetFullPath\(emulator\);"
|
||||
r"|string path = AppConfig\.GetFullPath\((?:emulator(?:\.Replace\(\"-\", \" \"\))?"
|
||||
r"|\"\w+\")\);"
|
||||
)
|
||||
_PORTABLE_FOLDER = r'string (userFolder|setupPath) = Path\.Combine\(path, "(\w+)"\);'
|
||||
# Ryujinx's generator hands --root-data-dir saves/switch/ryujinx/portable
|
||||
# instead when that folder exists.
|
||||
_ALTERNATE_ROOT = (
|
||||
r'string portablePath = Path\.Combine\(AppConfig\.GetFullPath\("saves"\), "switch", '
|
||||
r'"ryujinx", "portable"\);'
|
||||
)
|
||||
|
||||
|
||||
def portable_users(sources: dict[str, Source]) -> list[Record]:
|
||||
"""The portable user folder a generator creates beside the binary.
|
||||
"""The portable data folder a generator gives an emulator beside its binary.
|
||||
|
||||
The emulator folder comes from AppConfig.GetFullPath(emulator), and the
|
||||
generator creates <folder>/user before launching: the emulator then
|
||||
keeps its data there (keys/, sys_modules/) in place of %APPDATA%.
|
||||
The emulator folder comes from AppConfig.GetFullPath, and the generator
|
||||
creates or configures <folder>/user (<folder>/portable for Ryujinx)
|
||||
before launching: the emulator then keeps its data there (keys/,
|
||||
sys_modules/, system/) in place of %APPDATA%.
|
||||
"""
|
||||
records = []
|
||||
for name, emulator, binary in PORTABLE_USERS:
|
||||
for name, emulator, binary, handle in PORTABLE_USERS:
|
||||
src = sources[f"{GENERATORS}/{name}"]
|
||||
found = src.find(_EMULATOR_FOLDER, f"{emulator} folder")
|
||||
user = src.find(r'string userFolder = Path\.Combine\(path, "(\w+)"\);', "user folder")
|
||||
create = src.find(
|
||||
r"Directory\.CreateDirectory\(userFolder\)", "user folder creation", user.end()
|
||||
)
|
||||
folder = src.find(_PORTABLE_FOLDER, "portable folder")
|
||||
spans = [src.lines(found.start(), found.end()), src.lines(folder.start(), folder.end())]
|
||||
if folder.group(1) == "userFolder":
|
||||
create = src.find(
|
||||
r"Directory\.CreateDirectory\(userFolder\)", "user folder creation", folder.end()
|
||||
)
|
||||
spans[-1] = src.lines(folder.start(), create.end())
|
||||
depends = None
|
||||
alternate = re.search(_ALTERNATE_ROOT, src.text)
|
||||
if alternate:
|
||||
spans.append(src.lines(alternate.start(), alternate.end()))
|
||||
depends = "unless saves/switch/ryujinx/portable exists, passed with -r"
|
||||
records.append(_record(
|
||||
emulator,
|
||||
"standalone",
|
||||
"passes_path",
|
||||
[],
|
||||
source_ref((src.path, [
|
||||
src.lines(found.start(), found.end()), src.lines(user.start(), create.end()),
|
||||
])),
|
||||
search_dir=f'<AppConfig.GetFullPath("{emulator}")>/{user.group(1)}',
|
||||
passes_to=f"PORTABLE_DIR, the user folder beside {binary}",
|
||||
source_ref((src.path, spans)),
|
||||
search_dir=f'<AppConfig.GetFullPath("{emulator}")>/{folder.group(2)}',
|
||||
passes_to=f"{handle}, the portable folder beside {binary}",
|
||||
fails_when_missing=False,
|
||||
depends_on_option=depends,
|
||||
))
|
||||
return records
|
||||
|
||||
|
||||
@@ -980,6 +980,7 @@ _UNPLACED_REASONS = {
|
||||
"outside_bios_folder": "read in a folder the platform keeps outside the BIOS tree",
|
||||
"no_roms_directory": "read below a roms folder the platform's registry does not name",
|
||||
"no_emulators_directory": "read in an emulator folder the platform's registry does not describe",
|
||||
"no_configs_directory": "read below a configs folder the platform's file does not record",
|
||||
"no_platform_slug": "no platform slug for its system",
|
||||
"below_platform_folder": "below the folder the platform lists",
|
||||
"no_save_directory": "read below a save directory the platform keeps in no fixed place",
|
||||
|
||||
@@ -1150,7 +1150,7 @@ class OuterRootsAreUntrusted(unittest.TestCase):
|
||||
def test_unsafe_from_bios_is_refused(self):
|
||||
for value in (*self.UNSAFE, "../../../x", "../x/..", "x", "./x", "/x", "..x", None):
|
||||
with self.subTest(from_bios=value), self.assertRaises(ValueError):
|
||||
install._check_roms_directory({"roms_directory": {**self.ROMS, "from_bios": value}})
|
||||
install._check_relative_directories({"roms_directory": {**self.ROMS, "from_bios": value}})
|
||||
|
||||
def test_the_relations_the_rom_folders_files_use_are_accepted(self):
|
||||
bios = Path("/card/system/bios")
|
||||
@@ -1161,13 +1161,13 @@ class OuterRootsAreUntrusted(unittest.TestCase):
|
||||
}
|
||||
for value, root in expected.items():
|
||||
with self.subTest(from_bios=value):
|
||||
install._check_roms_directory({"roms_directory": {**self.ROMS, "from_bios": value}})
|
||||
self.assertEqual(install._roms_root(bios, value), root)
|
||||
install._check_relative_directories({"roms_directory": {**self.ROMS, "from_bios": value}})
|
||||
self.assertEqual(install._relative_root(bios, value), root)
|
||||
self.assertEqual(install.MAX_ROMS_UPS, 2)
|
||||
|
||||
def test_a_list_without_a_roms_root_leaves_the_files_out(self):
|
||||
files = [{"dest": "pico-8/pico8_64", "root": "roms", "cores": ["x"]}]
|
||||
kept, left = install.place_roms_entries(files, Path("/card/roms/bios"), {}, "x", say=False)
|
||||
kept, left = install.place_relative_entries(files, Path("/card/roms/bios"), {}, "x", say=False)
|
||||
self.assertEqual((kept, [o["reason"] for o in left]), ([], ["no_roms_directory"]))
|
||||
|
||||
def test_unsafe_emulator_folders_are_refused(self):
|
||||
@@ -1192,7 +1192,7 @@ class OuterRootsAreUntrusted(unittest.TestCase):
|
||||
with unittest.mock.patch.object(install.os, "name", host):
|
||||
for value in ("../C:x", "..\\x", "../x:", "../Roms:x"):
|
||||
with self.subTest(host=host, from_bios=value), self.assertRaises(ValueError):
|
||||
install._check_roms_directory(
|
||||
install._check_relative_directories(
|
||||
{"roms_directory": {**self.ROMS, "from_bios": value}})
|
||||
for value in ("C:x/{key}", "x:/{key}", "emu:x/{key}"):
|
||||
layout = {**self.EMULATORS, "searched": [value], "default": value}
|
||||
|
||||
+135
-31
@@ -21,7 +21,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from launchers import handed_directories, standalone_bindings # noqa: E402
|
||||
from scraper import knulli_launchers as kl # noqa: E402
|
||||
from scraper import retrobat_launchers as rl # noqa: E402
|
||||
from scraper.batocera_launchers import ( # noqa: E402
|
||||
GENERATORS,
|
||||
@@ -128,35 +127,27 @@ class AFolderTheGeneratorEnters(unittest.TestCase):
|
||||
self.assertEqual(found, {"": ("outside", "")})
|
||||
|
||||
|
||||
class ADataFolderTheEnvironmentSets(unittest.TestCase):
|
||||
SUYU = f"{kl.GENERATORS}/suyu/suyuGenerator.py"
|
||||
TEXT = textwrap.dedent("""\
|
||||
from ...batoceraPaths import CACHE, CONFIGS, SAVES
|
||||
|
||||
class SuyuGenerator:
|
||||
def generate(self, system, rom):
|
||||
commandArray = ["/usr/bin/suyu", "-f", "-g", rom]
|
||||
return Command.Command(array=commandArray, env={
|
||||
"XDG_CONFIG_HOME": CONFIGS,
|
||||
"XDG_DATA_HOME": SAVES / "switch",
|
||||
})
|
||||
""")
|
||||
|
||||
def test_the_data_folder_is_a_record_and_binds_outside(self):
|
||||
files = {f"{kl.CONFIGGEN}/batoceraPaths.py": PATHS,
|
||||
f"{kl.GENERATORS}/importer.py": IMPORTER_TEXT, self.SUYU: self.TEXT}
|
||||
(record,) = records(kl.data_homes(kl.configgen(files.__getitem__)))
|
||||
self.assertEqual(
|
||||
(record["emulator"], record["search_dir"], record["passes_to"], record["source_ref"]),
|
||||
("suyu", "/userdata/saves/switch", "environment XDG_DATA_HOME", f"{self.SUYU}:8"),
|
||||
)
|
||||
config = {"base_destination": "bios", "standalone_cores": ["suyu"], "launchers": [record]}
|
||||
class AVariableADeclaredFolderIsSpelledFrom(unittest.TestCase):
|
||||
def test_a_data_folder_outside_the_tree_binds_outside(self):
|
||||
record = {"emulator": "emu", "build": "standalone", "kind": "passes_path",
|
||||
"names": [], "search_dir": "/userdata/saves/switch",
|
||||
"passes_to": "environment XDG_DATA_HOME", "source_ref": "x.py:1"}
|
||||
config = {"base_destination": "bios", "standalone_cores": ["emu"], "launchers": [record]}
|
||||
found = standalone_bindings(
|
||||
{"suyu"}, ["$XDG_DATA_HOME/suyu", "SuyuPath::KeysDir = keys"],
|
||||
handed_directories(config),
|
||||
{"emu"}, ["$XDG_DATA_HOME/emu"], handed_directories(config)
|
||||
)
|
||||
self.assertEqual(found, {"": ("outside", "")})
|
||||
|
||||
def test_a_data_folder_in_the_tree_binds_below_it(self):
|
||||
record = {"emulator": "emu", "build": "standalone", "kind": "passes_path",
|
||||
"names": [], "search_dir": "/userdata/bios/data",
|
||||
"passes_to": "environment XDG_DATA_HOME", "source_ref": "x.py:1"}
|
||||
config = {"base_destination": "bios", "standalone_cores": ["emu"], "launchers": [record]}
|
||||
found = standalone_bindings(
|
||||
{"emu"}, ["$XDG_DATA_HOME/emu"], handed_directories(config)
|
||||
)
|
||||
self.assertEqual(found, {"": ("", "data/emu")})
|
||||
|
||||
|
||||
class APortableUserFolder(unittest.TestCase):
|
||||
CITRON = """
|
||||
@@ -185,12 +176,28 @@ class APortableUserFolder(unittest.TestCase):
|
||||
}
|
||||
"""
|
||||
|
||||
RYUJINX = """
|
||||
public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution)
|
||||
{
|
||||
string path = AppConfig.GetFullPath("ryujinx");
|
||||
string portablePath = Path.Combine(AppConfig.GetFullPath("saves"), "switch", "ryujinx", "portable");
|
||||
string setupPath = Path.Combine(path, "portable");
|
||||
}
|
||||
"""
|
||||
|
||||
def sources(self) -> dict[str, rl.Source]:
|
||||
def source(ref: str, text: str) -> rl.Source:
|
||||
return rl.Source(ref, textwrap.dedent(text).lstrip("\n"))
|
||||
|
||||
def yuzu_like(exe: str) -> str:
|
||||
return self.CITRON.replace("citron.exe", exe)
|
||||
|
||||
return {
|
||||
f"{rl.GENERATORS}/Citron.Generator.cs": rl.Source(
|
||||
"c", textwrap.dedent(self.CITRON).lstrip("\n")),
|
||||
f"{rl.GENERATORS}/ShadPS4.Generator.cs": rl.Source(
|
||||
"s", textwrap.dedent(self.SHADPS4).lstrip("\n")),
|
||||
f"{rl.GENERATORS}/Citron.Generator.cs": source("c", self.CITRON),
|
||||
f"{rl.GENERATORS}/Eden.Generator.cs": source("e", yuzu_like("eden.exe")),
|
||||
f"{rl.GENERATORS}/Ryujinx.Generator.cs": source("r", self.RYUJINX),
|
||||
f"{rl.GENERATORS}/ShadPS4.Generator.cs": source("s", self.SHADPS4),
|
||||
f"{rl.GENERATORS}/Yuzu.Generator.cs": source("y", yuzu_like("yuzu.exe")),
|
||||
}
|
||||
|
||||
def test_each_generator_gives_its_user_folder(self):
|
||||
@@ -198,7 +205,15 @@ class APortableUserFolder(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
[(r["emulator"], r["search_dir"], r["source_ref"]) for r in found],
|
||||
[("citron", '<AppConfig.GetFullPath("citron")>/user', "c:3, 7-8"),
|
||||
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10")],
|
||||
("eden", '<AppConfig.GetFullPath("eden")>/user', "e:3, 7-8"),
|
||||
("ryujinx", '<AppConfig.GetFullPath("ryujinx")>/portable', "r:3-5"),
|
||||
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10"),
|
||||
("yuzu", '<AppConfig.GetFullPath("yuzu")>/user', "y:3, 7-8")],
|
||||
)
|
||||
ryujinx = found[2]
|
||||
self.assertEqual(ryujinx["passes_to"], "DefaultPortableDir, the portable folder beside Ryujinx.exe")
|
||||
self.assertEqual(
|
||||
ryujinx["depends_on_option"], "unless saves/switch/ryujinx/portable exists, passed with -r"
|
||||
)
|
||||
|
||||
def test_the_user_folder_binds_below_the_emulator_folder(self):
|
||||
@@ -213,5 +228,94 @@ class APortableUserFolder(unittest.TestCase):
|
||||
self.assertEqual(found, {"": ("emulators", "citron/user")})
|
||||
|
||||
|
||||
|
||||
class ADefaultTheBuildLacksIsNotRun(unittest.TestCase):
|
||||
"""KNULLI's configgen-defaults.yml picks suyu for the Switch, which its
|
||||
es_systems.yml does not offer and no package builds."""
|
||||
|
||||
ES = textwrap.dedent("""\
|
||||
switch:
|
||||
emulators:
|
||||
citron:
|
||||
citron: { requireAnyOf: [BR2_PACKAGE_CITRON] }
|
||||
voxatron:
|
||||
emulators: {}
|
||||
""")
|
||||
DEFAULTS = textwrap.dedent("""\
|
||||
switch:
|
||||
emulator: suyu
|
||||
core: suyu
|
||||
voxatron:
|
||||
emulator: lexaloffle
|
||||
core: vox_official
|
||||
kodi:
|
||||
emulator: kodi
|
||||
core: kodi
|
||||
""")
|
||||
LAUNCHERS = [{"emulator": "lexaloffle", "kind": "fixed_name", "passes_to": "exec",
|
||||
"search_dir": "/userdata/bios"}]
|
||||
|
||||
def test_only_what_a_package_or_the_user_supplies_is_run(self):
|
||||
from scraper import batocera_scraper as bs
|
||||
|
||||
paths = ["package/batocera/emulators/citron/Config.in", "package/kodi/Config.in"]
|
||||
built = bs.packages_and_programs(bs.package_names(paths), self.LAUNCHERS)
|
||||
cores, standalone, _dual = bs.configgen_cores(self.DEFAULTS, self.ES, set(), built)
|
||||
self.assertEqual(sorted(cores), ["citron", "kodi", "vox_official"])
|
||||
self.assertIn("lexaloffle", standalone)
|
||||
self.assertNotIn("suyu", standalone)
|
||||
by_system = bs.es_systems_by_system(self.ES, self.DEFAULTS, set(), built)
|
||||
self.assertEqual(by_system["switch"], ["citron"])
|
||||
# Without the packages, every default counts, as before.
|
||||
self.assertIn("suyu", bs.configgen_cores(self.DEFAULTS, self.ES, set())[0])
|
||||
|
||||
def test_the_real_knulli_list_names_no_suyu(self):
|
||||
import yaml
|
||||
|
||||
config = yaml.safe_load((REPO_ROOT / "platforms" / "knulli.yml").read_text())
|
||||
self.assertNotIn("suyu", config["cores"])
|
||||
self.assertNotIn("suyu", config.get("standalone_cores") or [])
|
||||
|
||||
|
||||
|
||||
class AUserFolderInTheConfigsFolder(unittest.TestCase):
|
||||
"""Batocera starts shadPS4 from /userdata/system/configs/shadps4, a folder
|
||||
of the configs root its batoceraPaths builds (../system/configs)."""
|
||||
|
||||
def test_the_sys_modules_go_below_the_configs_root(self):
|
||||
from common import load_emulator_profiles, load_platform_config, platform_layouts
|
||||
from common import standalone_set
|
||||
|
||||
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["shadps4"]
|
||||
for platform in ("batocera", "knulli"):
|
||||
config = load_platform_config(platform, str(REPO_ROOT / "platforms"))
|
||||
((view, _standalone),) = platform_layouts("shadps4", profile, standalone_set(config))
|
||||
with self.subTest(platform=platform):
|
||||
self.assertEqual(
|
||||
{(f.get("root"), f["standalone_path"].rsplit("/", 1)[0]) for f in view["files"]},
|
||||
{("configs", "shadps4/user/sys_modules"), ("configs", "shadps4/user/fonts/font")},
|
||||
)
|
||||
|
||||
def test_the_installer_follows_from_bios_to_the_configs_folder(self):
|
||||
import install
|
||||
|
||||
manifest = {"configs_directory": {"bios_folder": "bios", "from_bios": "../system/configs"}}
|
||||
install._check_relative_directories(manifest)
|
||||
files = [{"dest": "shadps4/user/sys_modules/libSceNgs2.sprx", "root": "configs"}]
|
||||
bios = Path("/userdata/bios")
|
||||
kept, left = install.place_relative_entries(files, bios, manifest, "x", say=False)
|
||||
self.assertEqual((len(kept), left), (1, []))
|
||||
self.assertEqual(
|
||||
install._entry_place(bios, kept[0])[0], Path("/userdata/system/configs")
|
||||
)
|
||||
kept, left = install.place_relative_entries(
|
||||
files, Path("/elsewhere/firmware"), manifest, "x", say=False)
|
||||
self.assertEqual((kept, [o["reason"] for o in left]), ([], ["no_configs_directory"]))
|
||||
for value in ("../C:x", "../../../x", "../system/../x"):
|
||||
with self.subTest(from_bios=value), self.assertRaises(ValueError):
|
||||
install._check_relative_directories(
|
||||
{"configs_directory": {"bios_folder": "bios", "from_bios": value}})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -383,10 +383,10 @@ class AProgramRunFromItsFolder(unittest.TestCase):
|
||||
import install
|
||||
|
||||
manifest = self.manifest("romsplat")
|
||||
install._check_roms_directory(manifest)
|
||||
install._check_relative_directories(manifest)
|
||||
files = list(manifest["files"])
|
||||
card = self.root / "card" / "roms" / "bios"
|
||||
kept, left = install.place_roms_entries(files, card, manifest, "romsplat", say=False)
|
||||
kept, left = install.place_relative_entries(files, card, manifest, "romsplat", say=False)
|
||||
self.assertEqual((len(kept), left), (2, []))
|
||||
self.assertEqual(
|
||||
install._entry_path(card, kept[0]),
|
||||
@@ -394,22 +394,22 @@ class AProgramRunFromItsFolder(unittest.TestCase):
|
||||
if install._entry_path(card, kept[0]).is_absolute() else card.parent / kept[0]["dest"],
|
||||
)
|
||||
other = self.root / "elsewhere"
|
||||
kept, left = install.place_roms_entries(files, other, manifest, "romsplat", say=False)
|
||||
kept, left = install.place_relative_entries(files, other, manifest, "romsplat", say=False)
|
||||
self.assertEqual(kept, [])
|
||||
self.assertEqual({o["reason"] for o in left}, {"no_roms_directory"})
|
||||
with self.assertRaises(ValueError):
|
||||
install._check_roms_directory({"files": files})
|
||||
install._check_relative_directories({"files": files})
|
||||
# A roms root beside the BIOS folder (Batocera's ../roms).
|
||||
beside = {**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": "../roms"}}
|
||||
install._check_roms_directory(beside)
|
||||
install._check_relative_directories(beside)
|
||||
disk = self.root / "userdata" / "bios"
|
||||
install.place_roms_entries(files, disk, beside, "romsplat", say=False)
|
||||
install.place_relative_entries(files, disk, beside, "romsplat", say=False)
|
||||
self.assertEqual(install._entry_place(disk, kept_entry := files[0])[0],
|
||||
self.root / "userdata" / "roms")
|
||||
self.assertEqual(kept_entry["root"], "roms")
|
||||
for climbing in ("roms", "../roms/..", "/storage/roms"):
|
||||
with self.subTest(from_bios=climbing), self.assertRaises(ValueError):
|
||||
install._check_roms_directory(
|
||||
install._check_relative_directories(
|
||||
{**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": climbing}}
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user