diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 684df02..2f5837c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,25 +7,29 @@ on: workflow_dispatch: jobs: - windows: - runs-on: windows-latest + # Cross-compile Windows winhttp.dll on Linux (no native Windows runner on git.mithraic.cloud) + windows-cross: + runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v4 with: { submodules: recursive } - - name: CMake Configure (Win) - run: cmake -B build -DCMAKE_BUILD_TYPE=Release - - name: Build - run: cmake --build build --config Release + - name: Install mingw + cmake + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build mingw-w64 g++-mingw-w64-x86-64 + - name: CMake Configure (MinGW cross) + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DBUILD_WINHTTP_PROXY=ON + - name: Build (MinGW) + run: cmake --build build --config Release || (cat build/CMakeFiles/CMakeError.log; exit 1) - name: Package run: | - mkdir dist - cp build/winhttp.dll dist/ 2>/dev/null || echo "winhttp not built" - cp build/DevourUnlocker.dll dist/ 2>/dev/null || echo "dll not built" - echo '{"unlockRobes":true,"unlockCharacters":true}' > dist/DevourUnlocker.json - Compress-Archive -Path dist/* -DestinationPath DevourUnlocker_winhttp_${{ github.ref_name }}.zip - shell: pwsh + mkdir -p dist + cp build/winhttp.dll dist/ 2>/dev/null || cp build/*.dll dist/ 2>/dev/null || echo "no dll built (expected on cross without win32 SDK)" + cp build/DevourUnlocker.dll dist/ 2>/dev/null || true + cp DevourUnlocker.json dist/ 2>/dev/null || echo '{"unlockRobes":true,"unlockCharacters":true,"unlockPerks":true,"unlockPets":true,"unlockFlashlights":true,"unlockEmotes":true,"unlockOutfits":true}' > dist/DevourUnlocker.json + zip -j DevourUnlocker_winhttp_${{ github.ref_name }}.zip dist/* || true + ls -lh dist/ - uses: actions/upload-artifact@v4 - with: { name: winhttp, path: DevourUnlocker_winhttp_*.zip } + with: { name: winhttp-cross, path: DevourUnlocker_winhttp_*.zip } linux: runs-on: ubuntu-latest diff --git a/BepInEx/DevourUnlocker.cs b/BepInEx/DevourUnlocker.cs index a3294f7..84b8540 100644 --- a/BepInEx/DevourUnlocker.cs +++ b/BepInEx/DevourUnlocker.cs @@ -40,7 +40,6 @@ namespace DevourUnlocker.BepInEx // Generic unlock patches — try multiple owner classes static class Patches { - // Use TargetMethod to handle both old and new class names [HarmonyPatch] class IsRobeUnlockedPatch { @@ -53,15 +52,14 @@ namespace DevourUnlocker.BepInEx var m = AccessTools.Method(type, "IsRobeUnlocked"); if (m != null) { Plugin.Log?.LogInfo($"Found IsRobeUnlocked on {t}"); return m; } } - Plugin.Log?.LogWarning("IsRobeUnlocked not found — will retry via standalone"); + Plugin.Log?.LogWarning("IsRobeUnlocked not found"); return null; } static bool Prefix(ref bool __result, object __instance, string robeName) { - if (!DevourGUI.configUnlockRobes) return true; // run original + if (!DevourGUI.configUnlockRobes) return true; if (DevourGUI.robeOverrides.TryGetValue(robeName ?? "", out var v)) { __result = v; return false; } __result = true; - if (!string.IsNullOrEmpty(robeName)) Plugin.Log?.LogInfo($"[Robe] {robeName} -> unlocked"); return false; } } @@ -88,6 +86,33 @@ namespace DevourUnlocker.BepInEx return false; } } + + // UpdateLocked* patches — force unlock for pets/perks/flashlights/emotes/outfits/robes + static System.Reflection.MethodBase FindUpdate(string name) { + var types = new[] { "LobbyController", "UILobbyController", "InventoryController", "NolanRankController" }; + foreach (var t in types) { + var type = AccessTools.TypeByName(t); + if (type==null) continue; + var m = AccessTools.Method(type, name); + if (m!=null) { Plugin.Log?.LogInfo($"Found {name} on {t}"); return m; } + } + return null; + } + [HarmonyPatch] class UpdateLockedPetsPatch { static System.Reflection.MethodBase TargetMethod()=> FindUpdate("UpdateLockedPets"); static bool Prefix()=> !DevourGUI.configUnlockPets ? true : false; } + [HarmonyPatch] class UpdateLockedPerksPatch { static System.Reflection.MethodBase TargetMethod()=> FindUpdate("UpdateLockedPerks"); static bool Prefix()=> !DevourGUI.configUnlockPerks ? true : false; } + [HarmonyPatch] class UpdateLockedFlashlightsPatch { static System.Reflection.MethodBase TargetMethod()=> FindUpdate("UpdateLockedFlashlights"); static bool Prefix()=> !DevourGUI.configUnlockFlashlights ? true : false; } + [HarmonyPatch] class UpdateLockedEmotesPatch { static System.Reflection.MethodBase TargetMethod()=> FindUpdate("UpdateLockedEmotes"); static bool Prefix()=> !DevourGUI.configUnlockEmotes ? true : false; } + [HarmonyPatch] class UpdateLockedOutfitsPatch { static System.Reflection.MethodBase TargetMethod()=> FindUpdate("UpdateLockedOutfits"); static bool Prefix()=> !DevourGUI.configUnlockOutfits ? true : false; } + [HarmonyPatch] class UpdateLockedRobesPatch { static System.Reflection.MethodBase TargetMethod()=> FindUpdate("UpdateLockedRobes"); static bool Prefix()=> !DevourGUI.configUnlockRobes ? true : false; } + + [HarmonyPatch] class HasPerkPatch { + static System.Reflection.MethodBase TargetMethod(){ var t=AccessTools.TypeByName("NolanRankController"); return t==null?null:AccessTools.Method(t,"HasPerk"); } + static bool Prefix(ref bool __result, string perkName){ if(!DevourGUI.configUnlockPerks) return true; if(DevourGUI.perkOverrides.TryGetValue(perkName??"",out var v)){__result=v; return false;} __result=true; return false;} + } + [HarmonyPatch] class HasOutfitPatch { + static System.Reflection.MethodBase TargetMethod(){ var t=AccessTools.TypeByName("NolanRankController"); return t==null?null:AccessTools.Method(t,"HasOutfit"); } + static bool Prefix(ref bool __result, string outfitName){ if(!DevourGUI.configUnlockOutfits) return true; if(DevourGUI.outfitOverrides.TryGetValue(outfitName??"",out var v)){__result=v; return false;} __result=true; return false;} + } } static class DevourGUI @@ -95,44 +120,66 @@ namespace DevourUnlocker.BepInEx public static bool visible = true; public static bool configUnlockRobes = true; public static bool configUnlockChars = true; + public static bool configUnlockPerks = true; + public static bool configUnlockPets = true; + public static bool configUnlockFlashlights = true; + public static bool configUnlockEmotes = true; + public static bool configUnlockOutfits = true; public static System.Collections.Generic.Dictionary robeOverrides = new(); public static System.Collections.Generic.Dictionary charOverrides = new(); + public static System.Collections.Generic.Dictionary perkOverrides = new(); + public static System.Collections.Generic.Dictionary outfitOverrides = new(); static string[] robes = new[] { "StraightJacket","Patchwork","CherryBlossom","Eggs","Poison","Web", "TownHard","TownNightmare","TownBooks","Horseshoes", "SlaughterhouseHard","SlaughterhouseNightmare","SlaughterhousePigs","BarbedWires", "Golden","ManorHard","ManorNightmare","ManorHeadsBuried","Feathers", - "CarnivalHard","CarnivalNightmare","CarnivalDollHeads","Tickets" + "CarnivalHard","CarnivalNightmare","CarnivalDollHeads","Tickets", + "Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare" }; static string[] chars = new[] {"Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"}; + static string[] perks = new[] {"Acceleration","Aftershock","Conservationist","Crawler","DuracellBunny","Evader","FastWorker","FieldMedic","FirstResponder","FullyCharged","Immune","Inspired","Keymaster","Patient","Regeneration","Speedy","SupplyRunner","Trapper","Teamwork"}; + static string[] pets = new[] {"Crow","Pig","Goat","Rat","Spider","Cat","Pug"}; static UnityEngine.Vector2 scroll; + static string currentTab = "Robes"; public static void Draw() { - var rect = new UnityEngine.Rect(20, 20, 520, 600); + var rect = new UnityEngine.Rect(20, 20, 600, 640); UnityEngine.GUI.Box(rect, ""); GUILayout.BeginArea(rect); - GUILayout.Label("DevourUnlocker v2 — BepInEx Edition (F6 toggle)"); - GUILayout.Label("Multiplayer safe — client visual unlock"); - configUnlockRobes = GUILayout.Toggle(configUnlockRobes, "Unlock Robes"); - configUnlockChars = GUILayout.Toggle(configUnlockChars, "Unlock Characters"); - if (GUILayout.Button("Unlock All")) { configUnlockRobes = configUnlockChars = true; robeOverrides.Clear(); charOverrides.Clear(); } - if (GUILayout.Button("Lock All")) { configUnlockRobes = configUnlockChars = false; } - GUILayout.Label("— Robes —"); - scroll = GUILayout.BeginScrollView(scroll, GUILayout.Height(220)); - foreach (var r in robes) { - bool cur = robeOverrides.TryGetValue(r, out var ov) ? ov : configUnlockRobes; - bool nxt = GUILayout.Toggle(cur, r); - if (nxt != cur) robeOverrides[r] = nxt; + GUILayout.Label("DevourUnlocker v2 — BepInEx Edition (F6 toggle) — ALL categories"); + GUILayout.BeginHorizontal(); + if(GUILayout.Button("Robes")) currentTab="Robes"; + if(GUILayout.Button("Chars")) currentTab="Chars"; + if(GUILayout.Button("Perks")) currentTab="Perks"; + if(GUILayout.Button("Pets")) currentTab="Pets"; + GUILayout.EndHorizontal(); + configUnlockRobes = GUILayout.Toggle(configUnlockRobes, "Robes"); + configUnlockChars = GUILayout.Toggle(configUnlockChars, "Chars"); + configUnlockPerks = GUILayout.Toggle(configUnlockPerks, "Perks"); + configUnlockPets = GUILayout.Toggle(configUnlockPets, "Pets"); + configUnlockFlashlights = GUILayout.Toggle(configUnlockFlashlights, "Flashlights"); + configUnlockEmotes = GUILayout.Toggle(configUnlockEmotes, "Emotes"); + configUnlockOutfits = GUILayout.Toggle(configUnlockOutfits, "Outfits"); + if (GUILayout.Button("Unlock ALL categories")) { configUnlockRobes=configUnlockChars=configUnlockPerks=configUnlockPets=configUnlockFlashlights=configUnlockEmotes=configUnlockOutfits=true; robeOverrides.Clear(); charOverrides.Clear(); perkOverrides.Clear(); } + if (GUILayout.Button("Lock ALL")) { configUnlockRobes=configUnlockChars=configUnlockPerks=configUnlockPets=configUnlockFlashlights=configUnlockEmotes=configUnlockOutfits=false; } + string[] list = currentTab=="Robes"? robes : currentTab=="Chars"? chars : currentTab=="Perks"? perks : pets; + var dict = currentTab=="Robes"? robeOverrides : currentTab=="Chars"? charOverrides : currentTab=="Perks"? perkOverrides : null; + // pets use simple global toggle via UpdateLockedPets bypass, per-item not needed but show + GUILayout.Label($"— {currentTab} —"); + scroll = GUILayout.BeginScrollView(scroll, GUILayout.Height(260)); + if(dict!=null){ + foreach (var r in list) { + bool cur = dict.TryGetValue(r, out var ov) ? ov : true; + bool nxt = GUILayout.Toggle(cur, r); + if (nxt != cur) dict[r] = nxt; + } + } else { + GUILayout.Label("Uses global toggle (UpdateLocked* bypass)"); } GUILayout.EndScrollView(); - GUILayout.Label("— Characters —"); - foreach (var c in chars) { - bool cur = charOverrides.TryGetValue(c, out var ov) ? ov : configUnlockChars; - bool nxt = GUILayout.Toggle(cur, c); - if (nxt != cur) charOverrides[c] = nxt; - } GUILayout.EndArea(); } } diff --git a/src/config.cpp b/src/config.cpp index dafa438..4a79185 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -37,6 +37,28 @@ std::vector Config::allRobes() { std::vector Config::allCharacters() { return {"Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"}; } +std::vector Config::allPerks() { + return { + "Acceleration","Aftershock","Conservationist","Crawler","DuracellBunny","Evader", + "FastWorker","FieldMedic","FirstResponder","FullyCharged","Immune","Inspired", + "Keymaster","LongSighted","Patient","PowerfulInsight","Quartermaster","Regeneration", + "Repellent","Speedy","SpeedWalker","StayBack","SupplyRunner","Tamed","Teamwork", + "TeamLeader","Trapper","UnderPressure","CrawlerVent","ConservationistHard","PatientHard", + "RegenerationHard" + }; +} +std::vector Config::allPets() { + return {"Crow","Pig","Goat","Rat","Spider","Cat","Pug"}; +} +std::vector Config::allFlashlights() { + return {"Basic","UV","Battery","Gold","Carnival","Manor","Hospital","Infinity"}; +} +std::vector Config::allEmotes() { + return {"Wave","Dance","Sit","Point","Laugh","Cry","Angry","Cheer","Clap","Taunt","Shrug","Bow"}; +} +std::vector Config::allOutfits() { + return {"Default","Formal","Casual","Winter","Summer","Anniversary","Hard","Nightmare"}; +} bool Config::load(const std::string& path) { std::ifstream f(path); @@ -59,6 +81,7 @@ bool Config::load(const std::string& path) { g_config.unlockFlashlights = has("unlockFlashlights", true); g_config.unlockEmotes = has("unlockEmotes", true); g_config.unlockPets = has("unlockPets", true); + g_config.unlockOutfits = has("unlockOutfits", true); g_config.guiVisible = has("guiVisible", true); return true; } @@ -74,6 +97,7 @@ bool Config::save(const std::string& path) { f << " \"unlockFlashlights\": " << (c.unlockFlashlights?"true":"false") << ",\n"; f << " \"unlockEmotes\": " << (c.unlockEmotes?"true":"false") << ",\n"; f << " \"unlockPets\": " << (c.unlockPets?"true":"false") << ",\n"; + f << " \"unlockOutfits\": " << (c.unlockOutfits?"true":"false") << ",\n"; f << " \"guiVisible\": " << (c.guiVisible?"true":"false") << "\n"; f << "}\n"; return true; diff --git a/src/config.hpp b/src/config.hpp index a06eb39..ed39e84 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -17,6 +17,13 @@ struct UnlockConfig { // Per-item overrides (empty = use global). Key is internal name e.g. "StraightJacket" std::unordered_map robeOverrides; std::unordered_map characterOverrides; + std::unordered_map perkOverrides; + std::unordered_map petOverrides; + std::unordered_map flashlightOverrides; + std::unordered_map emoteOverrides; + std::unordered_map outfitOverrides; + + bool unlockOutfits = true; // UI bool guiVisible = true; @@ -31,6 +38,11 @@ public: static std::string defaultPath(); static std::vector allRobes(); static std::vector allCharacters(); + static std::vector allPerks(); + static std::vector allPets(); + static std::vector allFlashlights(); + static std::vector allEmotes(); + static std::vector allOutfits(); }; } // namespace devour diff --git a/src/gui/overlay.cpp b/src/gui/overlay.cpp index d3da269..d4f1869 100644 --- a/src/gui/overlay.cpp +++ b/src/gui/overlay.cpp @@ -70,39 +70,41 @@ void render() { changed |= ImGui::Checkbox("Unlock Characters", &cfg.unlockCharacters); changed |= ImGui::Checkbox("Unlock Perks", &cfg.unlockPerks); changed |= ImGui::Checkbox("Unlock Flashlights", &cfg.unlockFlashlights); - changed |= ImGui::Checkbox("Unlock Emotes/ Pets", &cfg.unlockEmotes); + changed |= ImGui::Checkbox("Unlock Emotes", &cfg.unlockEmotes); + changed |= ImGui::Checkbox("Unlock Pets", &cfg.unlockPets); + changed |= ImGui::Checkbox("Unlock Outfits", &cfg.unlockOutfits); if (changed) Config::save(Config::defaultPath()); if (ImGui::Button("Unlock All", ImVec2(120,30))) { - cfg.unlockRobes = cfg.unlockCharacters = cfg.unlockPerks = cfg.unlockFlashlights = cfg.unlockEmotes = true; + cfg.unlockRobes = cfg.unlockCharacters = cfg.unlockPerks = cfg.unlockFlashlights = cfg.unlockEmotes = cfg.unlockPets = cfg.unlockOutfits = true; Config::save(Config::defaultPath()); } ImGui::SameLine(); if (ImGui::Button("Lock All", ImVec2(120,30))) { - cfg.unlockRobes = cfg.unlockCharacters = cfg.unlockPerks = cfg.unlockFlashlights = cfg.unlockEmotes = false; + cfg.unlockRobes = cfg.unlockCharacters = cfg.unlockPerks = cfg.unlockFlashlights = cfg.unlockEmotes = cfg.unlockPets = cfg.unlockOutfits = false; Config::save(Config::defaultPath()); } + ImGui::TextDisabled("Pets now fixed via UpdateLockedPets hook"); } - if (ImGui::CollapsingHeader("Robes")) { - for (auto& n : Config::allRobes()) { - auto it = cfg.robeOverrides.find(n); - bool enabled = it == cfg.robeOverrides.end() ? cfg.unlockRobes : it->second; - if (ImGui::Checkbox(n.c_str(), &enabled)) { - cfg.robeOverrides[n] = enabled; - Config::save(Config::defaultPath()); + auto drawCategory = [&](const char* label, std::vector items, auto& overrides, bool global){ + if (ImGui::CollapsingHeader(label)) { + for (auto& n : items) { + auto it = overrides.find(n); + bool enabled = it == overrides.end() ? global : it->second; + if (ImGui::Checkbox(n.c_str(), &enabled)) { + overrides[n] = enabled; + Config::save(Config::defaultPath()); + } } } - } - if (ImGui::CollapsingHeader("Characters")) { - for (auto& n : Config::allCharacters()) { - auto it = cfg.characterOverrides.find(n); - bool enabled = it == cfg.characterOverrides.end() ? cfg.unlockCharacters : it->second; - if (ImGui::Checkbox(n.c_str(), &enabled)) { - cfg.characterOverrides[n] = enabled; - Config::save(Config::defaultPath()); - } - } - } + }; + drawCategory("Robes", Config::allRobes(), cfg.robeOverrides, cfg.unlockRobes); + drawCategory("Characters", Config::allCharacters(), cfg.characterOverrides, cfg.unlockCharacters); + drawCategory("Perks", Config::allPerks(), cfg.perkOverrides, cfg.unlockPerks); + drawCategory("Pets", Config::allPets(), cfg.petOverrides, cfg.unlockPets); + drawCategory("Flashlights", Config::allFlashlights(), cfg.flashlightOverrides, cfg.unlockFlashlights); + drawCategory("Emotes", Config::allEmotes(), cfg.emoteOverrides, cfg.unlockEmotes); + drawCategory("Outfits", Config::allOutfits(), cfg.outfitOverrides, cfg.unlockOutfits); ImGui::Separator(); if (ImGui::Button("Save", ImVec2(80,28))) Config::save(Config::defaultPath()); diff --git a/src/il2cpp_resolver.cpp b/src/il2cpp_resolver.cpp index 5375b1a..5c0123b 100644 --- a/src/il2cpp_resolver.cpp +++ b/src/il2cpp_resolver.cpp @@ -139,6 +139,14 @@ ResolvedMethod Il2CppResolver::resolveByStringXref(const char* str, int occurren 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 @@ -152,6 +160,14 @@ ResolvedMethod Il2CppResolver::resolveByStringXref(const char* s, int) { return 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 diff --git a/src/il2cpp_resolver.hpp b/src/il2cpp_resolver.hpp index 63e1992..f801e28 100644 --- a/src/il2cpp_resolver.hpp +++ b/src/il2cpp_resolver.hpp @@ -30,7 +30,15 @@ public: static ResolvedMethod isRobeUnlocked(); static ResolvedMethod isCharacterUnlocked(); - static ResolvedMethod hasOutfit(); // optional future + static ResolvedMethod hasOutfit(); + static ResolvedMethod hasPerk(); + static ResolvedMethod hasPet(); // may fallback to UpdateLockedPets + static ResolvedMethod updateLockedPets(); + static ResolvedMethod updateLockedPerks(); + static ResolvedMethod updateLockedFlashlights(); + static ResolvedMethod updateLockedEmotes(); + static ResolvedMethod updateLockedOutfits(); + static ResolvedMethod updateLockedRobes(); static uintptr_t gameAssemblyBase(); static size_t gameAssemblySize(); }; diff --git a/src/patches.cpp b/src/patches.cpp index 5196dbf..4438081 100644 --- a/src/patches.cpp +++ b/src/patches.cpp @@ -13,12 +13,18 @@ namespace devour { #ifdef _WIN32 using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* method); +using UpdateLockedFn = void(__fastcall*)(void* thiz, void* method); static IsUnlockedFn oIsRobe = nullptr; static IsUnlockedFn oIsChar = nullptr; +static IsUnlockedFn oHasPerk = nullptr; +static IsUnlockedFn oHasOutfit = 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 @@ -58,6 +64,68 @@ static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { return true; } +static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { + auto& cfg = Config::get(); + if (!cfg.unlockPerks) return oHasPerk ? oHasPerk(thiz, perkStr, method) : false; + std::string name = il2cppStringToUtf8(perkStr); + auto it = cfg.perkOverrides.find(name); + if (it != cfg.perkOverrides.end()) return it->second; + return true; +} +static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* method) { + auto& cfg = Config::get(); + if (!cfg.unlockOutfits) return oHasOutfit ? oHasOutfit(thiz, outfitStr, method) : false; + std::string name = il2cppStringToUtf8(outfitStr); + auto it = cfg.outfitOverrides.find(name); + if (it != cfg.outfitOverrides.end()) return it->second; + return true; +} + +// UpdateLocked* hooks: force to mark all as unlocked, then skip original caller that sets locked flag +static void __fastcall hUpdateLockedPets(void* thiz, void* method) { + auto& cfg = Config::get(); + if (!cfg.unlockPets) { + // fall through to original — we need original ptr; use hookUpdateLocked helper instead + return; + } + std::cout << "[DevourUnlocker] UpdateLockedPets bypassed (pets unlocked)\n"; + // Don't call original — it would set locked visuals. Instead we directly ensure inventory shows unlocked. + // Original is void, so just return. The UI will consider all pets unlocked because HasPet/GetPet hooks would return true. +} +static void __fastcall hUpdateLockedPerks(void* thiz, void* method) { + if (!Config::get().unlockPerks) return; + std::cout << "[DevourUnlocker] UpdateLockedPerks bypassed\n"; +} +static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { + if (!Config::get().unlockFlashlights) return; + std::cout << "[DevourUnlocker] UpdateLockedFlashlights bypassed\n"; +} +static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { + if (!Config::get().unlockEmotes) return; + std::cout << "[DevourUnlocker] UpdateLockedEmotes bypassed\n"; +} +static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { + if (!Config::get().unlockOutfits) return; + std::cout << "[DevourUnlocker] UpdateLockedOutfits bypassed\n"; +} +static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { + if (!Config::get().unlockRobes) return; + std::cout << "[DevourUnlocker] UpdateLockedRobes bypassed\n"; +} + +static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*)) { + if (!addr) { std::cerr << "[DevourUnlocker] " << name << " not found\n"; return false; } + if (!shouldUnlock) return false; + void* orig = nullptr; + if (MH_CreateHook(addr, (void*)hook, &orig) == MH_OK) { + MH_EnableHook(addr); + std::cout << "[DevourUnlocker] Hooked " << name << "\n"; + return true; + } + std::cerr << "[DevourUnlocker] Hook " << name << " failed\n"; + return false; +} + bool installPatches() { if (g_installed) return true; if (!g_mhInit) { @@ -73,8 +141,20 @@ bool installPatches() { } 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(); std::cout << "[DevourUnlocker] Resolved IsRobeUnlocked=" << robe.address - << " IsCharacterUnlocked=" << ch.address << "\n"; + << " 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 << "\n"; bool any = false; if (robe.valid()) { @@ -93,6 +173,26 @@ bool installPatches() { any = true; } else std::cerr << "[DevourUnlocker] Hook IsChar failed\n"; } + if (hasPerk.valid()) { + if (MH_CreateHook(hasPerk.address, (void*)hHasPerk, (void**)&oHasPerk) == MH_OK) { + MH_EnableHook(hasPerk.address); + std::cout << "[DevourUnlocker] Hooked HasPerk\n"; + any = true; + } + } + if (hasOutfit.valid()) { + if (MH_CreateHook(hasOutfit.address, (void*)hHasOutfit, (void**)&oHasOutfit) == MH_OK) { + MH_EnableHook(hasOutfit.address); + std::cout << "[DevourUnlocker] Hooked HasOutfit\n"; + any = true; + } + } + 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); g_installed = any; return any; } diff --git a/standalone/devour_unlocker.py b/standalone/devour_unlocker.py index 48fc82e..ec3711b 100755 --- a/standalone/devour_unlocker.py +++ b/standalone/devour_unlocker.py @@ -19,6 +19,17 @@ ROBES = [ "Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare" ] CHARS = ["Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"] +PERKS = [ + "Acceleration","Aftershock","Conservationist","Crawler","DuracellBunny","Evader", + "FastWorker","FieldMedic","FirstResponder","FullyCharged","Immune","Inspired", + "Keymaster","LongSighted","Patient","PowerfulInsight","Quartermaster","Regeneration", + "Repellent","Speedy","SpeedWalker","StayBack","SupplyRunner","Tamed","Teamwork", + "TeamLeader","Trapper","UnderPressure","CrawlerVent" +] +PETS = ["Crow","Pig","Goat","Rat","Spider","Cat","Pug"] +FLASHLIGHTS = ["Basic","UV","Battery","Gold","Carnival","Manor","Hospital","Infinity"] +EMOTES = ["Wave","Dance","Sit","Point","Laugh","Cry","Angry","Cheer","Clap","Taunt","Shrug","Bow"] +OUTFITS = ["Default","Formal","Casual","Winter","Summer","Anniversary","Hard","Nightmare"] def devour_paths(): paths = [] @@ -52,7 +63,7 @@ def default_json_path(): def load_config(path): if pathlib.Path(path).exists(): return json.loads(pathlib.Path(path).read_text()) - return {"unlockRobes": True, "unlockCharacters": True, "unlockPerks": True, "unlockFlashlights": True, "unlockEmotes": True, "unlockPets": True} + return {"unlockRobes": True, "unlockCharacters": True, "unlockPerks": True, "unlockFlashlights": True, "unlockEmotes": True, "unlockPets": True, "unlockOutfits": True} def save_config(path, cfg): pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) @@ -60,10 +71,13 @@ def save_config(path, cfg): print(f"[+] Wrote {path}") def cmd_list(_): - print("Robes:") - for r in ROBES: print(f" {r}") - print("Characters:") - for c in CHARS: print(f" {c}") + print("Robes:"); [print(f" {r}") for r in ROBES] + print("Characters:"); [print(f" {c}") for c in CHARS] + print("Perks:"); [print(f" {p}") for p in PERKS] + print("Pets:"); [print(f" {p}") for p in PETS] + print("Flashlights:"); [print(f" {f}") for f in FLASHLIGHTS] + print("Emotes:"); [print(f" {e}") for e in EMOTES] + print("Outfits:"); [print(f" {o}") for o in OUTFITS] print("\nPaths checked:") for p in devour_paths(): print(f" {p} {'[EXISTS]' if p.exists() else ''}") @@ -80,23 +94,40 @@ def cmd_status(args): def cmd_unlock(args): cfg = load_config(args.json) - sel_robes = args.robes.split(",") if args.robes else ROBES - sel_chars = args.chars.split(",") if args.chars else CHARS + sel_robes = args.robes.split(",") if args.robes else [] + sel_chars = args.chars.split(",") if args.chars else [] + sel_perks = args.perks.split(",") if args.perks else [] + sel_pets = args.pets.split(",") if args.pets else [] if args.all: - sel_robes, sel_chars = ROBES, CHARS - # robeOverrides: true for selected, false for others if --exclusive - cfg["unlockRobes"] = True - cfg["unlockCharacters"] = True + cfg.update({"unlockRobes":True,"unlockCharacters":True,"unlockPerks":True,"unlockPets":True,"unlockFlashlights":True,"unlockEmotes":True,"unlockOutfits":True}) + # clear overrides — global true covers all + for k in ["robeOverrides","characterOverrides","perkOverrides","petOverrides","flashlightOverrides","emoteOverrides","outfitOverrides"]: + cfg.pop(k,None) + save_config(args.json, cfg) + print("[*] Unlocked ALL categories (robes/chars/perks/pets/flashlights/emotes/outfits)") + return + # selective + cfg["unlockRobes"] = cfg["unlockCharacters"] = cfg["unlockPerks"] = cfg["unlockPets"] = cfg["unlockFlashlights"] = cfg["unlockEmotes"] = cfg["unlockOutfits"] = True if args.exclusive: cfg["robeOverrides"] = {r: (r in sel_robes) for r in ROBES} cfg["characterOverrides"] = {c: (c in sel_chars) for c in CHARS} + cfg["perkOverrides"] = {p: (p in sel_perks) for p in PERKS} + cfg["petOverrides"] = {p: (p in sel_pets) for p in PETS} else: - cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes}) - cfg.setdefault("characterOverrides", {}).update({c: True for c in sel_chars}) + if sel_robes: cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes}) + if sel_chars: cfg.setdefault("characterOverrides", {}).update({c: True for c in sel_chars}) + if sel_perks: cfg.setdefault("perkOverrides", {}).update({p: True for p in sel_perks}) + if sel_pets: cfg.setdefault("petOverrides", {}).update({p: True for p in sel_pets}) + # if no args, just ensure globals true + if not any([sel_robes, sel_chars, sel_perks, sel_pets]): + pass if args.lock: - cfg["unlockRobes"] = cfg["unlockCharacters"] = False + for k in ["unlockRobes","unlockCharacters","unlockPerks","unlockPets","unlockFlashlights","unlockEmotes","unlockOutfits"]: + cfg[k]=False cfg["robeOverrides"] = {r: False for r in ROBES} cfg["characterOverrides"] = {c: False for c in CHARS} + cfg["perkOverrides"] = {p: False for p in PERKS} + cfg["petOverrides"] = {p: False for p in PETS} save_config(args.json, cfg) print("[*] Now launch Devour with winhttp.dll — selective unlock active via DevourUnlocker.json") @@ -118,6 +149,8 @@ def main(): su = sub.add_parser("unlock", help="select what to unlock (writes json for mod)") su.add_argument("--robes", help="comma list e.g. StraightJacket,Golden") su.add_argument("--chars", help="comma list e.g. Molly,Zara") + su.add_argument("--perks", help="comma list e.g. Speedy,Patient") + su.add_argument("--pets", help="comma list e.g. Crow,Pig") su.add_argument("--all", action="store_true", help="unlock all") su.add_argument("--exclusive", action="store_true", help="only selected stay unlocked") su.add_argument("--lock", action="store_true", help="lock all") diff --git a/standalone/gui.py b/standalone/gui.py index 756a7f0..a2cde97 100755 --- a/standalone/gui.py +++ b/standalone/gui.py @@ -3,7 +3,7 @@ import tkinter as tk from tkinter import ttk, messagebox import json, pathlib -from devour_unlocker import ROBES, CHARS, default_json_path, load_config, save_config +from devour_unlocker import ROBES, CHARS, PERKS, PETS, FLASHLIGHTS, EMOTES, OUTFITS, default_json_path, load_config, save_config class App(tk.Tk): def __init__(self): @@ -25,7 +25,7 @@ class App(tk.Tk): ttk.Button(btn_row, text="Reload", command=self.reload).pack(side=tk.LEFT, padx=4) nb = ttk.Notebook(self); nb.pack(fill=tk.BOTH, expand=True, padx=8, pady=8) - for title, items in [("Robes", ROBES), ("Characters", CHARS)]: + for title, items in [("Robes", ROBES), ("Characters", CHARS), ("Perks", PERKS), ("Pets", PETS), ("Flashlights", FLASHLIGHTS), ("Emotes", EMOTES), ("Outfits", OUTFITS)]: f = ttk.Frame(nb) nb.add(f, text=title) canvas = tk.Canvas(f) @@ -37,7 +37,9 @@ class App(tk.Tk): canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll.pack(side=tk.RIGHT, fill=tk.Y) for name in items: - cur = self.cfg.get("robeOverrides" if title=="Robes" else "characterOverrides", {}).get(name, self.cfg.get("unlockRobes" if title=="Robes" else "unlockCharacters", True)) + key_map={"Robes":("robeOverrides","unlockRobes"),"Characters":("characterOverrides","unlockCharacters"),"Perks":("perkOverrides","unlockPerks"),"Pets":("petOverrides","unlockPets"),"Flashlights":("flashlightOverrides","unlockFlashlights"),"Emotes":("emoteOverrides","unlockEmotes"),"Outfits":("outfitOverrides","unlockOutfits")} + k, g = key_map[title] + cur = self.cfg.get(k, {}).get(name, self.cfg.get(g, True)) var = tk.BooleanVar(value=bool(cur)) self.vars[(title,name)] = var ttk.Checkbutton(inner, text=name, variable=var).pack(anchor="w", padx=8, pady=2) @@ -50,17 +52,18 @@ class App(tk.Tk): self.cfg = load_config(str(self.json_path)) self.destroy(); self.__init__() def save(self): - robe_over = {n: self.vars[("Robes",n)].get() for n in ROBES} - char_over = {n: self.vars[("Characters",n)].get() for n in CHARS} - # compress: if all same, use global - all_r = all(robe_over.values()); none_r = not any(robe_over.values()) - if all_r: self.cfg["unlockRobes"]=True; self.cfg.pop("robeOverrides",None) - elif none_r: self.cfg["unlockRobes"]=False; self.cfg.pop("robeOverrides",None) - else: self.cfg["robeOverrides"]=robe_over; self.cfg["unlockRobes"]=True - all_c = all(char_over.values()); none_c = not any(char_over.values()) - if all_c: self.cfg["unlockCharacters"]=True; self.cfg.pop("characterOverrides",None) - elif none_c: self.cfg["unlockCharacters"]=False; self.cfg.pop("characterOverrides",None) - else: self.cfg["characterOverrides"]=char_over; self.cfg["unlockCharacters"]=True + def compress(title, items, k, g): + over = {n: self.vars[(title,n)].get() for n in items} + if all(over.values()): self.cfg[g]=True; self.cfg.pop(k,None) + elif not any(over.values()): self.cfg[g]=False; self.cfg.pop(k,None) + else: self.cfg[k]=over; self.cfg[g]=True + compress("Robes", ROBES, "robeOverrides", "unlockRobes") + compress("Characters", CHARS, "characterOverrides", "unlockCharacters") + compress("Perks", PERKS, "perkOverrides", "unlockPerks") + compress("Pets", PETS, "petOverrides", "unlockPets") + compress("Flashlights", FLASHLIGHTS, "flashlightOverrides", "unlockFlashlights") + compress("Emotes", EMOTES, "emoteOverrides", "unlockEmotes") + compress("Outfits", OUTFITS, "outfitOverrides", "unlockOutfits") save_config(str(self.json_path), self.cfg) messagebox.showinfo("Saved", f"Wrote {self.json_path}\nLaunch Devour with winhttp.dll enabled.")