v2.0.1: expand to all item types + fix pets + linux CI
Some checks are pending
Build DevourUnlocker / windows-cross (push) Waiting to run
Build DevourUnlocker / linux (push) Waiting to run
Build DevourUnlocker / standalone (push) Waiting to run

- Config: add allPets (Crow/Pig/Goat/Rat/Spider/Cat/Pug), allPerks (29 inc Acceleration/Quartermaster etc), flashlights/emotes/outfits; unlockOutfits global
- Resolver: resolve HasPerk/HasPet/UpdateLocked* for Pets/Perks/Flashlights/Emotes/Outfits/Robes
- Patches: hook IsRobe/IsCharacter + HasPerk/HasOutfit + UpdateLockedPets/Perks/Flashlights/Emotes/Outfits/Robes bypass — fixes pets not unlocking
- GUI: 7 tabs (Robes/Chars/Perks/Pets/Flashlights/Emotes/Outfits), Unlock All covers all categories
- Standalone: --perks/--pets/--all now covers all categories, petOverrides etc, list shows 30 robes+9 chars+29 perks+7 pets+8 flashlights+12 emotes+8 outfits
- GUI standalone: 7 notebooks with per-category checkboxes
- BepInEx: add UpdateLocked* prefix patches + HasPerk/HasOutfit, GUI tabs for all
- CI: switch windows-latest to ubuntu-latest mingw cross (git.mithraic.cloud has only linux runner, windows jobs were stuck Waiting)
This commit is contained in:
MiTHRAL 2026-08-21 21:15:58 -04:00
parent 422f47f25c
commit 4af18a2f75
10 changed files with 337 additions and 88 deletions

View file

@ -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

View file

@ -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<string,bool> robeOverrides = new();
public static System.Collections.Generic.Dictionary<string,bool> charOverrides = new();
public static System.Collections.Generic.Dictionary<string,bool> perkOverrides = new();
public static System.Collections.Generic.Dictionary<string,bool> 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();
}
}

View file

@ -37,6 +37,28 @@ std::vector<std::string> Config::allRobes() {
std::vector<std::string> Config::allCharacters() {
return {"Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"};
}
std::vector<std::string> 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<std::string> Config::allPets() {
return {"Crow","Pig","Goat","Rat","Spider","Cat","Pug"};
}
std::vector<std::string> Config::allFlashlights() {
return {"Basic","UV","Battery","Gold","Carnival","Manor","Hospital","Infinity"};
}
std::vector<std::string> Config::allEmotes() {
return {"Wave","Dance","Sit","Point","Laugh","Cry","Angry","Cheer","Clap","Taunt","Shrug","Bow"};
}
std::vector<std::string> 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;

View file

@ -17,6 +17,13 @@ struct UnlockConfig {
// Per-item overrides (empty = use global). Key is internal name e.g. "StraightJacket"
std::unordered_map<std::string, bool> robeOverrides;
std::unordered_map<std::string, bool> characterOverrides;
std::unordered_map<std::string, bool> perkOverrides;
std::unordered_map<std::string, bool> petOverrides;
std::unordered_map<std::string, bool> flashlightOverrides;
std::unordered_map<std::string, bool> emoteOverrides;
std::unordered_map<std::string, bool> outfitOverrides;
bool unlockOutfits = true;
// UI
bool guiVisible = true;
@ -31,6 +38,11 @@ public:
static std::string defaultPath();
static std::vector<std::string> allRobes();
static std::vector<std::string> allCharacters();
static std::vector<std::string> allPerks();
static std::vector<std::string> allPets();
static std::vector<std::string> allFlashlights();
static std::vector<std::string> allEmotes();
static std::vector<std::string> allOutfits();
};
} // namespace devour

View file

@ -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<std::string> 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());

View file

@ -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

View file

@ -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();
};

View file

@ -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;
}

View file

@ -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")

View file

@ -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.")