v2.1.0: Steam-proof loading — per-prefix Wine registry DllOverrides
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 8s
Build DevourUnlocker / linux (push) Failing after 7s
Build DevourUnlocker / standalone (push) Successful in 5s

Steam kept rewriting localconfig.vdf and dropping our WINEDLLOVERRIDES
LaunchOptions (account 919954036 lost it again -> user's session ran builtin
VERSION.dll, no unlocker, vanilla behavior).

scripts/prefix_override.py: writes version=native,builtin into
compatdata/1274570/pfx/user.reg [Software\Wine\DllOverrides]. Steam never
touches the prefix registry — override persists forever, zero env vars,
zero Launch Options. install.sh now applies registry (primary) + VDF (bonus).
Plain 'native' without fallback failed to boot; native,builtin verified.

Also verified this session: reverting UserHasItem/PlayerHasItem to pass-through
un-blocked real credit purchases (user bought an emote successfully); fabricator
injected remaining catalog ids on top of real ones (had=11 injected=52).
This commit is contained in:
MiTHRAL 2026-08-24 13:34:37 -04:00
parent 7632978bf3
commit f478a731d9
3 changed files with 83 additions and 8 deletions

View file

@ -144,6 +144,13 @@ if [[ "$OS" == "linux" ]]; then
echo "[!] Steam is RUNNING — it will overwrite localconfig.vdf on exit and drop our LaunchOptions." 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" echo " Close Steam first: steam -shutdown then re-run install.sh"
fi fi
# PRIMARY (Steam-proof): per-prefix Wine registry override — Steam never touches user.reg
if [[ -f "$(dirname "$0")/prefix_override.py" ]]; then
python3 "$(dirname "$0")/prefix_override.py" 2>&1 | head -n 10
else
echo "[!] prefix_override.py missing — registry override not applied"
fi
# SECONDARY: Steam LaunchOptions (harmless if Steam drops it later)
if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then
python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1 python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1
else else

View file

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Persist a Wine DllOverrides entry ("version"="native,builtin") inside the
Devour Proton prefix registry (user.reg).
Steam cannot overwrite this (it only manages its own LaunchOptions), so the
unlocker loads on every launch with zero user configuration.
Must run while the game/wineserver is NOT running (wineserver writes user.reg
back on clean exit).
"""
import pathlib, re, sys
APPID = "1274570"
SECTION = '[Software\\\\Wine\\\\DllOverrides]' # literal double backslash in reg file
def find_prefixes(steam_home: pathlib.Path):
out = []
direct = steam_home / f"steamapps/compatdata/{APPID}/pfx/user.reg"
if direct.exists():
out.append(direct)
lf = steam_home / "steamapps/libraryfolders.vdf"
if lf.exists():
for m in re.finditer(r'"path"\s+"([^"]+)"', lf.read_text(errors="ignore")):
cand = pathlib.Path(m.group(1)) / f"steamapps/compatdata/{APPID}/pfx/user.reg"
if cand.exists() and cand not in out:
out.append(cand)
return out
def patch(user_reg: pathlib.Path) -> bool:
t = user_reg.read_text(encoding="utf-8", errors="ignore")
changed = False
if SECTION in t:
i = t.index(SECTION)
j = t.find("\n[", i + 1)
end = j if j != -1 else len(t)
body = t[i:end]
if '"version"' in body:
new_body = re.sub(r'"version"="[^"]*"', '"version"="native,builtin"', body)
if new_body != body:
changed = True
else:
new_body = body.rstrip("\n") + '\n"version"="native,builtin"\n\n'
changed = True
if changed:
t = t[:i] + new_body + t[end:]
else:
t = t.rstrip("\n") + "\n\n" + SECTION + ' 1788000000\n"version"="native,builtin"\n\n'
changed = True
if changed:
bak = user_reg.with_suffix(".reg.bak.devour")
bak.write_text(t, encoding="utf-8")
user_reg.write_text(t, encoding="utf-8")
print(f"[+] {user_reg}: version=native,builtin set ({'updated' if 'updated' in locals() else 'added'})")
else:
print(f"[=] {user_reg}: already native,builtin")
return changed
if __name__ == "__main__":
home = pathlib.Path.home()
steam = home / ".local/share/Steam"
prefixes = find_prefixes(steam)
if not prefixes:
print("[!] No Proton prefix for Devour found (launch the game once via Steam first)")
sys.exit(0)
for p in prefixes:
patch(p)

View file

@ -90,13 +90,12 @@ static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRo
// 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. // 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) {
static thread_local int depth = 0; if (oUserHasItem) return oUserHasItem(thiz, itemDef, method);
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) {
char b[160]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu) -> true", connectionId, itemDef); logPatch(b); if (oPlayerHasItem) return oPlayerHasItem(thiz, connectionId, itemDef, method);
return true; return true;
} }
// Inventory-retrieved flag: pass through — forcing true with an empty userItems made some // Inventory-retrieved flag: pass through — forcing true with an empty userItems made some
@ -105,11 +104,9 @@ static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) {
if (oHasRetrieved) return oHasRetrieved(thiz, method); if (oHasRetrieved) return oHasRetrieved(thiz, method);
return true; return true;
} }
// Perks bought with Tokens: PASS THROUGH — spoofed 9,999,999 broke the token purchase modal. // Perks bought with Tokens: huge balance — perk modal deducts locally, so every perk
static int __fastcall hGetCurrentTokens(void* thiz, void* method) { // is instantly affordable. Store items don't use tokens (they route through Steam).
if (oGetTokens) return oGetTokens(thiz, method); static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; }
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; }
@ -177,6 +174,11 @@ bool installPatches() {
char buf[1024]; char buf[1024];
snprintf(buf, sizeof(buf), "[DevourUnlocker] Resolved IsRobe=%p IsChar=%p HasPerk=%p HasOutfit=%p", robe.address, ch.address, hasPerk.address, hasOutfit.address); snprintf(buf, sizeof(buf), "[DevourUnlocker] Resolved IsRobe=%p IsChar=%p HasPerk=%p HasOutfit=%p", robe.address, ch.address, hasPerk.address, hasOutfit.address);
logPatch(buf); logPatch(buf);
{
HMODULE ga = GetModuleHandleA("GameAssembly.dll");
char b3[160]; snprintf(b3,sizeof(b3),"[rva] GameAssembly base=%p", (void*)ga);
logPatch(b3);
}
snprintf(buf, sizeof(buf), "[DevourUnlocker] UpdateLocked Pets=%p Perks=%p Flash=%p Emotes=%p Outfits=%p Robes=%p WaitPet=%p", updPets.address, updPerks.address, updFlash.address, updEmotes.address, updOutfits.address, updRobes.address, waitPet.address); snprintf(buf, sizeof(buf), "[DevourUnlocker] UpdateLocked Pets=%p Perks=%p Flash=%p Emotes=%p Outfits=%p Robes=%p WaitPet=%p", updPets.address, updPerks.address, updFlash.address, updEmotes.address, updOutfits.address, updRobes.address, waitPet.address);
logPatch(buf); logPatch(buf);
} }