v2.0.11: fabricate ownership at the source — inject catalog into SteamInventoryManager.userItems
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:
parent
de4a4328ae
commit
7632978bf3
3 changed files with 230 additions and 0 deletions
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue