DevourUnlocker/src/il2cpp_resolver.cpp
MiTHRAL 4af18a2f75
Some checks are pending
Build DevourUnlocker / windows-cross (push) Waiting to run
Build DevourUnlocker / linux (push) Waiting to run
Build DevourUnlocker / standalone (push) Waiting to run
v2.0.1: expand to all item types + fix pets + linux CI
- Config: add allPets (Crow/Pig/Goat/Rat/Spider/Cat/Pug), allPerks (29 inc Acceleration/Quartermaster etc), flashlights/emotes/outfits; unlockOutfits global
- Resolver: resolve HasPerk/HasPet/UpdateLocked* for Pets/Perks/Flashlights/Emotes/Outfits/Robes
- Patches: hook IsRobe/IsCharacter + HasPerk/HasOutfit + UpdateLockedPets/Perks/Flashlights/Emotes/Outfits/Robes bypass — fixes pets not unlocking
- GUI: 7 tabs (Robes/Chars/Perks/Pets/Flashlights/Emotes/Outfits), Unlock All covers all categories
- Standalone: --perks/--pets/--all now covers all categories, petOverrides etc, list shows 30 robes+9 chars+29 perks+7 pets+8 flashlights+12 emotes+8 outfits
- GUI standalone: 7 notebooks with per-category checkboxes
- BepInEx: add UpdateLocked* prefix patches + HasPerk/HasOutfit, GUI tabs for all
- CI: switch windows-latest to ubuntu-latest mingw cross (git.mithraic.cloud has only linux runner, windows jobs were stuck Waiting)
2026-08-21 21:15:58 -04:00

174 lines
8.1 KiB
C++

#include "il2cpp_resolver.hpp"
#include <string>
#include <thread>
#include <chrono>
#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;
}
// 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) {
// Use il2cpp exports to find method without offset
using domain_get_t = void*(*)();
using thread_attach_t = void*(*)(void*);
using assembly_foreach_t = void(*)(void(*)(void*, void*), void*);
// We call il2cpp_resolve_icall-like path: try to locate via il2cpp_class_from_name
auto il2cpp_domain_get = reinterpret_cast<domain_get_t>(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"));
if (!il2cpp_domain_get || !klass_from_name || !class_get_method) {
return resolveByStringXref(methodName);
}
void* domain = il2cpp_domain_get();
if (domain && il2cpp_thread_attach) il2cpp_thread_attach(domain);
// NolanRankController lives in Assembly-CSharp — hosts IsRobeUnlocked etc.
// Try common owner classes: "NolanRankController", "LobbyController", "InventoryController"
const char* candidates[] = {"NolanRankController", "LobbyController", "InventoryController", "GameController", nullptr};
for (int ci = 0; candidates[ci]; ++ci) {
void* klass = klass_from_name(nullptr, "Assembly-CSharp.dll", candidates[ci]);
// Fallback: il2cpp_class_from_name expects (image, ns, name) — try image-less variant search
if (!klass) {
// Try global search via xref fallback
continue;
}
void* method = class_get_method(klass, methodName);
if (method) {
// MethodInfo layout: methodPointer at offset 0x18 on x64 (stable across il2cpp)
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) {
// Scan for string pointer -> xref to find caller. Minimal heuristic:
// Search for ansi string bytes inside GameAssembly rdata, then scan .text for lea rcx, [addr]
uintptr_t base = gameAssemblyBase();
size_t sz = gameAssemblySize();
if (!base || !sz) return {str, nullptr};
// Find string bytes
size_t slen = strlen(str);
uintptr_t strAddr = 0;
for (size_t i = 0; i + slen < sz; ++i) {
if (memcmp(reinterpret_cast<void*>(base + i), str, slen) == 0) {
// Check null terminator
if (*reinterpret_cast<char*>(base + i + slen) == '\0') {
if (occurrence-- == 0) { strAddr = base + i; break; }
}
}
}
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("LobbyController", "UpdateLockedPets"); }
ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("LobbyController", "UpdateLockedPerks"); }
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("LobbyController", "UpdateLockedFlashlights"); }
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("LobbyController", "UpdateLockedEmotes"); }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("LobbyController", "UpdateLockedOutfits"); }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("LobbyController", "UpdateLockedRobes"); }
#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}; }
#endif
} // namespace devour