diff --git a/scripts/install.sh b/scripts/install.sh index 1e41034..ccbccf5 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -138,65 +138,19 @@ python3 "$(dirname "$0")/../standalone/devour_unlocker.py" unlock --all 2>&1 | t echo "[*] Installed to $GAME_DIR" # Auto-set Steam launch options for Proton silently (so user never needs manual WINEDLLOVERRIDES) +# Uses scripts/vdf_patch.py — bash-escaped heredoc python caused \\\" corruption before. 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 pgrep -x steam >/dev/null 2>&1; then + echo "[!] Steam is RUNNING — it will overwrite localconfig.vdf on exit and drop our LaunchOptions." + echo " Close Steam first: steam -shutdown then re-run install.sh" + fi + if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then + python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1 + else + echo "[!] vdf_patch.py missing — cannot auto-set Launch Options" + fi 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 "[*] Linux/Proton: version.dll is sole unlocker beside DEVOUR.exe — Steam Launch Options now auto-set, just press Play. If Steam was running during this install: steam -shutdown, re-run install.sh, reopen Steam." 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." diff --git a/scripts/vdf_patch.py b/scripts/vdf_patch.py new file mode 100644 index 0000000..206883f --- /dev/null +++ b/scripts/vdf_patch.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Patch Steam localconfig.vdf LaunchOptions for Devour (1274570). + +Sets WINEDLLOVERRIDES="version=n,b" %command% inside every userdata/*/config/ +localconfig.vdf Apps->1274570 block. Must be run while Steam is CLOSED — Steam +rewrites localconfig.vdf from memory on exit and discards external edits. + +VDF escaping note: quotes inside values are a single backslash + quote (\"). +Getting that through bash heredocs is what broke account 919954036 before +(it ended up as \\\"). This module builds bytes explicitly via chr(92). +""" +import pathlib, sys + +APPID = "1274570" +BQ = chr(92) + '"' # backslash + quote +TARGET_LINE = ( + "\t" * 6 + '"LaunchOptions"' + "\t\t" + + '"WINEDLLOVERRIDES=' + BQ + 'version=n,b' + BQ + ' %command%"' + "\n" +) + +def patch(path: pathlib.Path) -> bool: + lines = path.read_text(errors="ignore").splitlines(True) + changed = False + i = 0 + while i < len(lines) - 1: + if ( + lines[i].strip() == f'"{APPID}"' + and lines[i].startswith("\t" * 5) + and lines[i + 1].strip() == "{" + and lines[i + 1].startswith("\t" * 5) + ): + depth = 1 + j = i + 2 + lo_i = 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): + lo_i = j + j += 1 + if end is None: + i = j + 1 + continue + if lo_i is None: + lines.insert(end, TARGET_LINE) + print(f"{path.parent.parent.name}: inserted LaunchOptions") + changed = True + elif lines[lo_i] != TARGET_LINE: + print(f"{path.parent.parent.name}: fixed {lines[lo_i].strip()[:70]!r}") + lines[lo_i] = TARGET_LINE + changed = True + else: + print(f"{path.parent.parent.name}: already exact") + i = end + 1 + continue + i += 1 + if changed: + bak = path.with_suffix(".vdf.bak.devour") + bak.write_text("".join(lines)) + path.write_text("".join(lines)) + return changed + +if __name__ == "__main__": + paths = [pathlib.Path(a) for a in sys.argv[1:]] or list( + pathlib.Path.home().glob(".local/share/Steam/userdata/*/config/localconfig.vdf") + ) + for p in paths: + if p.exists(): + patch(p) diff --git a/src/patches.cpp b/src/patches.cpp index 8d90178..1db6103 100644 --- a/src/patches.cpp +++ b/src/patches.cpp @@ -66,9 +66,18 @@ static std::string il2cppStringToUtf8(void* ilStr) { } static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* characterStr, void* method) { return true; } -static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { return true; } -static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { return true; } -static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { return true; } +static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { + logPatch((std::string("[trace] IsCharacterUnlocked(\"") + il2cppStringToUtf8(charStr) + "\") -> true").c_str()); + return true; +} +static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { + logPatch((std::string("[trace] HasPerk(\"") + il2cppStringToUtf8(perkStr) + "\") -> true").c_str()); + return true; +} +static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { + logPatch((std::string("[trace] HasOutfit(\"") + il2cppStringToUtf8(outfitStr) + "\",\"" + il2cppStringToUtf8(characterStr) + "\") -> true").c_str()); + return true; +} // UpdateLocked*: call the ORIGINAL so the UI runs its normal flow — ownership checks // (UserHasItem→true) make it mark everything unlocked instead of leaving defaults locked static void __fastcall hUpdateLockedPets(void* thiz, void* method) { if (oUpdPets) oUpdPets(thiz, method); } @@ -78,21 +87,29 @@ static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { if ( static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { if (oUpdEmotes) oUpdEmotes(thiz, method); } static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { if (oUpdOutfits) oUpdOutfits(thiz, method); } static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRobes) oUpdRobes(thiz, method); } -// Ownership override: every Steam catalog item (pets/flashlights/emotes/outfits/promos) counts as owned +// Ownership override: every Steam catalog item (pets/flashlights/emotes/outfits/promos) counts as owned. +// LOGS every query — if the pet UI never appears in the log, it reads the userItems list field directly. static bool __fastcall hUserHasItem(void* thiz, unsigned long long itemDef, void* method) { - (void)itemDef; + static thread_local int depth = 0; + if (depth < 2) { depth++; char b[128]; snprintf(b,sizeof(b),"[trace] UserHasItem(def=%llu) -> true", itemDef); logPatch(b); depth--; } return true; } // Multiplayer validator: every player counts as owning every item (lobby validation path) static bool __fastcall hPlayerHasItem(void* thiz, int connectionId, unsigned long long itemDef, void* method) { - (void)connectionId; (void)itemDef; + char b[160]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu) -> true", connectionId, itemDef); logPatch(b); return true; } -// Inventory-retrieved flag: unblocks PopulateInventoryWhenAvailable even if the Steam -// inventory query returns nothing under Proton — views populate with UserHasItem=true -static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) { return true; } -// Perks bought with Tokens: report a huge balance so every perk is purchasable/selectable -static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; } +// Inventory-retrieved flag: pass through — forcing true with an empty userItems made some +// flows treat items as definitively NOT owned; original behavior is safer now +static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) { + if (oHasRetrieved) return oHasRetrieved(thiz, method); + return true; +} +// Perks bought with Tokens: PASS THROUGH — spoofed 9,999,999 broke the token purchase modal. +static int __fastcall hGetCurrentTokens(void* thiz, void* method) { + if (oGetTokens) return oGetTokens(thiz, method); + return 0; +} static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*), UpdateLockedFn* origOut) { if (!addr) { logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; }