Files
PS5_RetroArch/tools/esde-settings.py
T
Mihawk eaba7a8114 Run EmulationStation (ES-DE 3.5.0) on the PS5 beside RetroArch
ES-DE is its own executable of the title, /app0/es-de/es-de.bin, started
through LoadExec. It draws through ../PS5_OpenGL and shows RetroArch's
content folders as its systems (frontends/es-de/systems.tsv), with the
Alekfull NX theme by default.

- tools/build-esde.sh: the dependencies (FreeType, HarfBuzz, ICU, pugixml,
  curl, FFmpeg and now libwebp, tools/build-libwebp.sh), the pinned stb
  headers, ES-DE at 50e4b600 with frontends/es-de/patches, CMake, and the
  pinned Alekfull NX theme.
- frontends/es-de: the port's main, a FreeImage stand-in over stb_image
  and libwebp, an armed framebuffer capture, and the staging of
  resources, themes and the systems file.
- tools/build-frontend.sh binds every import the SDK's platform layer
  replaces (readlink was a null import), wraps the thread functions as
  eboot.bin does, and fails on an import only libkernel_sys exports.
- frontends/common: iswctype_l with FreeBSD masks (the console's
  Dinkumware iswctype faulted), pathconf, lstat answered as stat (refused
  to a title), and a getcwd that can name a working directory.
- tools/run-title.sh --frontend-capture; tools/esde-settings.py.

Evidence: evidence/esde-first-light/ (capture.json, expectation.json,
capture-test.jsonl). On the base PS5 (13.40): ES-DE loads 24 systems and
the Alekfull NX theme in 1.4 s, with no fatal signal and no image it
could not load; the 30 s capture is the SNES system view at 1920x1080.
Command:
  PS5_FRONTENDS=es-de tools/build-title.sh
  tools/esde-settings.py Theme=alekfull-nx-es-de StartupSystem=snes
  tools/run-title.sh --no-build --relaunch-test=1 \
    --relaunch-image=es-de/es-de.bin --frontend-capture=15,30,50 --watch 70
Gates: tools/verify.sh PASS (format unit build integration evidence).
Known: no audio (the SDL2 build has none), games do not start yet.
2026-10-04 16:31:50 -04:00

86 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Read or change EmulationStation's settings on the console (frontends/es-de).
tools/esde-settings.py print the settings that are set
tools/esde-settings.py Theme=alekfull-nx-es-de StartupSystem=snes
ES-DE keeps its settings in /app0/es-de/ES-DE/settings/es_settings.xml, one element
per setting (<bool>, <int>, <float> or <string>, each with name and value) and no
root. A setting is changed in place, keeping its type; a new one is added as a
string. ES-DE writes the file itself when it quits or its menu changes a setting,
so change it while ES-DE is not running. The console's address and FTP account
are the ones tools/deploy-title.py reads from .env.
"""
from pathlib import Path
import importlib.util
import io
import json
import re
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ps5_ftp import connect # noqa: E402
ELEMENT = re.compile(r'<(bool|int|float|string) name="([^"]+)" value="([^"]*)" />')
def load_deploy():
spec = importlib.util.spec_from_file_location('dt', Path(__file__).resolve().parent / 'deploy-title.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def escape(value):
return value.replace('&', '&amp;').replace('"', '&quot;').replace('<', '&lt;').replace('>', '&gt;')
def apply(text, changes):
"""The settings file with each name=value of changes set, in place or added."""
lines = text.splitlines() if text.strip() else ['<?xml version="1.0"?>']
for name, value in changes:
for index, line in enumerate(lines):
match = ELEMENT.fullmatch(line.strip())
if match and match.group(2) == name:
lines[index] = f'<{match.group(1)} name="{name}" value="{escape(value)}" />'
break
else:
lines.append(f'<string name="{name}" value="{escape(value)}" />')
return '\n'.join(lines) + '\n'
def main(argv):
changes = []
for argument in argv[1:]:
name, separator, value = argument.partition('=')
if not separator or not re.fullmatch(r'[A-Za-z][A-Za-z0-9]*', name):
raise SystemExit(f'expected Name=value, not {argument!r}')
changes.append((name, value))
deploy = load_deploy()
param = Path(__file__).resolve().parent.parent / 'sce_sys' / 'param.json'
title = json.loads(param.read_text())['titleId']
remote = f'/data/homebrew/{title}/es-de/ES-DE/settings/es_settings.xml'
with connect(**deploy.load_settings()) as ftp:
current = io.BytesIO()
try:
ftp.retrbinary(f'RETR {remote}', current.write)
except Exception:
pass
text = current.getvalue().decode('utf-8')
if not changes:
sys.stdout.write(text)
return 0
updated = apply(text, changes)
ftp.storbinary(f'STOR {remote}', io.BytesIO(updated.encode('utf-8')))
check = io.BytesIO()
ftp.retrbinary(f'RETR {remote}', check.write)
if check.getvalue() != updated.encode('utf-8'):
raise SystemExit('the console did not keep the settings file as written')
for name, value in changes:
print(f' {name} = {value}')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))