DevourUnlocker/scripts/install.sh
MiTHRAL 626c78c266
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 7s
Build DevourUnlocker / linux (push) Failing after 6s
Build DevourUnlocker / standalone (push) Successful in 5s
fix truncated LaunchOptions without %command% + diagnose VDF 30-line window
- install.sh: also fix when LaunchOptions has version=n,b but missing %command% (truncated WINEDLLOVERRIDES="version=n,b" WINEDLLOVERRIDES=" case from previous malformed VDF)
- diagnose: use 30-line ahead window for Apps->1274570, show all 3 users OK
2026-08-24 00:34:53 -04:00

206 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
# DevourUnlocker — one-click per-platform installer (detects OS, pulls correct files, supports uninstall/update/repair)
# Primary proxy is version.dll (less flagged than winhttp.dll) — winhttp kept for compat
# Usage: bash install.sh [--install] [--uninstall] [--update|--repair] [--tag v2.1.0] [--game-dir /path] [--repo ad3laid3/DevourUnlocker]
REPO="${REPO:-ad3laid3/DevourUnlocker}"
TAG="${TAG:-latest}"
GAME_DIR=""
MODE="install"
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
*) echo "linux" ;;
esac
}
detect_game_dir() {
local cands=(
"$HOME/.local/share/Steam/steamapps/common/Devour"
"$HOME/.steam/steam/steamapps/common/Devour"
"/mnt/*/SteamLibrary/steamapps/common/Devour"
"C:/Program Files (x86)/Steam/steamapps/common/Devour"
)
for c in "${cands[@]}"; do for e in $c; do [[ -f "$e/DEVOUR.exe" ]] && { echo "$e"; return 0; }; done; done
local vdf="$HOME/.local/share/Steam/steamapps/libraryfolders.vdf"
if [[ -f "$vdf" ]]; then grep -o '"path"[[:space:]]*"[^"]*"' "$vdf" | cut -d'"' -f4 | while read -r p; do [[ -f "$p/steamapps/common/Devour/DEVOUR.exe" ]] && { echo "$p/steamapps/common/Devour"; return 0; }; done; fi
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--install) MODE="install"; shift;;
--uninstall) MODE="uninstall"; shift;;
--update|--repair) MODE="update"; shift;;
--tag) TAG="$2"; shift 2;;
--game-dir) GAME_DIR="$2"; shift 2;;
--repo) REPO="$2"; shift 2;;
*) echo "Unknown arg $1 (try --install|--uninstall|--update)"; exit 1;;
esac
done
if [[ -z "$GAME_DIR" ]]; then GAME_DIR="$(detect_game_dir || true)"; fi
if [[ -z "$GAME_DIR" || ! -f "$GAME_DIR/DEVOUR.exe" ]]; then echo "[!] Could not find Devour install. Use --game-dir /path/to/Devour"; exit 1; fi
OS=$(detect_os)
echo "[*] OS: $OS Game dir: $GAME_DIR Mode: $MODE Repo: $REPO Tag: $TAG"
# --- UNINSTALL ---
if [[ "$MODE" == "uninstall" ]]; then
echo "[*] Uninstalling (removing version.dll/winhttp.dll + Cloud patch)..."
rm -f "$GAME_DIR/version.dll" "$GAME_DIR/winhttp.dll" "$GAME_DIR/DevourUnlocker.dll" "$GAME_DIR/DevourUnlocker.json" "$GAME_DIR/DevourUnlocker.log"
[[ -f "$GAME_DIR/version.dll.bak" ]] && mv "$GAME_DIR/version.dll.bak" "$GAME_DIR/version.dll" && echo "[*] Restored version.dll.bak"
[[ -f "$GAME_DIR/winhttp.dll.bak" ]] && mv "$GAME_DIR/winhttp.dll.bak" "$GAME_DIR/winhttp.dll" && echo "[*] Restored winhttp.dll.bak"
# Revert collectables.json to Steam Cloud (remove our superset)
python3 -c "
import pathlib, json
for p in pathlib.Path.home().glob('.local/share/Steam/userdata/*/1274570/remote/collectables.json'):
try:
j=json.loads(p.read_text())
# clear to minimal (will be re-fetched from cloud on next launch)
p.write_text('{}')
print(f'[+] Cleared {p}')
except: pass
"
echo "[*] Uninstalled. Remove from Steam → Devour → Properties → Launch Options: WINEDLLOVERRIDES if you added it."
exit 0
fi
# --- UPDATE / REPAIR: redownload + verify GameAssembly hasn't shifted ---
if [[ "$MODE" == "update" ]]; then
echo "[*] Repair: checking GameAssembly hash + metadata offsets..."
GA="$GAME_DIR/GameAssembly.dll"
if [[ -f "$GA" ]]; then
sha=$(sha1sum "$GA" | cut -d' ' -f1)
echo "[*] GameAssembly sha1 $sha"
# Compare to last installed sha stored in DevourUnlocker.json if present
if [[ -f "$GAME_DIR/DevourUnlocker.json" ]]; then
old=$(python3 -c "import json,pathlib; print(json.loads(open('$GAME_DIR/DevourUnlocker.json').read()).get('gameAssemblySha',''))" 2>/dev/null || echo "")
if [[ -n "$old" && "$old" != "$sha" ]]; then
echo "[!] GameAssembly changed since last install (game updated) — will re-resolve offsets"
fi
fi
fi
# Fall through to install (redownload)
fi
# --- INSTALL ---
if [[ "$TAG" == "latest" ]]; then
# Forgejo (git.mithraic.cloud) uses /api/v1/repos/:owner/:repo/releases/latest
API="https://git.mithraic.cloud/api/v1/repos/$REPO/releases/latest"
if curl -s "$API" | grep -q "tag_name"; then
TAG=$(curl -s "$API" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
else
API="https://api.github.com/repos/$REPO/releases/latest"
TAG=$(curl -s "$API" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
fi
[[ -z "$TAG" ]] && { echo "[!] Could not resolve latest tag from $REPO"; exit 1; }
fi
echo "[*] Installing $REPO $TAG for $OS"
# Per-platform artifact names
if [[ "$OS" == "windows" ]]; then ZIP="DevourUnlocker_version_${TAG}.zip"; URL="https://git.mithraic.cloud/$REPO/releases/download/$TAG/$ZIP"; ALT_URL="https://github.com/$REPO/releases/download/$TAG/$ZIP"
elif [[ "$OS" == "macos" ]]; then echo "[!] macOS no native GameAssembly.dylib available — will install Linux/Windows version for Proton/Wine. Use --game-dir to override."; ZIP="DevourUnlocker_winhttp_${TAG}.zip"; URL="https://git.mithraic.cloud/$REPO/releases/download/$TAG/$ZIP"; ALT_URL="https://github.com/$REPO/releases/download/$TAG/$ZIP"
else ZIP="DevourUnlocker_version_${TAG}.zip"; URL="https://git.mithraic.cloud/$REPO/releases/download/$TAG/$ZIP"; ALT_URL="https://github.com/$REPO/releases/download/$TAG/$ZIP"; fi
TMP="$(mktemp -d)"
echo "[*] Downloading $URL (fallback $ALT_URL)"
if ! curl -L -o "$TMP/pkg.zip" "$URL"; then curl -L -o "$TMP/pkg.zip" "$ALT_URL" || { echo "[!] Download failed — check REPO/TAG"; echo " Fallback: build locally: cmake -B build && cmake --build build"; exit 1; }; fi
unzip -o "$TMP/pkg.zip" -d "$TMP" >/dev/null
# Backup existing version/winhttp if not ours (check for DevourUnlocker string)
for dll in version.dll winhttp.dll; do
if [[ -f "$GAME_DIR/$dll" ]]; then
if ! grep -q "DevourUnlocker" "$GAME_DIR/$dll" 2>/dev/null; then
cp "$GAME_DIR/$dll" "$GAME_DIR/$dll.bak" && echo "[*] Backed up $dll → $dll.bak"
fi
fi
done
# version.dll is sole unlocker (full hooks, less flagged than winhttp) — winhttp kept only as optional compat
if [[ -f "$TMP/version.dll" ]]; then cp -v "$TMP/version.dll" "$GAME_DIR/version.dll"; echo "[*] Installed version.dll (primary unlocker — all categories)"; fi
if [[ -f "$TMP/winhttp.dll" ]]; then cp -v "$TMP/winhttp.dll" "$GAME_DIR/winhttp.dll"; echo "[*] Installed winhttp.dll (optional compat, not needed)"; fi
cp -v "$TMP/DevourUnlocker.dll" "$GAME_DIR/DevourUnlocker.dll" 2>/dev/null || true
# Record GameAssembly hash for future update checks
GA_SHA=$(sha1sum "$GAME_DIR/GameAssembly.dll" 2>/dev/null | cut -d' ' -f1 || echo "")
if [[ -f "$TMP/DevourUnlocker.json" ]]; then
python3 -c "import json; j=json.load(open('$TMP/DevourUnlocker.json')); j['gameAssemblySha']='$GA_SHA'; open('$GAME_DIR/DevourUnlocker.json','w').write(json.dumps(j,indent=2))"
else
python3 -c "import json; open('$GAME_DIR/DevourUnlocker.json','w').write(json.dumps({'unlockRobes':True,'unlockCharacters':True,'unlockPerks':True,'unlockPets':True,'unlockFlashlights':True,'unlockEmotes':True,'unlockOutfits':True,'gameAssemblySha':'$GA_SHA'},indent=2))"
fi
# Patch Steam Cloud collectables.json via python helper (uses proper game calls: HasCollectedAll* checks)
echo "[*] Patching Steam Cloud collectables.json (proper game call: HasCollectedAllBarbedWires etc)..."
python3 "$(dirname "$0")/../standalone/devour_unlocker.py" unlock --all 2>&1 | tail -n 20 || echo "[!] Could not patch collectables.json — will still unlock via runtime hook"
echo "[*] Installed to $GAME_DIR"
# Auto-set Steam launch options for Proton silently (so user never needs manual WINEDLLOVERRIDES)
if [[ "$OS" == "linux" ]]; then
patched_any=0
for lc in "$HOME/.local/share/Steam/userdata/"*/config/localconfig.vdf; do
if [[ -f "$lc" ]] && grep -q '"1274570"' "$lc"; then
cp "$lc" "$lc.bak.$(date +%s)" 2>/dev/null || true
python3 -c "
import pathlib, re, sys
lc=sys.argv[1]
LAUNCH='WINEDLLOVERRIDES=\\\\\"version=n,b\\\\\" %command%'
p=pathlib.Path(lc)
txt=p.read_text(errors='ignore')
lines=txt.splitlines(True)
patched=0
i=0
while i < len(lines)-1:
if lines[i].strip()=='\"1274570\"' and lines[i].startswith('\t'*5) and lines[i+1].strip()=='{' and lines[i+1].startswith('\t'*5):
start=i+1; depth=1; j=i+2; found=None; found_idx=None; end=None
while j < len(lines):
s=lines[j].strip()
if s=='{': depth+=1
elif s=='}':
depth-=1
if depth==0: end=j; break
if s.startswith('\"LaunchOptions\"') and lines[j].startswith('\t'*6):
found=lines[j]; found_idx=j
j+=1
if end is None: i=j+1; continue
if found_idx is not None:
if 'version=n,b' in found and 'WINEDLLOVERRIDES' in found:
if '\\\\\"version=n,b\\\\\"' not in found or '%command%' not in found:
lines[found_idx]='\t'*6+'\"LaunchOptions\"\t\t\"'+LAUNCH+'\"\n'; print(f'[*] {lc}: fixed escaping/truncated'); patched+=1
else:
import re as _re
m=_re.search(r'\"LaunchOptions\"\s+\"([^\"]*)\"', found)
ex=m.group(1) if m else ''
exu=ex.replace('\\\\\"','\"')
if 'WINEDLLOVERRIDES' in exu:
new=exu.replace('winhttp=n,b','version=n,b').replace('winhttp=n','version=n,b')
if 'version=n,b' not in new:
new=f'WINEDLLOVERRIDES=\"version=n,b\" {exu}' if '%command%' not in exu else exu.replace('%command%','WINEDLLOVERRIDES=\"version=n,b\" %command%') if 'WINEDLLOVERRIDES' not in exu.split('%command%')[0] else exu
newe=new.replace('\"','\\\\\"')
lines[found_idx]='\t'*6+'\"LaunchOptions\"\t\t\"'+newe+'\"\n'; print(f'[*] {lc}: updated winhttp -> version'); patched+=1
else:
if '%command%' in exu: combined=exu.replace('%command%','WINEDLLOVERRIDES=\"version=n,b\" %command%') if exu.strip() and 'WINEDLLOVERRIDES' not in exu else f'WINEDLLOVERRIDES=\"version=n,b\" %command%'
else: combined=f'WINEDLLOVERRIDES=\"version=n,b\" {exu}' if exu.strip() else 'WINEDLLOVERRIDES=\"version=n,b\" %command%'
combe=combined.replace('\"','\\\\\"')
lines[found_idx]='\t'*6+'\"LaunchOptions\"\t\t\"'+combe+'\"\n'; print(f'[*] {lc}: prepended version'); patched+=1
else:
lines.insert(end, '\t'*6+'\"LaunchOptions\"\t\t\"'+LAUNCH+'\"\n'); print(f'[*] {lc}: inserted LaunchOptions'); patched+=1
i=end+1+(1 if found is None else 0); continue
i+=1
if patched: p.write_text(''.join(lines)); print(f'[+] {lc}: patched {patched} block(s)')
else: print(f'[=] {lc}: no Apps 1274570 block')
" "$lc" 2>&1 | head -n 10
[[ $? -eq 0 ]] && patched_any=1
fi
done
if [[ $patched_any -eq 0 ]]; then echo "[!] No localconfig.vdf with Apps->1274570 found — launch once from Steam, then re-run install.sh"; fi
echo "[*] Linux/Proton: version.dll is sole unlocker beside DEVOUR.exe — Steam Launch Options now auto-set, just press Play (restart Steam if it was running). No manual WINEDLLOVERRIDES needed."
echo "[*] Fallback: bash $(dirname "$0")/launch.sh (also auto-sets WINEDLLOVERRIDES)"
else
echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded (UnityPlayer.dll imports VERSION.dll) — no launch option needed. Just launch Devour."
fi
echo "[*] version.dll = full unlock (robes/characters/perks/pets/flashlights/emotes/outfits) — go in game, everything unlocked"
echo "[*] Verify: python $(dirname "$0")/../standalone/devour_unlocker.py diagnose"
rm -rf "$TMP"