DevourUnlocker/src/il2cpp_resolver.cpp
MiTHRAL f1a75f8f71
Some checks failed
Build DevourUnlocker / windows-cross (push) Has been cancelled
Build DevourUnlocker / linux (push) Has been cancelled
Build DevourUnlocker / standalone (push) Has been cancelled
v2.0.4: fix standalone via collectables.json + pet hook + winhttp forwarding
- winhttp: runtime forwarding for all WinHttp* (fixes Wine crash on WinHttpGetIEProxyConfigForCurrentUser, was forwarding to winhttp_real.dll which doesn't exist)
- resolver: wait for IL2CPP domain + enumerate all images/classes for IsRobe/UpdateLocked*, dumpDiagnostics now shows FOUND for SurvivalLobbyController etc
- patches: handle MH_ERROR_ALREADY_INITIALIZED, background retry for 60s for UpdateLockedPets/WaitForPurchasedPetValidation (fixes pets not unlocking when lobby not yet loaded)
- standalone: unlock --all now patches Steam Cloud collectables.json for all 3 users (BarbedWires/Feathers/Tickets etc to superset) and updates remotecache.vdf sha/size/time so Steam doesn't overwrite; also auto-deploys winhttp.dll via injector
- diagnose: checks winhttp.dll, DevourUnlocker.log, WINEDLLOVERRIDES, collectables.json
- launch.sh: sets WINEDLLOVERRIDES and launches via Proton
2026-08-22 19:19:32 -04:00

320 lines
17 KiB
C++

#include "il2cpp_resolver.hpp"
#include <string>
#include <cstring>
#include <thread>
#include <chrono>
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <windows.h>
#include <psapi.h>
#else
#include <dlfcn.h>
#include <link.h>
#include <unistd.h>
#endif
namespace devour {
#ifdef _WIN32
static HMODULE g_gameAsm = nullptr;
uintptr_t Il2CppResolver::gameAssemblyBase() {
return reinterpret_cast<uintptr_t>(g_gameAsm);
}
size_t Il2CppResolver::gameAssemblySize() {
if (!g_gameAsm) return 0;
MODULEINFO mi{};
if (GetModuleInformation(GetCurrentProcess(), g_gameAsm, &mi, sizeof(mi)))
return mi.SizeOfImage;
return 0;
}
void* Il2CppResolver::getProc(const char* name) {
if (!g_gameAsm) return nullptr;
return reinterpret_cast<void*>(GetProcAddress(g_gameAsm, name));
}
bool Il2CppResolver::init() {
for (int i = 0; i < 600; ++i) { // 60s timeout
g_gameAsm = GetModuleHandleA("GameAssembly.dll");
if (g_gameAsm) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
static bool waitForDomain(int timeoutMs = 30000){
auto domain_get = reinterpret_cast<void*(*)()>(Il2CppResolver::getProc("il2cpp_domain_get"));
if(!domain_get) return false;
for(int i=0;i<timeoutMs/100;i++){
void* d = domain_get();
if(d) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
// Simple pattern scan
std::vector<void*> Il2CppResolver::scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size) {
std::vector<void*> out;
size_t patLen = strlen(mask);
for (size_t i = 0; i + patLen < size; ++i) {
bool ok = true;
for (size_t j = 0; j < patLen; ++j) {
if (mask[j] == 'x' && reinterpret_cast<char*>(start + i)[j] != pattern[j]) { ok = false; break; }
}
if (ok) out.push_back(reinterpret_cast<void*>(start + i));
}
return out;
}
ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* methodName) {
// Domain wait is now handled by installPatches once, not per-resolve to avoid 6*15s stalls
// If domain not ready, fallback to string xref quickly
auto domain_get_test = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
if (domain_get_test && !domain_get_test()) {
return resolveByStringXref(methodName);
}
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto il2cpp_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 class_get_method = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_method_from_name"));
auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto assembly_get_image = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
if (!il2cpp_domain_get || !klass_from_name || !class_get_method || !domain_get_assemblies || !assembly_get_image) {
return resolveByStringXref(methodName);
}
void* domain = il2cpp_domain_get();
if (domain && il2cpp_thread_attach) il2cpp_thread_attach(domain);
size_t nAssemblies = 0;
const void** assemblies = nullptr;
for(int w=0; w<100; w++){
assemblies = domain_get_assemblies(domain, &nAssemblies);
if(assemblies && nAssemblies>0) break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!assemblies || nAssemblies==0) return resolveByStringXref(methodName);
const char* candidates[] = {klassName, "NolanRankController", "LobbyController", "InventoryController", "GameController", "SurvivalLobbyController", "NolanRankController", nullptr};
for (size_t ai = 0; ai < nAssemblies; ++ai) {
void* image = assembly_get_image((void*)assemblies[ai]);
if (!image) continue;
for (int ci = 0; candidates[ci]; ++ci) {
void* klass = klass_from_name(image, "", candidates[ci]);
if (!klass) {
// Try with no namespace but also try with "Devour" namespace?
klass = klass_from_name(image, "Devour", candidates[ci]);
}
if (!klass) continue;
void* method = class_get_method(klass, methodName);
if (method) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(method) + 0x18);
if (ptr) return {methodName, ptr};
}
}
}
// Brute force: enumerate all classes in all images for methodName (UpdateLocked* etc live in unknown class)
auto image_get_class_count = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_image_get_class_count"));
auto image_get_class = reinterpret_cast<void*(*)(void*, int)>(getProc("il2cpp_image_get_class"));
if (image_get_class_count && image_get_class) {
for (size_t ai = 0; ai < nAssemblies; ++ai) {
void* image = assembly_get_image((void*)assemblies[ai]);
if (!image) continue;
int count = image_get_class_count(image);
for (int ci = 0; ci < count; ++ci) {
void* klass = image_get_class(image, ci);
if (!klass) continue;
void* method = class_get_method(klass, methodName);
if (method) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(method) + 0x18);
if (ptr) return {methodName, ptr};
}
}
}
}
return resolveByStringXref(methodName);
}
ResolvedMethod Il2CppResolver::resolveByStringXref(const char* str, int occurrence) {
uintptr_t base = gameAssemblyBase();
size_t sz = gameAssemblySize();
if (!base || !sz) return {str, nullptr};
// Fast find via memchr + memcmp — don't require null terminator (IsRobeUnlocked appears inside longer log string)
size_t slen = strlen(str);
uintptr_t strAddr = 0;
const char* start = reinterpret_cast<const char*>(base);
const char* end = start + sz - slen - 1;
const char* p = start;
while (p < end) {
p = (const char*)memchr(p, str[0], end - p);
if (!p) break;
if (memcmp(p, str, slen)==0) {
if (occurrence--==0) { strAddr = reinterpret_cast<uintptr_t>(p); break; }
}
p++;
}
if (!strAddr) return {str, nullptr};
// Scan for 48 8D 0D ? ? ? ? (lea rcx, [rip+disp]) referencing strAddr
// Simplified: look for 4-byte disp that resolves to strAddr
for (size_t i = 0; i + 7 < sz; ++i) {
uint8_t* p = reinterpret_cast<uint8_t*>(base + i);
if (p[0] == 0x48 && p[1] == 0x8D && p[2] == 0x0D) {
int32_t disp = *reinterpret_cast<int32_t*>(p + 3);
uintptr_t target = base + i + 7 + disp;
if (target == strAddr) {
// Found xref — function start is previous 0xCC or 0x48 89 5C ? ... search back
// Heuristic: walk back up to 0x200 for function prologue 40 53 / 48 83 EC
for (int back = 0; back < 0x300; ++back) {
uint8_t* q = p - back;
if (q < reinterpret_cast<uint8_t*>(base)) break;
if (q[0] == 0x40 && q[1] == 0x53) return {str, q}; // push rbx
if (q[0] == 0x48 && q[1] == 0x83 && q[2] == 0xEC) return {str, q};
}
return {str, p}; // fallback
}
}
}
return {str, nullptr};
}
ResolvedMethod Il2CppResolver::isRobeUnlocked() { return resolve("NolanRankController", "IsRobeUnlocked"); }
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRankController", "IsCharacterUnlocked"); }
ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("NolanRankController", "HasOutfit"); }
ResolvedMethod Il2CppResolver::hasPerk() { return resolve("NolanRankController", "HasPerk"); }
ResolvedMethod Il2CppResolver::hasPet() { return resolve("NolanRankController", "HasPet"); }
ResolvedMethod Il2CppResolver::updateLockedPets() { return resolve("SurvivalLobbyController", "UpdateLockedPets"); }
ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("SurvivalLobbyController", "UpdateLockedPerks"); }
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("SurvivalLobbyController", "UpdateLockedFlashlights"); }
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("SurvivalLobbyController", "UpdateLockedEmotes"); }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("SurvivalLobbyController", "UpdateLockedOutfits"); }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("SurvivalLobbyController", "UpdateLockedRobes"); }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return resolve("SurvivalLobbyController", "WaitForPurchasedPetValidation"); }
void Il2CppResolver::dumpDiagnostics(){
#ifdef _WIN32
// Also log via OutputDebugString for Wine debug
OutputDebugStringA("[Diag] dumpDiagnostics enter\n");
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 << "[Diag] dumpDiagnostics ENTER\n"; f.flush();
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto assembly_get_image = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
auto image_get_class_count = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_image_get_class_count"));
auto image_get_class = reinterpret_cast<void*(*)(void*, int)>(getProc("il2cpp_image_get_class"));
auto class_get_name = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_name"));
auto class_get_namespace = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_namespace"));
auto class_get_method_count = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_class_num_methods"));
auto class_get_methods = reinterpret_cast<void**(*)(void*, void**)>(getProc("il2cpp_class_get_methods"));
auto method_get_name = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(!il2cpp_domain_get || !domain_get_assemblies || !assembly_get_image || !image_get_class_count || !image_get_class) {
f << "[Diag] missing IL2CPP exports for dump\n"; return;
}
void* domain = il2cpp_domain_get();
if(!domain){ f << "[Diag] domain null\n"; return; }
size_t nAssemblies=0; const void** assemblies = domain_get_assemblies(domain, &nAssemblies);
f << "[Diag] assemblies=" << nAssemblies << "\n";
for(size_t ai=0; ai<nAssemblies && ai<32; ++ai){
void* image = assembly_get_image((void*)assemblies[ai]);
if(!image) continue;
auto image_get_name = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_image_get_name"));
const char* iname = image_get_name ? image_get_name(image) : "?";
int cnt = image_get_class_count(image);
f << "[Diag] image " << iname << " classes=" << cnt << " ptr=" << image << "\n";
// Log first 10 class names of this image as sample
for(int ci=0; ci<cnt && ci<10; ++ci){
void* klass = image_get_class(image, ci);
if(!klass) continue;
const char* kname = class_get_name ? class_get_name(klass) : "?";
const char* ns = class_get_namespace ? class_get_namespace(klass) : "";
f << " sample class " << ns << "." << kname << "\n";
}
// Now search for UpdateLocked etc in all classes of this image — also dump Nolan/Survival specifically
for(int ci=0; ci<cnt && ci<2000; ++ci){
void* klass = image_get_class(image, ci);
if(!klass) continue;
const char* kname = class_get_name ? class_get_name(klass) : "?";
std::string kn=kname?kname:"";
bool isTargetKlass = kn=="NolanRankController" || kn=="SurvivalLobbyController" || kn=="LobbyController";
auto class_get_method = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_method_from_name"));
if(isTargetKlass && class_get_method){
f << " DIAG target klass " << kname << " in " << iname << "\n";
const char* targets[]={"UpdateLockedPets","UpdateLockedPerks","IsRobeUnlocked","IsCharacterUnlocked","HasPerk","HasOutfit","UpdateLockedRobes",nullptr};
for(int ti=0; targets[ti]; ++ti){
void* m = class_get_method(klass, targets[ti]);
f << " try " << targets[ti] << " -> " << m;
if(m){
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(m) + 0x18);
f << " ptr=" << ptr;
auto method_get_name2 = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(method_get_name2) f << " name=" << method_get_name2(m);
}
f << "\n";
}
// Dump all methods of this klass
auto class_get_methods2 = reinterpret_cast<const void**(*)(void*, void**)>(getProc("il2cpp_class_get_methods"));
auto method_get_name2 = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(class_get_methods2 && method_get_name2){
void* iter2=nullptr;
int mc2=0;
f << " all methods of " << kname << ":\n";
while(true){
const void** methods = (const void**)class_get_methods2(klass, &iter2);
if(!methods || !*methods) break;
const char* mn = method_get_name2((void*)*methods);
f << " " << (mn?mn:"<null>") << "\n";
if(++mc2>100) break;
}
}
}
if(class_get_method){
const char* targets[]={"UpdateLockedPets","UpdateLockedPerks","IsRobeUnlocked","IsCharacterUnlocked","HasPerk","HasOutfit",nullptr};
for(int ti=0; targets[ti]; ++ti){
void* m = class_get_method(klass, targets[ti]);
if(m) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(m) + 0x18);
f << " FOUND " << kname << "." << targets[ti] << " @ " << m << " -> " << ptr << " (image " << iname << ")\n";
}
}
}
}
}
f << "[Diag] dump done\n";
}catch(...){}
#endif
}
#else // Linux stubs
uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; }
size_t Il2CppResolver::gameAssemblySize() { return 0; }
void* Il2CppResolver::getProc(const char*) { return nullptr; }
bool Il2CppResolver::init() { return false; }
std::vector<void*> Il2CppResolver::scanPattern(const char*, const char*, uintptr_t, size_t) { return {}; }
ResolvedMethod Il2CppResolver::resolve(const char*, const char* m) { return {m, nullptr}; }
ResolvedMethod Il2CppResolver::resolveByStringXref(const char* s, int) { return {s, nullptr}; }
ResolvedMethod Il2CppResolver::isRobeUnlocked() { return {"IsRobeUnlocked", nullptr}; }
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return {"IsCharacterUnlocked", nullptr}; }
ResolvedMethod Il2CppResolver::hasOutfit() { return {"HasOutfit", nullptr}; }
ResolvedMethod Il2CppResolver::hasPerk() { return {"HasPerk", nullptr}; }
ResolvedMethod Il2CppResolver::hasPet() { return {"HasPet", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedPets() { return {"UpdateLockedPets", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedPerks() { return {"UpdateLockedPerks", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return {"UpdateLockedFlashlights", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return {"UpdateLockedEmotes", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return {"UpdateLockedOutfits", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return {"UpdateLockedRobes", nullptr}; }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return {"WaitForPurchasedPetValidation", nullptr}; }
#endif
} // namespace devour