#include "patches.hpp" #include "il2cpp_resolver.hpp" #include "config.hpp" #include #include #include #ifdef _WIN32 #include #include #endif static void logPatch(const char* s){ #ifdef _WIN32 try{ char exe[MAX_PATH]; GetModuleFileNameA(nullptr, exe, MAX_PATH); std::filesystem::path p(exe); p = p.parent_path() / "DevourUnlocker.log"; std::ofstream f(p, std::ios::app); f << s << std::endl; }catch(...){} #endif } namespace devour { #ifdef _WIN32 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); using PlayerHasItemFn = bool(__fastcall*)(void* thiz, int connectionId, unsigned long long itemDef, void* method); using HasRetrievedFn = bool(__fastcall*)(void* thiz, void* method); static IsUnlockedFn oIsRobe = 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 PlayerHasItemFn oPlayerHasItem = nullptr; static HasRetrievedFn oHasRetrieved = nullptr; static bool g_installed = false; static bool g_mhInit = false; // Forward for UpdateLocked hooks — we patch them to bypass locked UI static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*)); static std::string il2cppStringToUtf8(void* ilStr) { if (!ilStr) return ""; // Il2CppString layout: length at offset 0x10, chars at 0x14 int32_t len = *reinterpret_cast(reinterpret_cast(ilStr) + 0x10); wchar_t* chars = reinterpret_cast(reinterpret_cast(ilStr) + 0x14); if (len <= 0 || len > 512) return ""; char buf[1024]{}; int n = WideCharToMultiByte(CP_UTF8, 0, chars, len, buf, sizeof(buf)-1, nullptr, nullptr); if (n > 0) return std::string(buf, n); return ""; } static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* characterStr, void* method) { return true; } static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { logPatch((std::string("[trace] IsCharacterUnlocked(\"") + il2cppStringToUtf8(charStr) + "\") -> true").c_str()); return true; } static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { logPatch((std::string("[trace] HasPerk(\"") + il2cppStringToUtf8(perkStr) + "\") -> true").c_str()); return true; } static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { logPatch((std::string("[trace] HasOutfit(\"") + il2cppStringToUtf8(outfitStr) + "\",\"" + il2cppStringToUtf8(characterStr) + "\") -> true").c_str()); 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. // LOGS every query — if the pet UI never appears in the log, it reads the userItems list field directly. static bool __fastcall hUserHasItem(void* thiz, unsigned long long itemDef, void* method) { // pass-through + trace (v2.1.0 proved forcing true blocks purchases) char b[96]; snprintf(b,sizeof(b),"[trace] UserHasItem(def=%llu)", (unsigned long long)itemDef); logPatch(b); if (oUserHasItem) return oUserHasItem(thiz, itemDef, method); return false; } // Multiplayer validator: every player counts as owning every item (lobby validation path) static bool __fastcall hPlayerHasItem(void* thiz, int connectionId, unsigned long long itemDef, void* method) { // MUST stay pass-through: forcing true desyncs lobby validation vs injected // userItems and UNLOCKS REGRESS (pets locked again). See v2.1.1 regression. char b[128]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu)", connectionId, (unsigned long long)itemDef); logPatch(b); if (oPlayerHasItem) return oPlayerHasItem(thiz, connectionId, itemDef, method); return false; } // Inventory-retrieved flag: pass through — forcing true with an empty userItems made some // flows treat items as definitively NOT owned; original behavior is safer now static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) { if (oHasRetrieved) return oHasRetrieved(thiz, method); return true; } // Perks bought with Tokens: huge balance — perk modal deducts locally, so every perk // is instantly affordable. Store items don't use tokens (they route through Steam). 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*), UpdateLockedFn* origOut) { if (!addr) { logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; } (void)shouldUnlock; // always-on void* orig = nullptr; MH_STATUS st = MH_CreateHook(addr, (void*)hook, &orig); if (st == MH_OK) { MH_EnableHook(addr); if (origOut) *origOut = reinterpret_cast(orig); logPatch((std::string("[DevourUnlocker] Hooked ")+name).c_str()); return true; } logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed st="+std::to_string((int)st)).c_str()); return false; } bool installPatches() { logPatch("[DevourUnlocker] installPatches enter"); if (g_installed) { logPatch("[DevourUnlocker] already installed"); return true; } if (!g_mhInit) { MH_STATUS st = MH_Initialize(); if (st != MH_OK && st != MH_ERROR_ALREADY_INITIALIZED) { std::cerr << "[DevourUnlocker] MH_Initialize failed " << st << "\n"; logPatch(("[DevourUnlocker] MH_Initialize failed "+std::to_string(st)).c_str()); return false; } g_mhInit = true; logPatch("[DevourUnlocker] MH_Initialize ok (or already)"); } if (!Il2CppResolver::init()) { std::cerr << "[DevourUnlocker] GameAssembly not found\n"; logPatch("[DevourUnlocker] GameAssembly not found"); return false; } logPatch("[DevourUnlocker] GameAssembly found"); // Wait for IL2CPP domain to be ready (Unity inits it a few seconds after GameAssembly load) { auto domain_get = reinterpret_cast(Il2CppResolver::getProc("il2cpp_domain_get")); if (domain_get) { for(int i=0;i<100;i++){ if(domain_get()){ logPatch("[DevourUnlocker] IL2CPP domain ready"); break; } Sleep(100); } if(!domain_get()) logPatch("[DevourUnlocker] IL2CPP domain still null after wait"); } } auto robe = Il2CppResolver::isRobeUnlocked(); auto ch = Il2CppResolver::isCharacterUnlocked(); auto hasPerk = Il2CppResolver::hasPerk(); auto hasOutfit = Il2CppResolver::hasOutfit(); auto updPets = Il2CppResolver::updateLockedPets(); auto updPerks = Il2CppResolver::updateLockedPerks(); auto updFlash = Il2CppResolver::updateLockedFlashlights(); auto updEmotes = Il2CppResolver::updateLockedEmotes(); auto updOutfits = Il2CppResolver::updateLockedOutfits(); auto updRobes = Il2CppResolver::updateLockedRobes(); auto waitPet = Il2CppResolver::waitForPurchasedPet(); std::cout << "[DevourUnlocker] Resolved IsRobeUnlocked=" << robe.address << " IsCharacterUnlocked=" << ch.address << " HasPerk=" << hasPerk.address << " HasOutfit=" << hasOutfit.address << "\n"; std::cout << "[DevourUnlocker] UpdateLocked: Pets=" << updPets.address << " Perks=" << updPerks.address << " Flash=" << updFlash.address << " Emotes=" << updEmotes.address << " Outfits=" << updOutfits.address << " Robes=" << updRobes.address << " WaitPet=" << waitPet.address << "\n"; { char buf[1024]; snprintf(buf, sizeof(buf), "[DevourUnlocker] Resolved IsRobe=%p IsChar=%p HasPerk=%p HasOutfit=%p", robe.address, ch.address, hasPerk.address, hasOutfit.address); logPatch(buf); { HMODULE ga = GetModuleHandleA("GameAssembly.dll"); char b3[160]; snprintf(b3,sizeof(b3),"[rva] GameAssembly base=%p", (void*)ga); logPatch(b3); } snprintf(buf, sizeof(buf), "[DevourUnlocker] UpdateLocked Pets=%p Perks=%p Flash=%p Emotes=%p Outfits=%p Robes=%p WaitPet=%p", updPets.address, updPerks.address, updFlash.address, updEmotes.address, updOutfits.address, updRobes.address, waitPet.address); logPatch(buf); } bool any = false; // Diagnostic dump if UpdateLocked* not found earlier if (!updPets.valid() || !updPerks.valid() || !robe.valid()) { logPatch("[DevourUnlocker] UpdateLocked/IsRobe not found — dumping IL2CPP diagnostics"); OutputDebugStringA("[DevourUnlocker] dumping diagnostics\n"); Il2CppResolver::dumpDiagnostics(); logPatch("[DevourUnlocker] dumpDiagnostics returned"); } if (robe.valid()) { if (MH_CreateHook(robe.address, (void*)hIsRobe, (void**)&oIsRobe) == MH_OK) { MH_EnableHook(robe.address); std::cout << "[DevourUnlocker] Hooked IsRobeUnlocked\n"; logPatch("[DevourUnlocker] Hooked IsRobeUnlocked"); any = true; } else std::cerr << "[DevourUnlocker] Hook IsRobe failed\n"; } else { std::cerr << "[DevourUnlocker] IsRobeUnlocked not found — will retry on next init or use standalone\n"; logPatch("[DevourUnlocker] IsRobeUnlocked not found"); } if (ch.valid()) { if (MH_CreateHook(ch.address, (void*)hIsChar, (void**)&oIsChar) == MH_OK) { MH_EnableHook(ch.address); logPatch("[DevourUnlocker] Hooked IsCharacterUnlocked"); any = true; } else logPatch("[DevourUnlocker] Hook IsChar failed"); } if (hasPerk.valid()) { if (MH_CreateHook(hasPerk.address, (void*)hHasPerk, (void**)&oHasPerk) == MH_OK) { MH_EnableHook(hasPerk.address); 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); logPatch("[DevourUnlocker] Hooked HasOutfit"); 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(); auto phi = Il2CppResolver::playerHasItem(); auto hri = Il2CppResolver::hasRetrievedUserItems(); char b2[512]; snprintf(b2, sizeof(b2), "[DevourUnlocker] Resolved UserHasItem=%p GetCurrentTokens=%p PlayerHasItem=%p HasRetrievedUserItems=%p", uhi.address, tok.address, phi.address, hri.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"); if (phi.valid()) { void* orig = nullptr; if (MH_CreateHook(phi.address, (void*)hPlayerHasItem, &orig) == MH_OK) { MH_EnableHook(phi.address); oPlayerHasItem = reinterpret_cast(orig); logPatch("[DevourUnlocker] Hooked PlayerHasItem -> always owned"); any = true; } else logPatch("[DevourUnlocker] Hook PlayerHasItem failed"); } else logPatch("[DevourUnlocker] PlayerHasItem not found"); if (hri.valid()) { void* orig = nullptr; if (MH_CreateHook(hri.address, (void*)hHasRetrievedUserItems, &orig) == MH_OK) { MH_EnableHook(hri.address); oHasRetrieved = reinterpret_cast(orig); logPatch("[DevourUnlocker] Hooked HasRetrievedUserInventoryItems -> true"); any = true; } else logPatch("[DevourUnlocker] Hook HasRetrievedUserInventoryItems failed"); } else logPatch("[DevourUnlocker] HasRetrievedUserInventoryItems 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) { logPatch("[DevourUnlocker] Some hooks not found — scheduling retry for 60s (IL2CPP may not be ready until lobby)"); CreateThread(nullptr, 0, [](LPVOID)->DWORD{ for(int attempt=0; attempt<20; ++attempt){ Sleep(3000); auto r = Il2CppResolver::isRobeUnlocked(); auto c = Il2CppResolver::isCharacterUnlocked(); auto hp = Il2CppResolver::hasPerk(); auto ho = Il2CppResolver::hasOutfit(); auto p1 = Il2CppResolver::updateLockedPets(); auto p2 = Il2CppResolver::updateLockedPerks(); auto p3 = Il2CppResolver::updateLockedFlashlights(); auto p4 = Il2CppResolver::updateLockedEmotes(); auto p5 = Il2CppResolver::updateLockedOutfits(); auto p6 = Il2CppResolver::updateLockedRobes(); auto pw = Il2CppResolver::waitForPurchasedPet(); auto uh = Il2CppResolver::userHasItem(); auto tk = Il2CppResolver::getCurrentTokens(); auto ph = Il2CppResolver::playerHasItem(); auto hr = Il2CppResolver::hasRetrievedUserItems(); 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(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(ph.valid()){ void* og=nullptr; if(MH_CreateHook(ph.address,(void*)hPlayerHasItem,&og)==MH_OK){ MH_EnableHook(ph.address); oPlayerHasItem=reinterpret_cast(og); any2=true; logPatch("[DevourUnlocker] Retry hooked PlayerHasItem"); } } if(hr.valid()){ void* og=nullptr; if(MH_CreateHook(hr.address,(void*)hHasRetrievedUserItems,&og)==MH_OK){ MH_EnableHook(hr.address); oHasRetrieved=reinterpret_cast(og); any2=true; logPatch("[DevourUnlocker] Retry hooked HasRetrievedUserInventoryItems"); } } 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); if(attempt==10) Il2CppResolver::dumpDiagnostics(); } } return 0; }, nullptr, 0, nullptr); } g_installed = any; logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed"); // Ownership fabricator + introspection thread: // - every 15s inject missing catalog ids into userItems (list is recreated on refresh) // - one layout dump at 30s for diagnostics CreateThread(nullptr, 0, [](LPVOID)->DWORD{ Sleep(30000); logPatch("[DevourUnlocker] inspectInventoryState @30s"); Il2CppResolver::inspectInventoryState(); int total=0; for(int cycle=0; cycle<240; ++cycle){ // ~60 min of refills Sleep(15000); int n = Il2CppResolver::fabricateOwnership(); if(n>0){ total+=n; char b[128]; snprintf(b,sizeof(b),"[DevourUnlocker] ownership injected +%d (session total %d)",n,total); logPatch(b); } } return 0; }, nullptr, 0, nullptr); return any; } void removePatches() { if (!g_installed) return; MH_DisableHook(MH_ALL_HOOKS); MH_Uninitialize(); g_mhInit = false; g_installed = false; } bool patchesInstalled() { return g_installed; } #else // Linux stub — uses funchook or LD_PRELOAD interpose if needed bool installPatches() { std::cout << "[DevourUnlocker] Linux patches stub — use winhttp.dll under Proton\n"; return false; } void removePatches() {} bool patchesInstalled() { return false; } #endif } // namespace devour