Compare commits

..

4 commits

Author SHA1 Message Date
MiTHRAL
ecf1e680d6 v2.1.5: self-learning ownership — inject queried out-of-catalog defs (Supporter/DLC)
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 8s
Build DevourUnlocker / linux (push) Failing after 6s
Build DevourUnlocker / standalone (push) Successful in 5s
User session traces: only defs 7 & 51 queried all session, both OUTSIDE the
59-id store catalog -> those are DLC-granted items (Supporter flashlights).
Blind 1..256 sweep polluted userItems and broke pet display (reverted v2.1.4).

Now hUserHasItem/hPlayerHasItem remember every def the game asks about into a
global registry; fabricateOwnership() injects any remembered-but-missing id on
its next 15s cycle. Only game-validated ids enter the list — converges to
everything-owned without garbage.
2026-08-24 13:54:41 -04:00
MiTHRAL
817a2eff78 v2.1.4: remove def-id range sweep — polluting userItems with unknown ids broke pet display; restore exact v2.1.0 injection semantics, keep pass-through traces + catalog dump
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 7s
Build DevourUnlocker / linux (push) Failing after 7s
Build DevourUnlocker / standalone (push) Successful in 5s
2026-08-24 13:48:35 -04:00
MiTHRAL
fee1556da3 v2.1.1: PlayerHasItem -> true — emote validator gate (StoreEmotesOwnedOnSteam path)
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 8s
Build DevourUnlocker / linux (push) Failing after 8s
Build DevourUnlocker / standalone (push) Successful in 5s
2026-08-24 13:38:56 -04:00
MiTHRAL
f478a731d9 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).
2026-08-24 13:34:37 -04:00
5 changed files with 123 additions and 11 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 " Close Steam first: steam -shutdown then re-run install.sh"
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
python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1
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

@ -17,6 +17,10 @@
namespace devour {
static std::set<uint64_t> g_queriedDefs;
std::set<uint64_t>& QueriedDefs(){ return g_queriedDefs; }
void RememberQueriedDef(uint64_t id){ g_queriedDefs.insert(id); }
#ifdef _WIN32
static HMODULE g_gameAsm = nullptr;
@ -553,7 +557,26 @@ int Il2CppResolver::fabricateOwnership(){
char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/');
if(s){
std::ofstream f(std::string(exe,s-exe)+"\\DevourUnlocker.log",std::ios::app);
f<<"[own] fabricate: catalog="<<cSize<<" had="<<(uSize>0?uSize:(int)have.size()-injected)<<" injected="<<injected<<"\n";
static bool loggedCatalog=false;
if(!loggedCatalog && cSize>0){
loggedCatalog=true;
f<<"[own] catalog ids:";
for(int i=0;i<cSize;++i){ void* ip=*(void**)(cData+(size_t)i*8); if(!ip) continue; f<<" "<<*(uint32_t*)((unsigned char*)ip+0x10); }
f<<"\n";
}
// Self-learning: inject any def the game queried but that lives outside the
// store catalog (Supporter/DLC flashlights etc). Valid ids only - no sweeps.
int learned=0;
for(uint64_t qid : g_queriedDefs){
if(qid==0 || hasId(qid)) continue;
void* exc=nullptr; void* arg=&qid;
invoke_fn(addMethod,userList,&arg,&exc);
if(exc) continue;
have.push_back(qid);
++learned;
}
f<<"[own] fabricate: catalog="<<cSize<<" had="<<(uSize>0?uSize:(int)(have.size()-injected))<<" injected="<<injected<<" learned="<<learned<<"\n";
}
}catch(...){}
} catch(...){ injected=-1; }

View file

@ -2,6 +2,7 @@
#include <cstdint>
#include <string>
#include <vector>
#include <set>
// Minimal IL2CPP types — avoids needing il2cpp headers at compile time
struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; };
@ -9,6 +10,12 @@ struct MethodInfo;
namespace devour {
// Def ids the game itself asked UserHasItem/PlayerHasItem about (DLC/promo defs
// absent from the store catalog live here). Fabricator injects these too.
std::set<uint64_t>& QueriedDefs();
void RememberQueriedDef(uint64_t id);
// Resolves IL2CPP method pointers without hard offsets.
// Strategy 1: via il2cpp exports (class/method lookup)
// Strategy 2: xref string sigscan as fallback

View file

@ -90,14 +90,20 @@ static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRo
// 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 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;
// pass-through + trace (v2.1.0 proved forcing true blocks purchases)
RememberQueriedDef((uint64_t)itemDef);
char b[96]; snprintf(b,sizeof(b),"[trace] UserHasItem(def=%llu)", (unsigned long long)itemDef); logPatch(b);
if (oUserHasItem) return oUserHasItem(thiz, itemDef, method);
return false;
}
// 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) {
char b[160]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu) -> true", connectionId, itemDef); logPatch(b);
return true;
// MUST stay pass-through: forcing true desyncs lobby validation vs injected
// userItems and UNLOCKS REGRESS (pets locked again). See v2.1.1 regression.
RememberQueriedDef((uint64_t)itemDef);
char b[128]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu)", connectionId, (unsigned long long)itemDef); logPatch(b);
if (oPlayerHasItem) return oPlayerHasItem(thiz, connectionId, itemDef, method);
return false;
}
// 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
@ -105,11 +111,9 @@ 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;
}
// Perks bought with Tokens: huge balance — perk modal deducts locally, so every perk
// is instantly affordable. Store items don't use tokens (they route through Steam).
static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; }
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; }
@ -177,6 +181,11 @@ bool installPatches() {
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);
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);
logPatch(buf);
}