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.
This commit is contained in:
MiTHRAL 2026-08-24 13:21:46 -04:00
parent de4a4328ae
commit 7632978bf3
3 changed files with 230 additions and 0 deletions

View file

@ -361,6 +361,208 @@ 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);
f<<"[own] fabricate: catalog="<<cSize<<" had="<<(uSize>0?uSize:(int)have.size()-injected)<<" injected="<<injected<<"\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

@ -45,6 +45,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

@ -324,6 +324,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;
} }