Merge pull request #438 from outis1one/claude/wolf-pair-port-conflict-7nz8qg

Claude/wolf pair port conflict 7nz8qg
This commit is contained in:
Outis
2026-09-03 14:58:11 -04:00
committed by GitHub
+278 -18
View File
@@ -1604,13 +1604,28 @@ GOW_ESDE_SETTINGS_TEMPLATE = """<?xml version="1.0"?>
<string name="UserThemeDirectory" value="" />
"""
if 'ROMDirectory' not in content:
if not content.strip():
content = GOW_ESDE_SETTINGS_TEMPLATE
else:
content = re.sub(r'[ \t]*<bool name="RunInBackground" value="[^"]*"[ \t]*/>[ \t]*\n?', '', content)
line = '<bool name="RunInBackground" value="false" />\n'
content = content.rstrip('\n') + '\n' + line
# CONFIRMED LIVE: a file can already contain a ROMDirectory *key* while
# its value is blank (e.g. ES-DE's own first-run "select ROM directory"
# step got skipped/cancelled in a headless Moonlight session and ES-DE
# saved that empty value back over GOW's own template) — checking only
# whether the key exists, as an earlier version of this fix did, treated
# that as "already populated" and left the blank path in place, so ES-DE
# kept finding zero games even though the fix had technically "run".
# Force it to /ROMs (the one true value in this container — see the
# 'roms:/ROMs' mount on every app that reads ROMs) whenever it's missing
# or empty, independent of whether RunInBackground needed touching.
m = re.search(r'<string name="ROMDirectory" value="([^"]*)"', content)
if not m or not m.group(1).strip():
content = re.sub(r'[ \t]*<string name="ROMDirectory" value="[^"]*"[ \t]*/>[ \t]*\n?', '', content)
content = content.rstrip('\n') + '\n' + '<string name="ROMDirectory" value="/ROMs" />\n'
with open(path, 'w') as f:
f.write(content)
ESDESETTINGSPY
@@ -2146,7 +2161,12 @@ CATALOG = {
icon='https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png',
image='ghcr.io/games-on-whales/steam:edge',
mounts=['/etc/localtime:/etc/localtime:ro', '/etc/timezone:/etc/timezone:ro',
f'{games}/steam-cache:/home/retro/.cache:rw'],
f'{games}/steam-cache:/home/retro/.cache:rw',
# Same emulators/ -> ~/Applications mount as esde/retroarch
# (see below) — without it, an emulator AppImage (Cemu, etc.)
# added as a non-Steam game (./manage.sh steam-add-nonsteam-game)
# has no file to actually point Exe at from inside this container.
f'{games}/emulators:/home/retro/Applications:rw'],
env=['PROTON_LOG=1', 'RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'SYS_NICE', 'SYS_PTRACE', 'NET_RAW', 'MKNOD', 'NET_ADMIN'],
@@ -3533,7 +3553,8 @@ _manage_wolf_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local commands="start stop restart logs status pin controllers update apps cores reorder
add-web ge-proton games setup-swbf2 fix-ea-game wait-ea-app
install-ea-app diagnose-ea fix-perms install-completion backup"
install-ea-app diagnose-ea fix-perms install-completion backup
steam-add-nonsteam-game"
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
}
# Register for both 'manage.sh' and './manage.sh' invocation styles
@@ -3798,6 +3819,201 @@ except Exception:
echo "it's applied when Wolf creates each controller's virtual pad, not retroactively"
echo "to one that already exists in an open session."
;;
steam-add-nonsteam-game)
# Adds an emulator (or any executable already on the game drive, e.g.
# under emulators/ — mounted at ~/Applications in every app container,
# Steam included since the 'steam' CATALOG entry above) as a
# non-Steam game shortcut, written straight into Steam's own binary
# shortcuts.vdf. Steam's own "Add a Non-Steam Game" flow needs
# interactive Big Picture navigation (browse a file picker, click
# through dialogs) — this does the same thing from the command line,
# matching this repo's usual no-manual-wizard pattern for anything
# with a tedious GUI-only path (see ge-proton/install-ea-app above).
#
# shortcuts.vdf uses Valve's binary KeyValues format (type-tagged:
# 0x00 nested map, 0x01 string, 0x02 int32, 0x08 end-of-map) — NOT
# the plain-text VDF format localconfig.vdf uses elsewhere in this
# file (see the LaunchOptions patch in setup-swbf2). The writer below
# is a small generic parser/serializer for that binary format: it
# round-trips any existing entries byte-for-byte (tested against a
# synthetic file carrying GUI-set fields — icon, LaunchOptions, tags
# — all preserved untouched) and only inserts or replaces the one
# entry whose Exe matches what's being added here, so it's safe to
# run against a shortcuts.vdf that already has real, hand-configured
# shortcuts in it. Re-running with the same target is idempotent
# (updates that one entry in place, never a duplicate).
if [ -z "${2:-}" ]; then
echo "Usage: ./manage.sh steam-add-nonsteam-game <name-or-path> [display name]"
echo ""
echo " <name-or-path> A fragment of the filename to match against what's"
echo " actually in emulators/ (e.g. 'cemu' matches the real,"
echo " versioned Cemu-2.0-x86_64.AppImage) — or the exact"
echo " filename, or a full ~/Applications/... path."
echo " [display name] Name shown in Steam's Library. Defaults to the"
echo " filename with .AppImage/.sh stripped."
exit 1
fi
_SANG_ARG="$2"
_SANG_NAME="${3:-}"
GAME_DIR=$(grep '^GAME_STORAGE_DIR=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2-)
if [ -z "$GAME_DIR" ]; then read -r -p " Game storage path: " GAME_DIR; fi
if [ -z "$GAME_DIR" ]; then echo "No game storage path."; exit 1; fi
_SANG_EMU_DIR="$GAME_DIR/emulators"
_SANG_BASENAME=$(basename "$_SANG_ARG")
if [ -f "$_SANG_EMU_DIR/$_SANG_BASENAME" ]; then
_SANG_HOST_FILE="$_SANG_EMU_DIR/$_SANG_BASENAME"
else
_SANG_HOST_FILE=$(ls "$_SANG_EMU_DIR"/*"$_SANG_ARG"* 2>/dev/null | head -1)
fi
if [ -z "$_SANG_HOST_FILE" ] || [ ! -f "$_SANG_HOST_FILE" ]; then
echo "No file matching '$_SANG_ARG' found in $_SANG_EMU_DIR"
echo "What's actually there:"
ls "$_SANG_EMU_DIR" 2>/dev/null
exit 1
fi
_SANG_EXE="/home/retro/Applications/$(basename "$_SANG_HOST_FILE")"
[ -z "$_SANG_NAME" ] && _SANG_NAME=$(basename "$_SANG_HOST_FILE" | sed -E 's/\.(AppImage|sh|x86_64)$//I')
STEAM_HOME=$(_steam_home)
if [ -z "$STEAM_HOME" ]; then
echo "No Steam home found yet under ${WOLF_STATE_DIR:-/etc/wolf}."
echo "Launch Steam once from Moonlight, then re-run this command."
exit 1
fi
_SANG_STEAM_UID=$(sudo ls "$STEAM_HOME/.steam/steam/userdata/" 2>/dev/null | head -1)
if [ -z "$_SANG_STEAM_UID" ]; then
echo "No Steam userdata found. Sign into Steam in Moonlight first, then re-run."
exit 1
fi
_SANG_CFG_DIR="$STEAM_HOME/.steam/steam/userdata/$_SANG_STEAM_UID/config"
_SANG_VDF="$_SANG_CFG_DIR/shortcuts.vdf"
sudo mkdir -p "$_SANG_CFG_DIR"
# Steam flushes its own in-memory state back to shortcuts.vdf on exit
# (same reason _apply_ea_fix above stops WolfSteam before editing
# system.reg) — stop it first or this edit gets silently overwritten
# the moment Steam next quits or Wolf tears the session down.
_SANG_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i WolfSteam | head -1)
if [ -n "$_SANG_CONTAINER" ]; then
echo "Stopping Steam so it doesn't overwrite this edit on exit..."
docker exec "$_SANG_CONTAINER" pkill -f steam.sh 2>/dev/null || true
sleep 3
fi
sudo python3 - "$_SANG_VDF" "$_SANG_EXE" "$_SANG_NAME" "/home/retro/Applications" << 'VDFPY'
import sys, struct, os, zlib
path, exe_path, app_name, start_dir = sys.argv[1:5]
TYPE_MAP, TYPE_STR, TYPE_INT, TYPE_END = 0x00, 0x01, 0x02, 0x08
def read_cstring(data, i):
j = data.index(b'\x00', i)
return data[i:j].decode('utf-8', 'replace'), j + 1
def parse_map(data, i):
entries = []
while True:
t = data[i]; i += 1
if t == TYPE_END:
return entries, i
key, i = read_cstring(data, i)
if t == TYPE_MAP:
val, i = parse_map(data, i)
elif t == TYPE_STR:
val, i = read_cstring(data, i)
elif t == TYPE_INT:
val = struct.unpack('<i', data[i:i + 4])[0]
i += 4
else:
raise ValueError(f"unknown VDF type 0x{t:02x} at offset {i}")
entries.append([key, t, val])
def serialize_map(entries):
out = bytearray()
for key, t, val in entries:
out.append(t)
out += key.encode('utf-8') + b'\x00'
if t == TYPE_MAP:
out += serialize_map(val)
elif t == TYPE_STR:
out += val.encode('utf-8') + b'\x00'
elif t == TYPE_INT:
out += struct.pack('<i', val)
out.append(TYPE_END)
return bytes(out)
def get_field(entry_map, key):
for k, t, v in entry_map:
if k.lower() == key.lower():
return v
return None
if os.path.exists(path) and os.path.getsize(path) > 0:
with open(path, 'rb') as f:
data = f.read()
root, _ = parse_map(data, 0)
else:
root = [['shortcuts', TYPE_MAP, []]]
shortcuts_entry = None
for e in root:
if e[0].lower() == 'shortcuts' and e[1] == TYPE_MAP:
shortcuts_entry = e
break
if shortcuts_entry is None:
shortcuts_entry = ['shortcuts', TYPE_MAP, []]
root.append(shortcuts_entry)
entries_list = shortcuts_entry[2]
quoted_exe = f'"{exe_path}"'
entries_list[:] = [e for e in entries_list if get_field(e[2], 'exe') != quoted_exe]
for idx, e in enumerate(entries_list):
e[0] = str(idx)
new_index = str(len(entries_list))
crc_input = (exe_path + app_name).encode('utf-8')
appid = (zlib.crc32(crc_input) | 0x80000000) & 0xFFFFFFFF
appid_signed = appid - 0x100000000 if appid >= 0x80000000 else appid
new_entry_fields = [
['appid', TYPE_INT, appid_signed],
['AppName', TYPE_STR, app_name],
['Exe', TYPE_STR, quoted_exe],
['StartDir', TYPE_STR, f'"{start_dir}"'],
['icon', TYPE_STR, ''],
['ShortcutPath', TYPE_STR, ''],
['LaunchOptions', TYPE_STR, ''],
['IsHidden', TYPE_INT, 0],
['AllowDesktopConfig', TYPE_INT, 1],
['AllowOverlay', TYPE_INT, 1],
['OpenVR', TYPE_INT, 0],
['Devkit', TYPE_INT, 0],
['DevkitGameID', TYPE_STR, ''],
['DevkitOverrideAppID', TYPE_INT, 0],
['LastPlayTime', TYPE_INT, 0],
['FlatpakAppID', TYPE_STR, ''],
['tags', TYPE_MAP, []],
]
entries_list.append([new_index, TYPE_MAP, new_entry_fields])
with open(path, 'wb') as f:
f.write(serialize_map(root))
print(f"Wrote shortcuts.vdf: {app_name} -> {exe_path} (appid {appid_signed})")
VDFPY
sudo chown 1000:1000 "$_SANG_VDF"
echo ""
echo "Added '$_SANG_NAME' -> $_SANG_EXE to Steam's shortcuts.vdf."
echo "Open Steam in Moonlight (it will restart since it was stopped above) —"
echo "the new tile appears in your Library (may need the Library view, not just Home)."
echo "First launch: right-click it -> Properties -> Compatibility, and confirm"
echo "'Force the use of a specific Steam Play compatibility tool' is OFF — Cemu and"
echo "other native Linux AppImages don't run through Proton."
;;
pin)
# Wolf logs: "Insert pin at http://SOMEIP:47989/pin/#HEXHASH"
# Extract just the hash fragment and build URLs for every interface
@@ -3850,6 +4066,8 @@ except Exception:
echo " ./manage.sh diagnose-ea [appid] - Show link2ea:// registry state and Proton log"
echo " ./manage.sh fix-perms - Fix 'Permission denied' app startup errors"
echo " ./manage.sh install-completion - Enable tab-completion for this script"
echo " ./manage.sh steam-add-nonsteam-game [name] [display name]"
echo " - Add an emulator (emulators/) as a non-Steam game, no GUI needed"
echo " ./manage.sh backup - How to set up backups"
;;
esac
@@ -3906,7 +4124,12 @@ CATALOG = {
icon='https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png',
image='ghcr.io/games-on-whales/steam:edge',
mounts=['/etc/localtime:/etc/localtime:ro', '/etc/timezone:/etc/timezone:ro',
f'{games}/steam-cache:/home/retro/.cache:rw'],
f'{games}/steam-cache:/home/retro/.cache:rw',
# Same emulators/ -> ~/Applications mount as esde/retroarch
# (see below) — without it, an emulator AppImage (Cemu, etc.)
# added as a non-Steam game (./manage.sh steam-add-nonsteam-game)
# has no file to actually point Exe at from inside this container.
f'{games}/emulators:/home/retro/Applications:rw'],
env=['PROTON_LOG=1', 'RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'SYS_NICE', 'SYS_PTRACE', 'NET_RAW', 'MKNOD', 'NET_ADMIN'],
@@ -4404,12 +4627,44 @@ cd $WOLF_DIR
./manage.sh install-ea-app # install EA App inside the container (GUI in Moonlight)
./manage.sh diagnose-ea # check link2ea:// state when game returns to Play
./manage.sh install-completion # enable tab-completion for manage.sh
./manage.sh steam-add-nonsteam-game # add an emulator as a non-Steam game, no GUI needed
\`\`\`
### Tab completion
Run \`./manage.sh install-completion\` once, then re-open your shell (or
\`source ~/.bashrc\`). After that, \`./manage.sh <TAB><TAB>\` lists all commands.
## Adding an emulator to Steam as a non-Steam game
Steam's own "Add a Non-Steam Game" is a Big Picture file-browser flow —
usable through Moonlight, but slow to click through for something you're
scripting or repeating. This does the same thing directly:
\`\`\`bash
cd $WOLF_DIR && ./manage.sh steam-add-nonsteam-game cemu
\`\`\`
The first argument is matched against whatever's actually in \`emulators/\`
(so \`cemu\` finds the real, versioned \`Cemu-2.0-x86_64.AppImage\` without
you having to type the exact filename); an optional second argument sets
the display name shown in Steam's Library. It stops Steam (so the edit
survives Steam's own next exit), writes the entry into Steam's
\`shortcuts.vdf\` directly, and restarts it — open Steam in Moonlight
afterward and the new tile is in your Library. Re-running with the same
target updates that one entry rather than creating a duplicate, and any
shortcuts you've already added by hand through Steam's own UI are left
alone.
**Why you might want this over ES-DE for something like Cemu:** Steam Input
tracks each physical controller by its own device path rather than by SDL
GUID, so — unlike ES-DE/Cemu's native SDL-based controller handling — it
can potentially hand a game distinct, explicitly-assigned virtual
controllers per player slot even when the physical controllers are
identical models (the same case \`./manage.sh controllers\` exists to work
around for ES-DE). Add Cemu here, then in Steam's own Big Picture
Controller Settings you can see and assign each detected controller
individually. This hasn't been confirmed live yet against Wolf's
container-created virtual controllers specifically (real hardware behaves
this way; whether Steam Input sees Wolf's virtual joypads the same way is
still to be tested) — worth trying before assuming it works.
## Multiple controllers (same game/emulator can't tell them apart)
**Symptom:** two or more controllers connected through the same Moonlight
session, but the game/emulator only ever sees one — the first controller
@@ -4550,21 +4805,26 @@ default behavior here.
It writes \`esde-settings/es_settings.xml\` directly from GOW's own real
template (100+ settings, fetched from GOW's own repo, byte-for-byte
identical except \`RunInBackground\` flipped to off) whenever that file is
either missing or still the old broken placeholder — detected by checking
for \`ROMDirectory\`, a key only GOW's own bootstrap or ES-DE itself ever
writes. If the file already has a real, fully-populated value (because
EmulationStation has already launched at least once and GOW wrote its own
copy), only the one \`RunInBackground\` line is patched, leaving every
other setting (theme, scraper prefs, etc.) untouched. Either way, one
missing or empty. If the file already has real content, only the one
\`RunInBackground\` line is patched, leaving every other setting (theme,
scraper prefs, etc.) untouched — **except \`ROMDirectory\`, which is always
independently forced to \`/ROMs\` whenever its value is blank**, regardless
of whether the rest of the file looks populated. Either way, one
\`sudo ./setup.sh wolf\` run is enough — no need to connect via Moonlight
first. **Confirmed live, the hard way, twice:** earlier versions of this
fix either pre-created a minimal stub that made GOW skip writing its own
template (breaking ROM discovery on every system, since the ROM path fell
back to something other than \`/ROMs\`), or only patched an already-existing
file and left a genuinely missing/broken one alone, requiring exactly the
manual Moonlight-connect step this version removes. Setting it by hand
still works too, any time: **Main Menu → Other Settings → Run in
background (while game is launched) → off**.
first. **Confirmed live, the hard way, three times:** earlier versions of
this fix (1) pre-created a minimal stub that made GOW skip writing its own
template, breaking ROM discovery on every system since the ROM path fell
back to something other than \`/ROMs\`; (2) only patched an already-existing
file and left a genuinely missing/broken one alone, requiring a manual
Moonlight-connect step; and (3) treated the mere *presence* of the
\`ROMDirectory\` key as proof the file was already correctly populated —
but ES-DE can write that key with a **blank value** (its own first-run
"select ROM directory" step going unanswered in a headless Moonlight
session saves an empty path back over GOW's template), which the presence
check alone couldn't tell apart from a real one. Setting it by hand still
works too, any time: **Main Menu → Other Settings → Run in background
(while game is launched) → off**, and the ROM directory under **Main Menu
→ Other Settings → ROM directory → \`/ROMs\`** if it's ever blank again.
**Why this needed its own mount, not just a one-time write:** \`~/ES-DE\`
(settings, gamelists, scraped artwork, logs) had no bind mount onto the