Merge pull request #438 from outis1one/claude/wolf-pair-port-conflict-7nz8qg
Claude/wolf pair port conflict 7nz8qg
This commit is contained in:
+278
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user