- 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)
174 lines
8.1 KiB
C++
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
|