From 48d0edac0a48711742932fdcd2782756a544b3eb Mon Sep 17 00:00:00 2001 From: MiTHRAL Date: Mon, 24 Aug 2026 01:16:18 -0400 Subject: [PATCH] =?UTF-8?q?v2.0.8:=20unlock=20all=20categories=20=E2=80=94?= =?UTF-8?q?=20ownership=20+=20currency=20override,=20not=20UI=20no-op?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/il2cpp_resolver.cpp | 133 ++++++++++++++++++++++++++++------------ src/il2cpp_resolver.hpp | 4 +- src/patches.cpp | 132 +++++++++++++++++++++++++++------------ 3 files changed, 189 insertions(+), 80 deletions(-) diff --git a/src/il2cpp_resolver.cpp b/src/il2cpp_resolver.cpp index 1c15485..e3759b1 100644 --- a/src/il2cpp_resolver.cpp +++ b/src/il2cpp_resolver.cpp @@ -70,7 +70,7 @@ std::vector Il2CppResolver::scanPattern(const char* pattern, const char* 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 // If domain not ready, fallback to string xref quickly auto domain_get_test = reinterpret_cast(getProc("il2cpp_domain_get")); @@ -80,7 +80,8 @@ ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* method auto il2cpp_domain_get = reinterpret_cast(getProc("il2cpp_domain_get")); auto il2cpp_thread_attach = reinterpret_cast(getProc("il2cpp_thread_attach")); auto klass_from_name = reinterpret_cast(getProc("il2cpp_class_from_name")); - auto class_get_method = reinterpret_cast(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(getProc("il2cpp_class_get_method_from_name")); auto domain_get_assemblies = reinterpret_cast(getProc("il2cpp_domain_get_assemblies")); auto assembly_get_image = reinterpret_cast(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); - 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(m); // MethodInfo.methodPointer @ +0x00 + if (ptr) return ptr; + } + } + return 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]); - } + for (int ni = 0; namespaces[ni]; ++ni) { + void* klass = klass_from_name(image, namespaces[ni], klassName); if (!klass) continue; - void* method = class_get_method(klass, methodName); - if (method) { - void* ptr = *reinterpret_cast(reinterpret_cast(method) + 0x18); - if (ptr) return {methodName, ptr}; - } + void* ptr = tryMethod(klass); + if (ptr) return {methodName, ptr}; } } - // Brute force: enumerate all classes in all images for methodName (UpdateLocked* etc live in unknown class) + // Brute force: enumerate all classes in all images for methodName (works regardless of namespace) auto image_get_class_count = reinterpret_cast(getProc("il2cpp_image_get_class_count")); auto image_get_class = reinterpret_cast(getProc("il2cpp_image_get_class")); if (image_get_class_count && image_get_class) { @@ -128,11 +138,8 @@ ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* method 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(reinterpret_cast(method) + 0x18); - if (ptr) return {methodName, ptr}; - } + void* ptr = tryMethod(klass); + if (ptr) return {methodName, ptr}; } } } @@ -183,18 +190,28 @@ ResolvedMethod Il2CppResolver::resolveByStringXref(const char* str, int occurren 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"); } +// Real targets from global-metadata.dat (v31, 2022.3.62f2): +// OptionsHelpers.IsRobeUnlocked(robe, character) — global ns +// OptionsHelpers.IsCharacterUnlocked(prefab) — global ns +// OptionsHelpers.HasOutfit(outfitName, characterName) — global ns +// InGameHelpers.HasPerk(perk) / NolanPerkController.HasPerk(perk) +// Horror.Menu.UpdateLocked{Pets,Perks,Flashlights,Emotes,Outfits}(selectionViews) +// Horror.Menu.UpdateLockedRobes() +// Horror.Menu.WaitForPurchasedPetValidation(pet) +ResolvedMethod Il2CppResolver::isRobeUnlocked() { return resolve("OptionsHelpers", "IsRobeUnlocked", 2); } +ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("OptionsHelpers", "IsCharacterUnlocked", 1); } +ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("OptionsHelpers", "HasOutfit", 2); } +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(){ #ifdef _WIN32 @@ -204,16 +221,52 @@ void Il2CppResolver::dumpDiagnostics(){ 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 (safe mode - minimal)\n"; f.flush(); + f << "[Diag] dumpDiagnostics ENTER (safe mode - targeted)\n"; f.flush(); auto il2cpp_domain_get = reinterpret_cast(getProc("il2cpp_domain_get")); if(!il2cpp_domain_get){ f << "[Diag] no domain_get\n"; return; } void* domain = il2cpp_domain_get(); 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) - f << "[Diag] domain=" << domain << " - skipping full enumeration to avoid crash\n"; - f << "[Diag] If hooks not found, IL2CPP may not be ready until lobby - retry thread will handle\n"; + auto klass_from_name = reinterpret_cast(getProc("il2cpp_class_from_name")); + auto class_get_method = reinterpret_cast(getProc("il2cpp_class_get_method_from_name")); + auto domain_get_assemblies = reinterpret_cast(getProc("il2cpp_domain_get_assemblies")); + auto assembly_get_image = reinterpret_cast(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(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"; - return; // DISABLED heavy enumeration that caused crash (strlen null at 0x8eb3) #if 0 auto domain_get_assemblies = reinterpret_cast(getProc("il2cpp_domain_get_assemblies")); @@ -262,7 +315,7 @@ void Il2CppResolver::dumpDiagnostics(){ void* m = class_get_method(klass, targets[ti]); f << " try " << targets[ti] << " -> " << m; if(m){ - void* ptr = *reinterpret_cast(reinterpret_cast(m) + 0x18); + void* ptr = *reinterpret_cast(m); // MethodInfo.methodPointer @ +0x00 f << " ptr=" << ptr; auto method_get_name2 = reinterpret_cast(getProc("il2cpp_method_get_name")); if(method_get_name2){ @@ -293,7 +346,7 @@ void Il2CppResolver::dumpDiagnostics(){ for(int ti=0; targets[ti]; ++ti){ void* m = class_get_method(klass, targets[ti]); if(m) { - void* ptr = *reinterpret_cast(reinterpret_cast(m) + 0x18); + void* ptr = *reinterpret_cast(m); // MethodInfo.methodPointer @ +0x00 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::updateLockedRobes() { return {"UpdateLockedRobes", nullptr}; } ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return {"WaitForPurchasedPetValidation", nullptr}; } +ResolvedMethod Il2CppResolver::userHasItem() { return {"UserHasItem", nullptr}; } +ResolvedMethod Il2CppResolver::getCurrentTokens() { return {"GetCurrentTokens", nullptr}; } #endif diff --git a/src/il2cpp_resolver.hpp b/src/il2cpp_resolver.hpp index ba2215a..34bb824 100644 --- a/src/il2cpp_resolver.hpp +++ b/src/il2cpp_resolver.hpp @@ -25,7 +25,7 @@ public: static void* getProc(const char* name); static std::vector 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 isRobeUnlocked(); @@ -40,6 +40,8 @@ public: static ResolvedMethod updateLockedOutfits(); static ResolvedMethod updateLockedRobes(); static ResolvedMethod waitForPurchasedPet(); + static ResolvedMethod userHasItem(); // SteamInventoryManager.UserHasItem(steamItemDefID) + static ResolvedMethod getCurrentTokens(); // Horror.Menu.GetCurrentTokens() static void dumpDiagnostics(); static uintptr_t gameAssemblyBase(); static size_t gameAssemblySize(); diff --git a/src/patches.cpp b/src/patches.cpp index c680797..f6dfa39 100644 --- a/src/patches.cpp +++ b/src/patches.cpp @@ -24,13 +24,25 @@ namespace devour { #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 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 oIsChar = nullptr; -static IsUnlockedFn oHasPerk = nullptr; +static IsUnlockedFn1 oIsChar = nullptr; +static IsUnlockedFn1 oHasPerk = 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_mhInit = false; @@ -49,31 +61,39 @@ static std::string il2cppStringToUtf8(void* ilStr) { 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 hHasPerk(void* thiz, void* perkStr, void* method) { return true; } -static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* method) { return true; } -// Always-on: every UpdateLocked* is bypassed -static void __fastcall hUpdateLockedPets(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedPets bypassed (always-on)"); } -static void __fastcall hWaitForPurchasedPet(void* thiz, void* method) { logPatch("[DevourUnlocker] WaitForPurchasedPetValidation bypassed (always-on)"); } -static void __fastcall hUpdateLockedPerks(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedPerks bypassed (always-on)"); } -static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedFlashlights bypassed"); } -static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedEmotes bypassed"); } -static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedOutfits bypassed"); } -static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { logPatch("[DevourUnlocker] UpdateLockedRobes bypassed"); } +static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { return true; } +// UpdateLocked*: call the ORIGINAL so the UI runs its normal flow — ownership checks +// (UserHasItem→true) make it mark everything unlocked instead of leaving defaults locked +static void __fastcall hUpdateLockedPets(void* thiz, void* method) { if (oUpdPets) oUpdPets(thiz, method); } +static void __fastcall hWaitForPurchasedPet(void* thiz, void* method) { if (oWaitPet) oWaitPet(thiz, method); } +static void __fastcall hUpdateLockedPerks(void* thiz, void* method) { if (oUpdPerks) oUpdPerks(thiz, method); } +static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { if (oUpdFlash) oUpdFlash(thiz, method); } +static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { if (oUpdEmotes) oUpdEmotes(thiz, method); } +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*)) { - if (!addr) { std::cerr << "[DevourUnlocker] " << name << " not found\n"; logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; } +static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*), UpdateLockedFn* origOut) { + if (!addr) { logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; } (void)shouldUnlock; // always-on 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); - std::cout << "[DevourUnlocker] Hooked " << name << "\n"; + if (origOut) *origOut = reinterpret_cast(orig); logPatch((std::string("[DevourUnlocker] Hooked ")+name).c_str()); return true; } - std::cerr << "[DevourUnlocker] Hook " << name << " failed\n"; - logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed").c_str()); + logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed st="+std::to_string((int)st)).c_str()); return false; } @@ -154,31 +174,57 @@ bool installPatches() { if (ch.valid()) { if (MH_CreateHook(ch.address, (void*)hIsChar, (void**)&oIsChar) == MH_OK) { MH_EnableHook(ch.address); - std::cout << "[DevourUnlocker] Hooked IsCharacterUnlocked\n"; + logPatch("[DevourUnlocker] Hooked IsCharacterUnlocked"); any = true; - } else std::cerr << "[DevourUnlocker] Hook IsChar failed\n"; + } else logPatch("[DevourUnlocker] Hook IsChar failed"); } if (hasPerk.valid()) { if (MH_CreateHook(hasPerk.address, (void*)hHasPerk, (void**)&oHasPerk) == MH_OK) { MH_EnableHook(hasPerk.address); - std::cout << "[DevourUnlocker] Hooked HasPerk\n"; + logPatch("[DevourUnlocker] Hooked HasPerk"); any = true; - } + } else logPatch("[DevourUnlocker] Hook HasPerk failed"); } if (hasOutfit.valid()) { if (MH_CreateHook(hasOutfit.address, (void*)hHasOutfit, (void**)&oHasOutfit) == MH_OK) { MH_EnableHook(hasOutfit.address); - std::cout << "[DevourUnlocker] Hooked HasOutfit\n"; + logPatch("[DevourUnlocker] Hooked HasOutfit"); any = true; - } + } else logPatch("[DevourUnlocker] Hook HasOutfit failed"); } - any |= hookUpdateLocked(updPets.address, "UpdateLockedPets", Config::get().unlockPets, hUpdateLockedPets); - any |= hookUpdateLocked(updPerks.address, "UpdateLockedPerks", Config::get().unlockPerks, hUpdateLockedPerks); - any |= hookUpdateLocked(updFlash.address, "UpdateLockedFlashlights", Config::get().unlockFlashlights, hUpdateLockedFlashlights); - any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes); - any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits); - any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes); - any |= hookUpdateLocked(waitPet.address, "WaitForPurchasedPetValidation", Config::get().unlockPets, hWaitForPurchasedPet); + // 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(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(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, &oUpdPets); + any |= hookUpdateLocked(updPerks.address, "UpdateLockedPerks", Config::get().unlockPerks, hUpdateLockedPerks, &oUpdPerks); + any |= hookUpdateLocked(updFlash.address, "UpdateLockedFlashlights", Config::get().unlockFlashlights, hUpdateLockedFlashlights, &oUpdFlash); + any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes, &oUpdEmotes); + any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits, &oUpdOutfits); + any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes, &oUpdRobes); + 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 bool needRetry = !updPets.valid() || !updPerks.valid() || !updFlash.valid() || !updEmotes.valid() || !updOutfits.valid() || !updRobes.valid() || !robe.valid() || !ch.valid() || !waitPet.valid(); if (needRetry) { @@ -197,17 +243,23 @@ bool installPatches() { auto p5 = Il2CppResolver::updateLockedOutfits(); auto p6 = Il2CppResolver::updateLockedRobes(); auto pw = Il2CppResolver::waitForPurchasedPet(); + auto uh = Il2CppResolver::userHasItem(); + auto tk = Il2CppResolver::getCurrentTokens(); 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(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(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks); - if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights); - if(p4.valid()) any2 |= hookUpdateLocked(p4.address, "UpdateLockedEmotes[retry]", Config::get().unlockEmotes, hUpdateLockedEmotes); - if(p5.valid()) any2 |= hookUpdateLocked(p5.address, "UpdateLockedOutfits[retry]", Config::get().unlockOutfits, hUpdateLockedOutfits); - if(p6.valid()) any2 |= hookUpdateLocked(p6.address, "UpdateLockedRobes[retry]", Config::get().unlockRobes, hUpdateLockedRobes); - if(pw.valid()) any2 |= hookUpdateLocked(pw.address, "WaitForPurchasedPetValidation[retry]", Config::get().unlockPets, hWaitForPurchasedPet); - if(any2){ logPatch("[DevourUnlocker] Retry hooked some — dumping diagnostics"); Il2CppResolver::dumpDiagnostics(); break; } + 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(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(uh.valid()){ void* og=nullptr; if(MH_CreateHook(uh.address,(void*)hUserHasItem,&og)==MH_OK){ MH_EnableHook(uh.address); oUserHasItem=reinterpret_cast(og); any2=true; logPatch("[DevourUnlocker] Retry hooked UserHasItem"); } } + if(tk.valid()){ void* og=nullptr; if(MH_CreateHook(tk.address,(void*)hGetCurrentTokens,&og)==MH_OK){ MH_EnableHook(tk.address); oGetTokens=reinterpret_cast(og); any2=true; logPatch("[DevourUnlocker] Retry hooked GetCurrentTokens"); } } + if(p1.valid()) any2 |= hookUpdateLocked(p1.address, "UpdateLockedPets[retry]", Config::get().unlockPets, hUpdateLockedPets, &oUpdPets); + if(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks, &oUpdPerks); + if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights, &oUpdFlash); + 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) { char buf[128]; snprintf(buf,sizeof(buf),"[DevourUnlocker] Retry attempt %d still not found",attempt); logPatch(buf);