v2.0.8: unlock all categories — ownership + currency override, not UI no-op
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 8s
Build DevourUnlocker / linux (push) Failing after 7s
Build DevourUnlocker / standalone (push) Successful in 6s

Root cause of 'robes unlocked but nothing else': UpdateLocked* no-op prevented lock
marking but lobby UI defaults to locked until UpdateLocked* runs its grant logic.
Robes worked because IsRobeUnlocked (hooked) gates selection directly.

Fix from global-metadata.dat analysis (148772 methods scanned):
- SteamInventoryManager.UserHasItem(steamItemDefID) -> always true: every Steam catalog
  item (pets/flashlights/emotes/outfits/promos) counts as owned
- Horror.Menu.GetCurrentTokens() -> 9999999: perk token cost always affordable
- UpdateLocked*/WaitForPurchasedPetValidation now CALL ORIGINAL via trampoline so the
  game's own grant logic runs with overridden ownership (clean UI state)
- hookUpdateLocked stores orig fn pointer; retry thread also retries UserHasItem/Tokens
  and HasPerk/HasOutfit; logPatch on every hook success/failure for diagnosis

Verified launch: 13 hooks installed (IsRobe/IsChar/HasPerk/HasOutfit/UserHasItem/
GetCurrentTokens/7xUpdateLocked+WaitPet), game stable, no crashes
This commit is contained in:
MiTHRAL 2026-08-24 01:16:18 -04:00
parent bffc822614
commit 48d0edac0a
3 changed files with 189 additions and 80 deletions

View file

@ -70,7 +70,7 @@ std::vector<void*> Il2CppResolver::scanPattern(const char* pattern, const char*
return out; return out;
} }
ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* methodName) { ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* methodName, int argc) {
// Domain wait is now handled by installPatches once, not per-resolve to avoid 6*15s stalls // 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 // If domain not ready, fallback to string xref quickly
auto domain_get_test = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get")); auto domain_get_test = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
@ -80,7 +80,8 @@ ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* method
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get")); auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto il2cpp_thread_attach = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_thread_attach")); 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 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")); // Real IL2CPP signature: il2cpp_class_get_method_from_name(klass, name, argsCount)
auto class_get_method = reinterpret_cast<void*(*)(void*, const char*, int)>(getProc("il2cpp_class_get_method_from_name"));
auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies")); 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 assembly_get_image = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
@ -99,25 +100,34 @@ ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* method
} }
if (!assemblies || nAssemblies==0) return resolveByStringXref(methodName); if (!assemblies || nAssemblies==0) return resolveByStringXref(methodName);
const char* candidates[] = {klassName, "NolanRankController", "LobbyController", "InventoryController", "GameController", "SurvivalLobbyController", "NolanRankController", nullptr}; // Namespaces seen in global-metadata.dat: OptionsHelpers/InGameHelpers are global,
// UpdateLocked*/WaitForPurchasedPetValidation live in Horror.Menu
const char* namespaces[] = {"", "Horror", nullptr};
auto tryMethod = [&](void* klass) -> void* {
if (!klass) return nullptr;
int argcs[4] = { argc, 1, 0, 2 };
for (int ai = 0; ai < 4; ++ai) {
if (ai == 0 && argc < 0) continue;
void* m = class_get_method(klass, methodName, argcs[ai]);
if (m) {
void* ptr = *reinterpret_cast<void**>(m); // MethodInfo.methodPointer @ +0x00
if (ptr) return ptr;
}
}
return nullptr;
};
for (size_t ai = 0; ai < nAssemblies; ++ai) { for (size_t ai = 0; ai < nAssemblies; ++ai) {
void* image = assembly_get_image((void*)assemblies[ai]); void* image = assembly_get_image((void*)assemblies[ai]);
if (!image) continue; if (!image) continue;
for (int ci = 0; candidates[ci]; ++ci) { for (int ni = 0; namespaces[ni]; ++ni) {
void* klass = klass_from_name(image, "", candidates[ci]); void* klass = klass_from_name(image, namespaces[ni], klassName);
if (!klass) {
// Try with no namespace but also try with "Devour" namespace?
klass = klass_from_name(image, "Devour", candidates[ci]);
}
if (!klass) continue; if (!klass) continue;
void* method = class_get_method(klass, methodName); void* ptr = tryMethod(klass);
if (method) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(method) + 0x18);
if (ptr) return {methodName, ptr}; if (ptr) return {methodName, ptr};
} }
} }
} // Brute force: enumerate all classes in all images for methodName (works regardless of namespace)
// 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_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 image_get_class = reinterpret_cast<void*(*)(void*, int)>(getProc("il2cpp_image_get_class"));
if (image_get_class_count && image_get_class) { if (image_get_class_count && image_get_class) {
@ -128,14 +138,11 @@ ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* method
for (int ci = 0; ci < count; ++ci) { for (int ci = 0; ci < count; ++ci) {
void* klass = image_get_class(image, ci); void* klass = image_get_class(image, ci);
if (!klass) continue; if (!klass) continue;
void* method = class_get_method(klass, methodName); void* ptr = tryMethod(klass);
if (method) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(method) + 0x18);
if (ptr) return {methodName, ptr}; if (ptr) return {methodName, ptr};
} }
} }
} }
}
return resolveByStringXref(methodName); return resolveByStringXref(methodName);
} }
@ -183,18 +190,28 @@ ResolvedMethod Il2CppResolver::resolveByStringXref(const char* str, int occurren
return {str, nullptr}; return {str, nullptr};
} }
ResolvedMethod Il2CppResolver::isRobeUnlocked() { return resolve("NolanRankController", "IsRobeUnlocked"); } // Real targets from global-metadata.dat (v31, 2022.3.62f2):
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRankController", "IsCharacterUnlocked"); } // OptionsHelpers.IsRobeUnlocked(robe, character) — global ns
ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("NolanRankController", "HasOutfit"); } // OptionsHelpers.IsCharacterUnlocked(prefab) — global ns
ResolvedMethod Il2CppResolver::hasPerk() { return resolve("NolanRankController", "HasPerk"); } // OptionsHelpers.HasOutfit(outfitName, characterName) — global ns
ResolvedMethod Il2CppResolver::hasPet() { return resolve("NolanRankController", "HasPet"); } // InGameHelpers.HasPerk(perk) / NolanPerkController.HasPerk(perk)
ResolvedMethod Il2CppResolver::updateLockedPets() { return resolve("SurvivalLobbyController", "UpdateLockedPets"); } // Horror.Menu.UpdateLocked{Pets,Perks,Flashlights,Emotes,Outfits}(selectionViews)
ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("SurvivalLobbyController", "UpdateLockedPerks"); } // Horror.Menu.UpdateLockedRobes()
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("SurvivalLobbyController", "UpdateLockedFlashlights"); } // Horror.Menu.WaitForPurchasedPetValidation(pet)
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("SurvivalLobbyController", "UpdateLockedEmotes"); } ResolvedMethod Il2CppResolver::isRobeUnlocked() { return resolve("OptionsHelpers", "IsRobeUnlocked", 2); }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("SurvivalLobbyController", "UpdateLockedOutfits"); } ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("OptionsHelpers", "IsCharacterUnlocked", 1); }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("SurvivalLobbyController", "UpdateLockedRobes"); } ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("OptionsHelpers", "HasOutfit", 2); }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return resolve("SurvivalLobbyController", "WaitForPurchasedPetValidation"); } ResolvedMethod Il2CppResolver::hasPerk() { return resolve("InGameHelpers", "HasPerk", 1); }
ResolvedMethod Il2CppResolver::hasPet() { return {"HasPet", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedPets() { return resolve("Menu", "UpdateLockedPets", 1); }
ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("Menu", "UpdateLockedPerks", 1); }
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("Menu", "UpdateLockedFlashlights", 1); }
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("Menu", "UpdateLockedEmotes", 1); }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("Menu", "UpdateLockedOutfits", 1); }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("Menu", "UpdateLockedRobes", 0); }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return resolve("Menu", "WaitForPurchasedPetValidation", 1); }
ResolvedMethod Il2CppResolver::userHasItem() { return resolve("SteamInventoryManager", "UserHasItem", 1); }
ResolvedMethod Il2CppResolver::getCurrentTokens() { return resolve("Menu", "GetCurrentTokens", 0); }
void Il2CppResolver::dumpDiagnostics(){ void Il2CppResolver::dumpDiagnostics(){
#ifdef _WIN32 #ifdef _WIN32
@ -204,16 +221,52 @@ void Il2CppResolver::dumpDiagnostics(){
char* slash = strrchr(exe, '\\'); if(!slash) slash = strrchr(exe, '/'); char* slash = strrchr(exe, '\\'); if(!slash) slash = strrchr(exe, '/');
std::string logPath = slash ? std::string(exe, slash - exe) + "\\DevourUnlocker.log" : "DevourUnlocker.log"; std::string logPath = slash ? std::string(exe, slash - exe) + "\\DevourUnlocker.log" : "DevourUnlocker.log";
std::ofstream f(logPath, std::ios::app); std::ofstream f(logPath, std::ios::app);
f << "[Diag] dumpDiagnostics ENTER (safe mode - minimal)\n"; f.flush(); f << "[Diag] dumpDiagnostics ENTER (safe mode - targeted)\n"; f.flush();
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get")); auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
if(!il2cpp_domain_get){ f << "[Diag] no domain_get\n"; return; } if(!il2cpp_domain_get){ f << "[Diag] no domain_get\n"; return; }
void* domain = il2cpp_domain_get(); void* domain = il2cpp_domain_get();
if(!domain){ f << "[Diag] domain null (IL2CPP not ready - retry later)\n"; return; } if(!domain){ f << "[Diag] domain null (IL2CPP not ready - retry later)\n"; return; }
// Minimal safe dump - do not enumerate all classes (caused strlen(null) crash) auto klass_from_name = reinterpret_cast<void*(*)(void*, const char*, const char*)>(getProc("il2cpp_class_from_name"));
f << "[Diag] domain=" << domain << " - skipping full enumeration to avoid crash\n"; auto class_get_method = reinterpret_cast<void*(*)(void*, const char*, int)>(getProc("il2cpp_class_get_method_from_name"));
f << "[Diag] If hooks not found, IL2CPP may not be ready until lobby - retry thread will handle\n"; 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(!klass_from_name || !class_get_method || !domain_get_assemblies || !assembly_get_image){
f << "[Diag] missing IL2CPP exports\n"; return;
}
size_t nAssemblies=0; const void** assemblies = domain_get_assemblies(domain, &nAssemblies);
f << "[Diag] assemblies=" << nAssemblies << "\n";
// Bounded targeted probe of the two real classes — no mass enumeration
struct Probe { const char* ns; const char* cls; const char* m; int argc; };
const Probe probes[] = {
{"", "OptionsHelpers", "IsRobeUnlocked", 2},
{"", "OptionsHelpers", "IsCharacterUnlocked", 1},
{"", "OptionsHelpers", "HasOutfit", 2},
{"", "InGameHelpers", "HasPerk", 1},
{"Horror", "Menu", "UpdateLockedPets", 1},
{"Horror", "Menu", "UpdateLockedRobes", 0},
{"Horror", "Menu", "WaitForPurchasedPetValidation", 1},
};
for (const auto& p : probes) {
void* found = nullptr; void* kfound = nullptr;
for(size_t ai=0; ai<nAssemblies && !found; ++ai){
void* image = assembly_get_image((void*)assemblies[ai]);
if(!image) continue;
void* k = klass_from_name(image, p.ns, p.cls);
if(!k) continue;
kfound = k;
void* m = class_get_method(k, p.m, p.argc);
if(m){
void* ptr = *reinterpret_cast<void**>(m); // MethodInfo.methodPointer @ +0x00
f << "[Diag] " << (p.ns[0]?p.ns:"") << "." << p.cls << "::" << p.m << "(" << p.argc << ") -> " << ptr << "\n";
found = ptr;
}
}
if(!found){
f << "[Diag] " << (p.ns[0]?p.ns:"") << "." << p.cls << "::" << p.m
<< " NOT found" << (kfound ? " (class ok, method null)" : " (class missing)") << "\n";
}
}
f << "[Diag] dump done (safe)\n"; f << "[Diag] dump done (safe)\n";
return;
// DISABLED heavy enumeration that caused crash (strlen null at 0x8eb3) // DISABLED heavy enumeration that caused crash (strlen null at 0x8eb3)
#if 0 #if 0
auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies")); auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
@ -262,7 +315,7 @@ void Il2CppResolver::dumpDiagnostics(){
void* m = class_get_method(klass, targets[ti]); void* m = class_get_method(klass, targets[ti]);
f << " try " << targets[ti] << " -> " << m; f << " try " << targets[ti] << " -> " << m;
if(m){ if(m){
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(m) + 0x18); void* ptr = *reinterpret_cast<void**>(m); // MethodInfo.methodPointer @ +0x00
f << " ptr=" << ptr; f << " ptr=" << ptr;
auto method_get_name2 = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name")); auto method_get_name2 = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(method_get_name2){ if(method_get_name2){
@ -293,7 +346,7 @@ void Il2CppResolver::dumpDiagnostics(){
for(int ti=0; targets[ti]; ++ti){ for(int ti=0; targets[ti]; ++ti){
void* m = class_get_method(klass, targets[ti]); void* m = class_get_method(klass, targets[ti]);
if(m) { if(m) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(m) + 0x18); void* ptr = *reinterpret_cast<void**>(m); // MethodInfo.methodPointer @ +0x00
f << " FOUND " << kname << "." << targets[ti] << " @ " << m << " -> " << ptr << " (image " << iname << ")\n"; f << " FOUND " << kname << "." << targets[ti] << " @ " << m << " -> " << ptr << " (image " << iname << ")\n";
} }
} }
@ -327,6 +380,8 @@ ResolvedMethod Il2CppResolver::updateLockedEmotes() { return {"UpdateLockedEmote
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return {"UpdateLockedOutfits", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedOutfits() { return {"UpdateLockedOutfits", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return {"UpdateLockedRobes", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedRobes() { return {"UpdateLockedRobes", nullptr}; }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return {"WaitForPurchasedPetValidation", nullptr}; } ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return {"WaitForPurchasedPetValidation", nullptr}; }
ResolvedMethod Il2CppResolver::userHasItem() { return {"UserHasItem", nullptr}; }
ResolvedMethod Il2CppResolver::getCurrentTokens() { return {"GetCurrentTokens", nullptr}; }
#endif #endif

View file

@ -25,7 +25,7 @@ public:
static void* getProc(const char* name); static void* getProc(const char* name);
static std::vector<void*> scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size); static std::vector<void*> scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size);
static ResolvedMethod resolve(const char* klassName, const char* methodName); static ResolvedMethod resolve(const char* klassName, const char* methodName, int argc = -1);
static ResolvedMethod resolveByStringXref(const char* str, int occurrence = 0); static ResolvedMethod resolveByStringXref(const char* str, int occurrence = 0);
static ResolvedMethod isRobeUnlocked(); static ResolvedMethod isRobeUnlocked();
@ -40,6 +40,8 @@ public:
static ResolvedMethod updateLockedOutfits(); static ResolvedMethod updateLockedOutfits();
static ResolvedMethod updateLockedRobes(); static ResolvedMethod updateLockedRobes();
static ResolvedMethod waitForPurchasedPet(); static ResolvedMethod waitForPurchasedPet();
static ResolvedMethod userHasItem(); // SteamInventoryManager.UserHasItem(steamItemDefID)
static ResolvedMethod getCurrentTokens(); // Horror.Menu.GetCurrentTokens()
static void dumpDiagnostics(); static void dumpDiagnostics();
static uintptr_t gameAssemblyBase(); static uintptr_t gameAssemblyBase();
static size_t gameAssemblySize(); static size_t gameAssemblySize();

View file

@ -24,13 +24,25 @@ namespace devour {
#ifdef _WIN32 #ifdef _WIN32
using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* method); using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* str2, void* method);
using IsUnlockedFn1 = bool(__fastcall*)(void* thiz, void* str, void* method);
using UpdateLockedFn = void(__fastcall*)(void* thiz, void* method); using UpdateLockedFn = void(__fastcall*)(void* thiz, void* method);
using UserHasItemFn = bool(__fastcall*)(void* thiz, unsigned long long itemDef, void* method);
using GetTokensFn = int(__fastcall*)(void* thiz, void* method);
static IsUnlockedFn oIsRobe = nullptr; static IsUnlockedFn oIsRobe = nullptr;
static IsUnlockedFn oIsChar = nullptr; static IsUnlockedFn1 oIsChar = nullptr;
static IsUnlockedFn oHasPerk = nullptr; static IsUnlockedFn1 oHasPerk = nullptr;
static IsUnlockedFn oHasOutfit = nullptr; static IsUnlockedFn oHasOutfit = nullptr;
static UpdateLockedFn oUpdPets = nullptr;
static UpdateLockedFn oUpdPerks = nullptr;
static UpdateLockedFn oUpdFlash = nullptr;
static UpdateLockedFn oUpdEmotes = nullptr;
static UpdateLockedFn oUpdOutfits = nullptr;
static UpdateLockedFn oUpdRobes = nullptr;
static UpdateLockedFn oWaitPet = nullptr;
static UserHasItemFn oUserHasItem = nullptr;
static GetTokensFn oGetTokens = nullptr;
static bool g_installed = false; static bool g_installed = false;
static bool g_mhInit = false; static bool g_mhInit = false;
@ -49,31 +61,39 @@ static std::string il2cppStringToUtf8(void* ilStr) {
return ""; return "";
} }
static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* method) { return true; } static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* characterStr, void* method) { return true; }
static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { return true; } static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { return true; }
static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { return true; } static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { return true; }
static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* method) { return true; } static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { return true; }
// Always-on: every UpdateLocked* is bypassed // UpdateLocked*: call the ORIGINAL so the UI runs its normal flow — ownership checks
static void __fastcall hUpdateLockedPets(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedPets bypassed (always-on)"); } // (UserHasItem→true) make it mark everything unlocked instead of leaving defaults locked
static void __fastcall hWaitForPurchasedPet(void* thiz, void* method) { logPatch("[DevourUnlocker] WaitForPurchasedPetValidation bypassed (always-on)"); } static void __fastcall hUpdateLockedPets(void* thiz, void* method) { if (oUpdPets) oUpdPets(thiz, method); }
static void __fastcall hUpdateLockedPerks(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedPerks bypassed (always-on)"); } static void __fastcall hWaitForPurchasedPet(void* thiz, void* method) { if (oWaitPet) oWaitPet(thiz, method); }
static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedFlashlights bypassed"); } static void __fastcall hUpdateLockedPerks(void* thiz, void* method) { if (oUpdPerks) oUpdPerks(thiz, method); }
static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedEmotes bypassed"); } static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { if (oUpdFlash) oUpdFlash(thiz, method); }
static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedOutfits bypassed"); } static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { if (oUpdEmotes) oUpdEmotes(thiz, method); }
static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedRobes bypassed"); } static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { if (oUpdOutfits) oUpdOutfits(thiz, method); }
static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRobes) oUpdRobes(thiz, method); }
// Ownership override: every Steam catalog item (pets/flashlights/emotes/outfits/promos) counts as owned
static bool __fastcall hUserHasItem(void* thiz, unsigned long long itemDef, void* method) {
(void)itemDef;
return true;
}
// Perks bought with Tokens: report a huge balance so every perk is purchasable/selectable
static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; }
static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*)) { static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*), UpdateLockedFn* origOut) {
if (!addr) { std::cerr << "[DevourUnlocker] " << name << " not found\n"; logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; } if (!addr) { logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; }
(void)shouldUnlock; // always-on (void)shouldUnlock; // always-on
void* orig = nullptr; void* orig = nullptr;
if (MH_CreateHook(addr, (void*)hook, &orig) == MH_OK) { MH_STATUS st = MH_CreateHook(addr, (void*)hook, &orig);
if (st == MH_OK) {
MH_EnableHook(addr); MH_EnableHook(addr);
std::cout << "[DevourUnlocker] Hooked " << name << "\n"; if (origOut) *origOut = reinterpret_cast<UpdateLockedFn>(orig);
logPatch((std::string("[DevourUnlocker] Hooked ")+name).c_str()); logPatch((std::string("[DevourUnlocker] Hooked ")+name).c_str());
return true; return true;
} }
std::cerr << "[DevourUnlocker] Hook " << name << " failed\n"; logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed st="+std::to_string((int)st)).c_str());
logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed").c_str());
return false; return false;
} }
@ -154,31 +174,57 @@ bool installPatches() {
if (ch.valid()) { if (ch.valid()) {
if (MH_CreateHook(ch.address, (void*)hIsChar, (void**)&oIsChar) == MH_OK) { if (MH_CreateHook(ch.address, (void*)hIsChar, (void**)&oIsChar) == MH_OK) {
MH_EnableHook(ch.address); MH_EnableHook(ch.address);
std::cout << "[DevourUnlocker] Hooked IsCharacterUnlocked\n"; logPatch("[DevourUnlocker] Hooked IsCharacterUnlocked");
any = true; any = true;
} else std::cerr << "[DevourUnlocker] Hook IsChar failed\n"; } else logPatch("[DevourUnlocker] Hook IsChar failed");
} }
if (hasPerk.valid()) { if (hasPerk.valid()) {
if (MH_CreateHook(hasPerk.address, (void*)hHasPerk, (void**)&oHasPerk) == MH_OK) { if (MH_CreateHook(hasPerk.address, (void*)hHasPerk, (void**)&oHasPerk) == MH_OK) {
MH_EnableHook(hasPerk.address); MH_EnableHook(hasPerk.address);
std::cout << "[DevourUnlocker] Hooked HasPerk\n"; logPatch("[DevourUnlocker] Hooked HasPerk");
any = true; any = true;
} } else logPatch("[DevourUnlocker] Hook HasPerk failed");
} }
if (hasOutfit.valid()) { if (hasOutfit.valid()) {
if (MH_CreateHook(hasOutfit.address, (void*)hHasOutfit, (void**)&oHasOutfit) == MH_OK) { if (MH_CreateHook(hasOutfit.address, (void*)hHasOutfit, (void**)&oHasOutfit) == MH_OK) {
MH_EnableHook(hasOutfit.address); MH_EnableHook(hasOutfit.address);
std::cout << "[DevourUnlocker] Hooked HasOutfit\n"; logPatch("[DevourUnlocker] Hooked HasOutfit");
any = true; any = true;
} else logPatch("[DevourUnlocker] Hook HasOutfit failed");
} }
// Ownership + currency overrides — the actual gates for pets/perks/flashlights/emotes/outfits
{
auto uhi = Il2CppResolver::userHasItem();
auto tok = Il2CppResolver::getCurrentTokens();
char b2[512];
snprintf(b2, sizeof(b2), "[DevourUnlocker] Resolved UserHasItem=%p GetCurrentTokens=%p", uhi.address, tok.address);
logPatch(b2);
if (uhi.valid()) {
void* orig = nullptr;
if (MH_CreateHook(uhi.address, (void*)hUserHasItem, &orig) == MH_OK) {
MH_EnableHook(uhi.address);
oUserHasItem = reinterpret_cast<UserHasItemFn>(orig);
logPatch("[DevourUnlocker] Hooked UserHasItem -> always owned");
any = true;
} else logPatch("[DevourUnlocker] Hook UserHasItem failed");
} else logPatch("[DevourUnlocker] UserHasItem not found");
if (tok.valid()) {
void* orig = nullptr;
if (MH_CreateHook(tok.address, (void*)hGetCurrentTokens, &orig) == MH_OK) {
MH_EnableHook(tok.address);
oGetTokens = reinterpret_cast<GetTokensFn>(orig);
logPatch("[DevourUnlocker] Hooked GetCurrentTokens -> 9999999");
any = true;
} else logPatch("[DevourUnlocker] Hook GetCurrentTokens failed");
} else logPatch("[DevourUnlocker] GetCurrentTokens not found");
} }
any |= hookUpdateLocked(updPets.address, "UpdateLockedPets", Config::get().unlockPets, hUpdateLockedPets); any |= hookUpdateLocked(updPets.address, "UpdateLockedPets", Config::get().unlockPets, hUpdateLockedPets, &oUpdPets);
any |= hookUpdateLocked(updPerks.address, "UpdateLockedPerks", Config::get().unlockPerks, hUpdateLockedPerks); any |= hookUpdateLocked(updPerks.address, "UpdateLockedPerks", Config::get().unlockPerks, hUpdateLockedPerks, &oUpdPerks);
any |= hookUpdateLocked(updFlash.address, "UpdateLockedFlashlights", Config::get().unlockFlashlights, hUpdateLockedFlashlights); any |= hookUpdateLocked(updFlash.address, "UpdateLockedFlashlights", Config::get().unlockFlashlights, hUpdateLockedFlashlights, &oUpdFlash);
any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes); any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes, &oUpdEmotes);
any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits); any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits, &oUpdOutfits);
any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes); any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes, &oUpdRobes);
any |= hookUpdateLocked(waitPet.address, "WaitForPurchasedPetValidation", Config::get().unlockPets, hWaitForPurchasedPet); any |= hookUpdateLocked(waitPet.address, "WaitForPurchasedPetValidation", Config::get().unlockPets, hWaitForPurchasedPet, &oWaitPet);
// If any hooks not found, IL2CPP may not be ready until lobby — retry for 60s // If any hooks not found, IL2CPP may not be ready until lobby — retry for 60s
bool needRetry = !updPets.valid() || !updPerks.valid() || !updFlash.valid() || !updEmotes.valid() || !updOutfits.valid() || !updRobes.valid() || !robe.valid() || !ch.valid() || !waitPet.valid(); bool needRetry = !updPets.valid() || !updPerks.valid() || !updFlash.valid() || !updEmotes.valid() || !updOutfits.valid() || !updRobes.valid() || !robe.valid() || !ch.valid() || !waitPet.valid();
if (needRetry) { if (needRetry) {
@ -197,17 +243,23 @@ bool installPatches() {
auto p5 = Il2CppResolver::updateLockedOutfits(); auto p5 = Il2CppResolver::updateLockedOutfits();
auto p6 = Il2CppResolver::updateLockedRobes(); auto p6 = Il2CppResolver::updateLockedRobes();
auto pw = Il2CppResolver::waitForPurchasedPet(); auto pw = Il2CppResolver::waitForPurchasedPet();
auto uh = Il2CppResolver::userHasItem();
auto tk = Il2CppResolver::getCurrentTokens();
bool any2=false; bool any2=false;
if(r.valid()){ if(MH_CreateHook(r.address,(void*)hIsRobe,(void**)&oIsRobe)==MH_OK){ MH_EnableHook(r.address); any2=true; logPatch("[DevourUnlocker] Retry hooked IsRobeUnlocked"); } } if(r.valid()){ if(MH_CreateHook(r.address,(void*)hIsRobe,(void**)&oIsRobe)==MH_OK){ MH_EnableHook(r.address); any2=true; logPatch("[DevourUnlocker] Retry hooked IsRobeUnlocked"); } }
if(c.valid()){ if(MH_CreateHook(c.address,(void*)hIsChar,(void**)&oIsChar)==MH_OK){ MH_EnableHook(c.address); any2=true; logPatch("[DevourUnlocker] Retry hooked IsCharacterUnlocked"); } } if(c.valid()){ if(MH_CreateHook(c.address,(void*)hIsChar,(void**)&oIsChar)==MH_OK){ MH_EnableHook(c.address); any2=true; logPatch("[DevourUnlocker] Retry hooked IsCharacterUnlocked"); } }
if(p1.valid()) any2 |= hookUpdateLocked(p1.address, "UpdateLockedPets[retry]", Config::get().unlockPets, hUpdateLockedPets); if(hp.valid()){ if(MH_CreateHook(hp.address,(void*)hHasPerk,(void**)&oHasPerk)==MH_OK){ MH_EnableHook(hp.address); any2=true; logPatch("[DevourUnlocker] Retry hooked HasPerk"); } }
if(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks); if(ho.valid()){ if(MH_CreateHook(ho.address,(void*)hHasOutfit,(void**)&oHasOutfit)==MH_OK){ MH_EnableHook(ho.address); any2=true; logPatch("[DevourUnlocker] Retry hooked HasOutfit"); } }
if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights); if(uh.valid()){ void* og=nullptr; if(MH_CreateHook(uh.address,(void*)hUserHasItem,&og)==MH_OK){ MH_EnableHook(uh.address); oUserHasItem=reinterpret_cast<UserHasItemFn>(og); any2=true; logPatch("[DevourUnlocker] Retry hooked UserHasItem"); } }
if(p4.valid()) any2 |= hookUpdateLocked(p4.address, "UpdateLockedEmotes[retry]", Config::get().unlockEmotes, hUpdateLockedEmotes); if(tk.valid()){ void* og=nullptr; if(MH_CreateHook(tk.address,(void*)hGetCurrentTokens,&og)==MH_OK){ MH_EnableHook(tk.address); oGetTokens=reinterpret_cast<GetTokensFn>(og); any2=true; logPatch("[DevourUnlocker] Retry hooked GetCurrentTokens"); } }
if(p5.valid()) any2 |= hookUpdateLocked(p5.address, "UpdateLockedOutfits[retry]", Config::get().unlockOutfits, hUpdateLockedOutfits); if(p1.valid()) any2 |= hookUpdateLocked(p1.address, "UpdateLockedPets[retry]", Config::get().unlockPets, hUpdateLockedPets, &oUpdPets);
if(p6.valid()) any2 |= hookUpdateLocked(p6.address, "UpdateLockedRobes[retry]", Config::get().unlockRobes, hUpdateLockedRobes); if(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks, &oUpdPerks);
if(pw.valid()) any2 |= hookUpdateLocked(pw.address, "WaitForPurchasedPetValidation[retry]", Config::get().unlockPets, hWaitForPurchasedPet); if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights, &oUpdFlash);
if(any2){ logPatch("[DevourUnlocker] Retry hooked some — dumping diagnostics"); Il2CppResolver::dumpDiagnostics(); break; } if(p4.valid()) any2 |= hookUpdateLocked(p4.address, "UpdateLockedEmotes[retry]", Config::get().unlockEmotes, hUpdateLockedEmotes, &oUpdEmotes);
if(p5.valid()) any2 |= hookUpdateLocked(p5.address, "UpdateLockedOutfits[retry]", Config::get().unlockOutfits, hUpdateLockedOutfits, &oUpdOutfits);
if(p6.valid()) any2 |= hookUpdateLocked(p6.address, "UpdateLockedRobes[retry]", Config::get().unlockRobes, hUpdateLockedRobes, &oUpdRobes);
if(pw.valid()) any2 |= hookUpdateLocked(pw.address, "WaitForPurchasedPetValidation[retry]", Config::get().unlockPets, hWaitForPurchasedPet, &oWaitPet);
if(any2){ logPatch("[DevourUnlocker] Retry hooked missing targets — done"); break; }
if(attempt==5 || attempt==10 || attempt==19) { if(attempt==5 || attempt==10 || attempt==19) {
char buf[128]; snprintf(buf,sizeof(buf),"[DevourUnlocker] Retry attempt %d still not found",attempt); char buf[128]; snprintf(buf,sizeof(buf),"[DevourUnlocker] Retry attempt %d still not found",attempt);
logPatch(buf); logPatch(buf);