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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; }
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@ public:
|
|||
static ResolvedMethod playerHasItem(); // SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID)
|
||||
static ResolvedMethod hasRetrievedUserItems(); // SteamInventoryManager.HasRetrievedUserInventoryItems()
|
||||
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 size_t gameAssemblySize();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -324,6 +324,25 @@ bool installPatches() {
|
|||
}
|
||||
g_installed = any;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue