v2.0.10: trace build — log ownership queries, unbreak token purchases
- GetCurrentTokens + HasRetrievedUserItems now pass through to original:
spoofed tokens broke the credit-purchase modal; forced retrieved-flag with
empty userItems made flows treat items as definitively not-owned
- UserHasItem/PlayerHasItem keep returning true but LOG itemDef on every call;
IsCharacterUnlocked/HasPerk/HasOutfit log decoded string args
-> next play session reveals whether pet/perk/outfit UI consults these at all
(if silent while browsing locked pets, UI reads userItems list field directly)
- install.sh: replace fragile inline heredoc python with scripts/vdf_patch.py
(chr(92)-built bytes; the old heredoc produced \\ corruption that silently
killed LaunchOptions for account 919954036); warn when Steam is running
This commit is contained in:
parent
d9a50caa4f
commit
de4a4328ae
3 changed files with 115 additions and 68 deletions
|
|
@ -138,65 +138,19 @@ python3 "$(dirname "$0")/../standalone/devour_unlocker.py" unlock --all 2>&1 | t
|
||||||
|
|
||||||
echo "[*] Installed to $GAME_DIR"
|
echo "[*] Installed to $GAME_DIR"
|
||||||
# Auto-set Steam launch options for Proton silently (so user never needs manual WINEDLLOVERRIDES)
|
# 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
|
if [[ "$OS" == "linux" ]]; then
|
||||||
patched_any=0
|
if pgrep -x steam >/dev/null 2>&1; then
|
||||||
for lc in "$HOME/.local/share/Steam/userdata/"*/config/localconfig.vdf; do
|
echo "[!] Steam is RUNNING — it will overwrite localconfig.vdf on exit and drop our LaunchOptions."
|
||||||
if [[ -f "$lc" ]] && grep -q '"1274570"' "$lc"; then
|
echo " Close Steam first: steam -shutdown then re-run install.sh"
|
||||||
cp "$lc" "$lc.bak.$(date +%s)" 2>/dev/null || true
|
fi
|
||||||
python3 -c "
|
if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then
|
||||||
import pathlib, re, sys
|
python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1
|
||||||
lc=sys.argv[1]
|
else
|
||||||
LAUNCH='WINEDLLOVERRIDES=\\\\\"version=n,b\\\\\" %command%'
|
echo "[!] vdf_patch.py missing — cannot auto-set Launch Options"
|
||||||
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
|
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
|
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)"
|
echo "[*] Fallback: bash $(dirname "$0")/launch.sh (also auto-sets WINEDLLOVERRIDES)"
|
||||||
else
|
else
|
||||||
echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded (UnityPlayer.dll imports VERSION.dll) — no launch option needed. Just launch Devour."
|
echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded (UnityPlayer.dll imports VERSION.dll) — no launch option needed. Just launch Devour."
|
||||||
|
|
|
||||||
76
scripts/vdf_patch.py
Normal file
76
scripts/vdf_patch.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -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 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 hIsChar(void* thiz, void* charStr, void* method) {
|
||||||
static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { return true; }
|
logPatch((std::string("[trace] IsCharacterUnlocked(\"") + il2cppStringToUtf8(charStr) + "\") -> true").c_str());
|
||||||
static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { return true; }
|
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
|
// 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
|
// (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); }
|
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 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 hUpdateLockedOutfits(void* thiz, void* method) { if (oUpdOutfits) oUpdOutfits(thiz, method); }
|
||||||
static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRobes) oUpdRobes(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) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
// Multiplayer validator: every player counts as owning every item (lobby validation path)
|
// 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) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
// Inventory-retrieved flag: unblocks PopulateInventoryWhenAvailable even if the Steam
|
// Inventory-retrieved flag: pass through — forcing true with an empty userItems made some
|
||||||
// inventory query returns nothing under Proton — views populate with UserHasItem=true
|
// flows treat items as definitively NOT owned; original behavior is safer now
|
||||||
static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) { return true; }
|
static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) {
|
||||||
// Perks bought with Tokens: report a huge balance so every perk is purchasable/selectable
|
if (oHasRetrieved) return oHasRetrieved(thiz, method);
|
||||||
static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; }
|
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) {
|
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; }
|
if (!addr) { logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue