#!/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"