Compare commits

..

5 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
MiTHRAL
7632978bf3 v2.0.11: fabricate ownership at the source — inject catalog into SteamInventoryManager.userItems
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
Trace proved pet/outfit UI never consults UserHasItem/HasPerk for lock display
(0 HasPerk calls while browsing; buys no-op'd on 'already owned' guard) — display
reads the raw userItems list field directly.

fabricateOwnership(): resolves SteamInventoryManager.instance static, walks
steamItems (List<SteamInventoryItem>, itemID int @+0x10), and List<Add>s every
missing def id into userItems (List<SteamItemDef_t>).

Critical fix: SteamItemDef_t is a 4-byte int enum even though
il2cpp_class_array_element_size reports 8 — reading with 8-byte stride produced
composite garbage ids so dedup never matched and values looked corrupt.
All readers/writers now use 4-byte stride; add verified in-memory:
add sent=1 stored_last=1 ... injected=59, game stable.

Periodic refill every 15s x240 (game recreates the list on inventory refresh).
Inspector keeps container summaries only (element deref removed after hang).
GetCurrentTokens/HasRetrievedUserItems remain pass-through (spoofing broke
credit purchases); UserHasItem/PlayerHasItem stay true as belt-and-braces.
2026-08-24 13:21:46 -04:00
5 changed files with 352 additions and 10 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

@ -17,6 +17,10 @@
namespace devour { 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 #ifdef _WIN32
static HMODULE g_gameAsm = nullptr; static HMODULE g_gameAsm = nullptr;
@ -361,6 +365,227 @@ void Il2CppResolver::dumpDiagnostics(){
#endif #endif
} }
void Il2CppResolver::inspectInventoryState(){
#ifdef _WIN32
try {
char exe[MAX_PATH]; GetModuleFileNameA(nullptr, exe, MAX_PATH);
char* slash = strrchr(exe, '\\'); if(!slash) slash = strrchr(exe, '/');
std::string logPath = slash ? std::string(exe, slash - exe) + "\\DevourUnlocker.log" : "DevourUnlocker.log";
std::ofstream f(logPath, std::ios::app);
f << "[inv] inspectInventoryState BEGIN\n" << std::unitbuf;
auto domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto klass_from_name = reinterpret_cast<void*(*)(void*, const char*, const char*)>(getProc("il2cpp_class_from_name"));
auto field_from_name = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_field_from_name"));
auto field_static_get = reinterpret_cast<void(*)(void*, void*)>(getProc("il2cpp_field_static_get_value")); // (field, out)
auto field_get_value = reinterpret_cast<void(*)(void*, void*, void*)>(getProc("il2cpp_field_get_value"));
auto obj_get_class = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_object_get_class"));
auto class_get_name_fn = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_name"));
auto arr_elem_size_cls = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_class_array_element_size"));
auto fields_iter = reinterpret_cast<void*(*)(void*, void**)>(getProc("il2cpp_class_get_fields"));
auto field_name_fn = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_field_get_name"));
auto field_off = reinterpret_cast<size_t(*)(void*)>(getProc("il2cpp_field_get_offset"));
if(!domain_get || !klass_from_name || !field_from_name || !field_static_get || !field_get_value || !obj_get_class || !class_get_name_fn){
f << "[inv] missing exports\n"; return;
}
void* domain = domain_get();
if(!domain){ f << "[inv] no domain\n"; return; }
// find SteamInventoryManager class across assemblies
auto dom_asm = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto asm_img = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
if(!dom_asm || !asm_img){ f << "[inv] no asm exports\n"; return; }
size_t n=0; const void** asms = dom_asm(domain,&n);
void* simClass=nullptr;
for(size_t i=0;i<n && !simClass;++i){
void* img=asm_img((void*)asms[i]); if(!img) continue;
simClass = klass_from_name(img,"","SteamInventoryManager");
}
if(!simClass){ f << "[inv] SteamInventoryManager class not found\n"; return; }
f << "[inv] SIM class " << class_get_name_fn(simClass) << "\n";
void* fInst = field_from_name(simClass,"instance");
if(!fInst){ f << "[inv] no 'instance' field\n"; return; }
void* mgr=nullptr;
field_static_get(fInst,&mgr);
if(!mgr){ f << "[inv] manager instance null (not Awake yet)\n"; return; }
f << "[inv] manager=" << mgr << "\n";
const char* containers[] = {"userItems","steamItems","m_SteamItemDetails",nullptr};
for(int ci=0;containers[ci];++ci){
void* fld = field_from_name(simClass,containers[ci]);
if(!fld){ f << "[inv] field "<<containers[ci]<<" missing\n"; continue; }
void* val=nullptr;
field_get_value(mgr,fld,&val);
if(!val){ f << "[inv] "<<containers[ci]<<" = null\n"; continue; }
void* vcls=obj_get_class(val);
f << "[inv] "<<containers[ci]<<" obj="<<val<<" class="<<(vcls?class_get_name_fn(vcls):"?\n");
f << "\n";
// If List<T>: _items @0x10, _size @0x18
unsigned char* lst=(unsigned char*)val;
void* arr = *(void**)(lst+0x10);
int32_t lsize = *(int32_t*)(lst+0x18);
f << "[inv] _items="<<arr<<" _size="<<lsize<<"\n";
if(!arr){ continue; }
void* arrCls = *(void**)arr;
int esz = arrCls && arr_elem_size_cls ? arr_elem_size_cls(arrCls) : 0;
const char* aname = arrCls? class_get_name_fn(arrCls):"?";
uint64_t maxlen = *(uint64_t*)((unsigned char*)arr+0x18);
f << "[inv] array class="<<aname<<" elemSize="<<esz<<" capacity="<<maxlen<<"\n";
}
f << "[inv] inspect END\n"; f.flush();
} catch(...){
try{ char e2[MAX_PATH]; GetModuleFileNameA(nullptr,e2,MAX_PATH); char* s2=strrchr(e2,'\\'); if(!s2) s2=strrchr(e2,'/'); if(s2){ std::ofstream f2(std::string(e2,s2-e2)+"\\DevourUnlocker.log",std::ios::app); f2<<"[inv] EXCEPTION during inspect (guarded)\n"; } }catch(...){}
}
#endif
}
int Il2CppResolver::fabricateOwnership(){
#ifdef _WIN32
int injected = -1; // -1 = not ready / error
try{
auto domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto thread_attach = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_thread_attach"));
auto klass_from_name = reinterpret_cast<void*(*)(void*, const char*, const char*)>(getProc("il2cpp_class_from_name"));
auto field_from_name = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_field_from_name"));
auto field_static_get = reinterpret_cast<void(*)(void*, void*)>(getProc("il2cpp_field_static_get_value"));
auto field_get_value = reinterpret_cast<void(*)(void*, void*, void*)>(getProc("il2cpp_field_get_value"));
auto obj_get_class = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_object_get_class"));
auto class_get_name_fn = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_name"));
auto get_method = reinterpret_cast<void*(*)(void*, const char*, int)>(getProc("il2cpp_class_get_method_from_name"));
auto invoke_fn = reinterpret_cast<void*(*)(void*, void*, void**, void**)>(getProc("il2cpp_runtime_invoke"));
if(!domain_get || !thread_attach || !klass_from_name || !field_from_name || !field_static_get ||
!field_get_value || !obj_get_class || !class_get_name_fn || !get_method || !invoke_fn) return -1;
void* domain = domain_get();
if(!domain) return -1;
if(thread_attach) thread_attach(domain);
auto dom_asm = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto asm_img = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
if(!dom_asm || !asm_img) return -1;
size_t n=0; const void** asms = dom_asm(domain,&n);
// SteamInventoryManager class + Steamworks.SteamItemDef_t enum class
void* simClass=nullptr; void* defEnumClass=nullptr;
for(size_t i=0;i<n;++i){
void* img=asm_img((void*)asms[i]); if(!img) continue;
if(!simClass) simClass = klass_from_name(img,"","SteamInventoryManager");
if(!defEnumClass) defEnumClass = klass_from_name(img,"Steamworks","SteamItemDef_t");
if(simClass && defEnumClass) break;
}
if(!simClass) return -1;
void* fInst = field_from_name(simClass,"instance");
if(!fInst) return -1;
void* mgr=nullptr;
field_static_get(fInst,&mgr);
if(!mgr) return -1;
void* fUser = field_from_name(simClass,"userItems");
void* fCat = field_from_name(simClass,"steamItems");
if(!fUser || !fCat) return -1;
void* userList=nullptr; void* catList=nullptr;
field_get_value(mgr,fUser,&userList);
field_get_value(mgr,fCat,&catList);
if(!userList || !catList) return -1;
unsigned char* ul=(unsigned char*)userList;
void* uArr=*(void**)(ul+0x10); int32_t uSize=*(int32_t*)(ul+0x18);
unsigned char* cl=(unsigned char*)catList;
void* cArr=*(void**)(cl+0x10); int32_t cSize=*(int32_t*)(cl+0x18);
if(!cArr || cSize<=0) return -1;
// existing owned ids (List<SteamItemDef_t>, enum is 4-byte int despite array_element_size saying 8)
std::vector<uint64_t> have;
if(uArr){
unsigned char* data=(unsigned char*)uArr+0x20;
for(int i=0;i<uSize;++i) have.push_back((uint64_t)*(uint32_t*)(data+(size_t)i*4));
}
auto hasId=[&](uint64_t id){ for(auto v:have) if(v==id) return true; return false; };
if(uSize >= cSize && cSize>0) return 0; // already fully populated
void* addMethod = get_method(obj_get_class(userList),"Add",1);
if(!addMethod) return -1;
unsigned char* cData=(unsigned char*)cArr+0x20;
// DIAGNOSTIC: hex-dump sample ids so mismatches are visible
auto dumpIds=[&](const char* tag){
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH);
char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/'); if(!s) return;
std::ofstream f(std::string(exe,s-exe)+"\\DevourUnlocker.log",std::ios::app);
f<<"[dbg] "<<tag<<":";
if(strcmp(tag,"have")==0){ for(size_t i=0;i<have.size()&&i<8;++i) f<<" "<<std::hex<<(unsigned long long)have[i]; }
else { for(int i=0;i<cSize&&i<8;++i){ void* ip=*(void**)(cData+(size_t)i*8); if(!ip) continue; uint64_t d=(uint64_t)*(uint32_t*)((unsigned char*)ip+0x10); f<<" "<<std::hex<<(unsigned long long)d; } }
f<<std::dec<<"\n";
}catch(...){}
};
dumpIds("have");
dumpIds("catalog");
injected=0;
int verified=0;
for(int i=0;i<cSize;++i){
void* itemPtr=*(void**)(cData+(size_t)i*8);
if(!itemPtr) continue;
uint64_t defId=(uint64_t)*(uint32_t*)((unsigned char*)itemPtr+0x10); // SteamInventoryItem.itemID @+0x10 (int-backed)
if(defId==0 || hasId(defId)) continue;
void* exc=nullptr; void* arg=&defId;
invoke_fn(addMethod,userList,&arg,&exc);
if(exc){ continue; }
have.push_back(defId);
++injected;
// verify first 3 adds actually stored the right value
if(verified<3){
void* uArrNow=*(void**)(ul+0x10); int32_t szNow=*(int32_t*)(ul+0x18);
if(uArrNow && szNow>0){
unsigned char* dNow=(unsigned char*)uArrNow+0x20;
uint64_t last=(uint64_t)*(uint32_t*)(dNow+(size_t)(szNow-1)*4);
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH);
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<<std::hex<<"[dbg] add sent="<<(unsigned long long)defId<<" stored_last="<<(unsigned long long)last<<" size="<<std::dec<<szNow<<"\n"; }
}catch(...){}
}
++verified;
}
}
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH);
char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/');
if(s){
std::ofstream f(std::string(exe,s-exe)+"\\DevourUnlocker.log",std::ios::app);
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; }
return injected;
#else
return -1;
#endif
}
#else // Linux stubs #else // Linux stubs
uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; } uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; }

View file

@ -2,6 +2,7 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
#include <set>
// Minimal IL2CPP types — avoids needing il2cpp headers at compile time // Minimal IL2CPP types — avoids needing il2cpp headers at compile time
struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; }; struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; };
@ -9,6 +10,12 @@ struct MethodInfo;
namespace devour { 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. // Resolves IL2CPP method pointers without hard offsets.
// Strategy 1: via il2cpp exports (class/method lookup) // Strategy 1: via il2cpp exports (class/method lookup)
// Strategy 2: xref string sigscan as fallback // Strategy 2: xref string sigscan as fallback
@ -45,6 +52,15 @@ public:
static ResolvedMethod playerHasItem(); // SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID) static ResolvedMethod playerHasItem(); // SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID)
static ResolvedMethod hasRetrievedUserItems(); // SteamInventoryManager.HasRetrievedUserInventoryItems() static ResolvedMethod hasRetrievedUserItems(); // SteamInventoryManager.HasRetrievedUserInventoryItems()
static void dumpDiagnostics(); static void dumpDiagnostics();
// Raw introspection of SteamInventoryManager singleton containers (userItems etc).
// Logs element type/size/sample bytes so we can fabricate ownership entries safely.
static void inspectInventoryState();
// Injects every catalog def id from steamItems into userItems (List<SteamItemDef_t>::Add).
// Returns number newly injected. Safe to call repeatedly — the game recreates the
// list on inventory refresh, so run this periodically.
static int fabricateOwnership();
// Accessors used by the inspector/fabricator
static void* getProcPub(const char* name) { return getProc(name); }
static uintptr_t gameAssemblyBase(); static uintptr_t gameAssemblyBase();
static size_t gameAssemblySize(); static size_t gameAssemblySize();
}; };

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. // 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; // pass-through + trace (v2.1.0 proved forcing true blocks purchases)
if (depth < 2) { depth++; char b[128]; snprintf(b,sizeof(b),"[trace] UserHasItem(def=%llu) -> true", itemDef); logPatch(b); depth--; } RememberQueriedDef((uint64_t)itemDef);
return true; 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) // 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); // MUST stay pass-through: forcing true desyncs lobby validation vs injected
return true; // 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 // 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 // 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); 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 +181,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);
} }
@ -324,6 +333,25 @@ bool installPatches() {
} }
g_installed = any; g_installed = any;
logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed"); logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed");
// Ownership fabricator + introspection thread:
// - every 15s inject missing catalog ids into userItems (list is recreated on refresh)
// - one layout dump at 30s for diagnostics
CreateThread(nullptr, 0, [](LPVOID)->DWORD{
Sleep(30000);
logPatch("[DevourUnlocker] inspectInventoryState @30s");
Il2CppResolver::inspectInventoryState();
int total=0;
for(int cycle=0; cycle<240; ++cycle){ // ~60 min of refills
Sleep(15000);
int n = Il2CppResolver::fabricateOwnership();
if(n>0){
total+=n;
char b[128]; snprintf(b,sizeof(b),"[DevourUnlocker] ownership injected +%d (session total %d)",n,total);
logPatch(b);
}
}
return 0;
}, nullptr, 0, nullptr);
return any; return any;
} }