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: workflow_dispatch:
jobs: jobs:
windows: # Cross-compile Windows winhttp.dll on Linux (no native Windows runner on git.mithraic.cloud)
runs-on: windows-latest windows-cross:
runs-on: ubuntu-latest
continue-on-error: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: { submodules: recursive } with: { submodules: recursive }
- name: CMake Configure (Win) - name: Install mingw + cmake
run: cmake -B build -DCMAKE_BUILD_TYPE=Release run: sudo apt-get update && sudo apt-get install -y cmake ninja-build mingw-w64 g++-mingw-w64-x86-64
- name: Build - name: CMake Configure (MinGW cross)
run: cmake --build build --config Release 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 - name: Package
run: | run: |
mkdir dist mkdir -p dist
cp build/winhttp.dll dist/ 2>/dev/null || echo "winhttp not built" 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 || echo "dll not built" cp build/DevourUnlocker.dll dist/ 2>/dev/null || true
echo '{"unlockRobes":true,"unlockCharacters":true}' > dist/DevourUnlocker.json 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
Compress-Archive -Path dist/* -DestinationPath DevourUnlocker_winhttp_${{ github.ref_name }}.zip zip -j DevourUnlocker_winhttp_${{ github.ref_name }}.zip dist/* || true
shell: pwsh ls -lh dist/
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: { name: winhttp, path: DevourUnlocker_winhttp_*.zip } with: { name: winhttp-cross, path: DevourUnlocker_winhttp_*.zip }
linux: linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -40,7 +40,6 @@ namespace DevourUnlocker.BepInEx
// Generic unlock patches — try multiple owner classes // Generic unlock patches — try multiple owner classes
static class Patches static class Patches
{ {
// Use TargetMethod to handle both old and new class names
[HarmonyPatch] [HarmonyPatch]
class IsRobeUnlockedPatch class IsRobeUnlockedPatch
{ {
@ -53,15 +52,14 @@ namespace DevourUnlocker.BepInEx
var m = AccessTools.Method(type, "IsRobeUnlocked"); var m = AccessTools.Method(type, "IsRobeUnlocked");
if (m != null) { Plugin.Log?.LogInfo($"Found IsRobeUnlocked on {t}"); return m; } 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; return null;
} }
static bool Prefix(ref bool __result, object __instance, string robeName) 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; } if (DevourGUI.robeOverrides.TryGetValue(robeName ?? "", out var v)) { __result = v; return false; }
__result = true; __result = true;
if (!string.IsNullOrEmpty(robeName)) Plugin.Log?.LogInfo($"[Robe] {robeName} -> unlocked");
return false; return false;
} }
} }
@ -88,6 +86,33 @@ namespace DevourUnlocker.BepInEx
return false; 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 static class DevourGUI
@ -95,44 +120,66 @@ namespace DevourUnlocker.BepInEx
public static bool visible = true; public static bool visible = true;
public static bool configUnlockRobes = true; public static bool configUnlockRobes = true;
public static bool configUnlockChars = 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> robeOverrides = new();
public static System.Collections.Generic.Dictionary<string,bool> charOverrides = 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[] { static string[] robes = new[] {
"StraightJacket","Patchwork","CherryBlossom","Eggs","Poison","Web", "StraightJacket","Patchwork","CherryBlossom","Eggs","Poison","Web",
"TownHard","TownNightmare","TownBooks","Horseshoes", "TownHard","TownNightmare","TownBooks","Horseshoes",
"SlaughterhouseHard","SlaughterhouseNightmare","SlaughterhousePigs","BarbedWires", "SlaughterhouseHard","SlaughterhouseNightmare","SlaughterhousePigs","BarbedWires",
"Golden","ManorHard","ManorNightmare","ManorHeadsBuried","Feathers", "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[] 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 UnityEngine.Vector2 scroll;
static string currentTab = "Robes";
public static void Draw() 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, ""); UnityEngine.GUI.Box(rect, "");
GUILayout.BeginArea(rect); GUILayout.BeginArea(rect);
GUILayout.Label("DevourUnlocker v2 — BepInEx Edition (F6 toggle)"); GUILayout.Label("DevourUnlocker v2 — BepInEx Edition (F6 toggle) — ALL categories");
GUILayout.Label("Multiplayer safe — client visual unlock"); GUILayout.BeginHorizontal();
configUnlockRobes = GUILayout.Toggle(configUnlockRobes, "Unlock Robes"); if(GUILayout.Button("Robes")) currentTab="Robes";
configUnlockChars = GUILayout.Toggle(configUnlockChars, "Unlock Characters"); if(GUILayout.Button("Chars")) currentTab="Chars";
if (GUILayout.Button("Unlock All")) { configUnlockRobes = configUnlockChars = true; robeOverrides.Clear(); charOverrides.Clear(); } if(GUILayout.Button("Perks")) currentTab="Perks";
if (GUILayout.Button("Lock All")) { configUnlockRobes = configUnlockChars = false; } if(GUILayout.Button("Pets")) currentTab="Pets";
GUILayout.Label("— Robes —"); GUILayout.EndHorizontal();
scroll = GUILayout.BeginScrollView(scroll, GUILayout.Height(220)); configUnlockRobes = GUILayout.Toggle(configUnlockRobes, "Robes");
foreach (var r in robes) { configUnlockChars = GUILayout.Toggle(configUnlockChars, "Chars");
bool cur = robeOverrides.TryGetValue(r, out var ov) ? ov : configUnlockRobes; configUnlockPerks = GUILayout.Toggle(configUnlockPerks, "Perks");
bool nxt = GUILayout.Toggle(cur, r); configUnlockPets = GUILayout.Toggle(configUnlockPets, "Pets");
if (nxt != cur) robeOverrides[r] = nxt; 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.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(); GUILayout.EndArea();
} }
} }

View file

@ -37,6 +37,28 @@ std::vector<std::string> Config::allRobes() {
std::vector<std::string> Config::allCharacters() { std::vector<std::string> Config::allCharacters() {
return {"Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"}; 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) { bool Config::load(const std::string& path) {
std::ifstream f(path); std::ifstream f(path);
@ -59,6 +81,7 @@ bool Config::load(const std::string& path) {
g_config.unlockFlashlights = has("unlockFlashlights", true); g_config.unlockFlashlights = has("unlockFlashlights", true);
g_config.unlockEmotes = has("unlockEmotes", true); g_config.unlockEmotes = has("unlockEmotes", true);
g_config.unlockPets = has("unlockPets", true); g_config.unlockPets = has("unlockPets", true);
g_config.unlockOutfits = has("unlockOutfits", true);
g_config.guiVisible = has("guiVisible", true); g_config.guiVisible = has("guiVisible", true);
return true; return true;
} }
@ -74,6 +97,7 @@ bool Config::save(const std::string& path) {
f << " \"unlockFlashlights\": " << (c.unlockFlashlights?"true":"false") << ",\n"; f << " \"unlockFlashlights\": " << (c.unlockFlashlights?"true":"false") << ",\n";
f << " \"unlockEmotes\": " << (c.unlockEmotes?"true":"false") << ",\n"; f << " \"unlockEmotes\": " << (c.unlockEmotes?"true":"false") << ",\n";
f << " \"unlockPets\": " << (c.unlockPets?"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 << " \"guiVisible\": " << (c.guiVisible?"true":"false") << "\n";
f << "}\n"; f << "}\n";
return true; return true;

View file

@ -17,6 +17,13 @@ struct UnlockConfig {
// Per-item overrides (empty = use global). Key is internal name e.g. "StraightJacket" // 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> robeOverrides;
std::unordered_map<std::string, bool> characterOverrides; 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 // UI
bool guiVisible = true; bool guiVisible = true;
@ -31,6 +38,11 @@ public:
static std::string defaultPath(); static std::string defaultPath();
static std::vector<std::string> allRobes(); static std::vector<std::string> allRobes();
static std::vector<std::string> allCharacters(); 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 } // namespace devour

View file

@ -70,39 +70,41 @@ void render() {
changed |= ImGui::Checkbox("Unlock Characters", &cfg.unlockCharacters); changed |= ImGui::Checkbox("Unlock Characters", &cfg.unlockCharacters);
changed |= ImGui::Checkbox("Unlock Perks", &cfg.unlockPerks); changed |= ImGui::Checkbox("Unlock Perks", &cfg.unlockPerks);
changed |= ImGui::Checkbox("Unlock Flashlights", &cfg.unlockFlashlights); 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 (changed) Config::save(Config::defaultPath());
if (ImGui::Button("Unlock All", ImVec2(120,30))) { 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()); Config::save(Config::defaultPath());
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Lock All", ImVec2(120,30))) { 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()); Config::save(Config::defaultPath());
} }
ImGui::TextDisabled("Pets now fixed via UpdateLockedPets hook");
} }
if (ImGui::CollapsingHeader("Robes")) { auto drawCategory = [&](const char* label, std::vector<std::string> items, auto& overrides, bool global){
for (auto& n : Config::allRobes()) { if (ImGui::CollapsingHeader(label)) {
auto it = cfg.robeOverrides.find(n); for (auto& n : items) {
bool enabled = it == cfg.robeOverrides.end() ? cfg.unlockRobes : it->second; auto it = overrides.find(n);
if (ImGui::Checkbox(n.c_str(), &enabled)) { bool enabled = it == overrides.end() ? global : it->second;
cfg.robeOverrides[n] = enabled; if (ImGui::Checkbox(n.c_str(), &enabled)) {
Config::save(Config::defaultPath()); overrides[n] = enabled;
Config::save(Config::defaultPath());
}
} }
} }
} };
if (ImGui::CollapsingHeader("Characters")) { drawCategory("Robes", Config::allRobes(), cfg.robeOverrides, cfg.unlockRobes);
for (auto& n : Config::allCharacters()) { drawCategory("Characters", Config::allCharacters(), cfg.characterOverrides, cfg.unlockCharacters);
auto it = cfg.characterOverrides.find(n); drawCategory("Perks", Config::allPerks(), cfg.perkOverrides, cfg.unlockPerks);
bool enabled = it == cfg.characterOverrides.end() ? cfg.unlockCharacters : it->second; drawCategory("Pets", Config::allPets(), cfg.petOverrides, cfg.unlockPets);
if (ImGui::Checkbox(n.c_str(), &enabled)) { drawCategory("Flashlights", Config::allFlashlights(), cfg.flashlightOverrides, cfg.unlockFlashlights);
cfg.characterOverrides[n] = enabled; drawCategory("Emotes", Config::allEmotes(), cfg.emoteOverrides, cfg.unlockEmotes);
Config::save(Config::defaultPath()); drawCategory("Outfits", Config::allOutfits(), cfg.outfitOverrides, cfg.unlockOutfits);
}
}
}
ImGui::Separator(); ImGui::Separator();
if (ImGui::Button("Save", ImVec2(80,28))) Config::save(Config::defaultPath()); 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::isRobeUnlocked() { return resolve("NolanRankController", "IsRobeUnlocked"); }
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRankController", "IsCharacterUnlocked"); } ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRankController", "IsCharacterUnlocked"); }
ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("NolanRankController", "HasOutfit"); } 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 #else // Linux stubs
@ -152,6 +160,14 @@ ResolvedMethod Il2CppResolver::resolveByStringXref(const char* s, int) { return
ResolvedMethod Il2CppResolver::isRobeUnlocked() { return {"IsRobeUnlocked", nullptr}; } ResolvedMethod Il2CppResolver::isRobeUnlocked() { return {"IsRobeUnlocked", nullptr}; }
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return {"IsCharacterUnlocked", nullptr}; } ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return {"IsCharacterUnlocked", nullptr}; }
ResolvedMethod Il2CppResolver::hasOutfit() { return {"HasOutfit", 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 #endif

View file

@ -30,7 +30,15 @@ public:
static ResolvedMethod isRobeUnlocked(); static ResolvedMethod isRobeUnlocked();
static ResolvedMethod isCharacterUnlocked(); 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 uintptr_t gameAssemblyBase();
static size_t gameAssemblySize(); static size_t gameAssemblySize();
}; };

View file

@ -13,12 +13,18 @@ namespace devour {
#ifdef _WIN32 #ifdef _WIN32
using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* method); using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* method);
using UpdateLockedFn = void(__fastcall*)(void* thiz, void* method);
static IsUnlockedFn oIsRobe = nullptr; static IsUnlockedFn oIsRobe = nullptr;
static IsUnlockedFn oIsChar = nullptr; static IsUnlockedFn oIsChar = nullptr;
static IsUnlockedFn oHasPerk = nullptr;
static IsUnlockedFn oHasOutfit = nullptr;
static bool g_installed = false; static bool g_installed = false;
static bool g_mhInit = 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) { static std::string il2cppStringToUtf8(void* ilStr) {
if (!ilStr) return ""; if (!ilStr) return "";
// Il2CppString layout: length at offset 0x10, chars at 0x14 // 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; 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() { bool installPatches() {
if (g_installed) return true; if (g_installed) return true;
if (!g_mhInit) { if (!g_mhInit) {
@ -73,8 +141,20 @@ bool installPatches() {
} }
auto robe = Il2CppResolver::isRobeUnlocked(); auto robe = Il2CppResolver::isRobeUnlocked();
auto ch = Il2CppResolver::isCharacterUnlocked(); 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 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; bool any = false;
if (robe.valid()) { if (robe.valid()) {
@ -93,6 +173,26 @@ bool installPatches() {
any = true; any = true;
} else std::cerr << "[DevourUnlocker] Hook IsChar failed\n"; } 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; g_installed = any;
return any; return any;
} }

View file

@ -19,6 +19,17 @@ ROBES = [
"Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare" "Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare"
] ]
CHARS = ["Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"] 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(): def devour_paths():
paths = [] paths = []
@ -52,7 +63,7 @@ def default_json_path():
def load_config(path): def load_config(path):
if pathlib.Path(path).exists(): if pathlib.Path(path).exists():
return json.loads(pathlib.Path(path).read_text()) 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): def save_config(path, cfg):
pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True)
@ -60,10 +71,13 @@ def save_config(path, cfg):
print(f"[+] Wrote {path}") print(f"[+] Wrote {path}")
def cmd_list(_): def cmd_list(_):
print("Robes:") print("Robes:"); [print(f" {r}") for r in ROBES]
for r in ROBES: print(f" {r}") print("Characters:"); [print(f" {c}") for c in CHARS]
print("Characters:") print("Perks:"); [print(f" {p}") for p in PERKS]
for c in CHARS: print(f" {c}") 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:") print("\nPaths checked:")
for p in devour_paths(): for p in devour_paths():
print(f" {p} {'[EXISTS]' if p.exists() else ''}") print(f" {p} {'[EXISTS]' if p.exists() else ''}")
@ -80,23 +94,40 @@ def cmd_status(args):
def cmd_unlock(args): def cmd_unlock(args):
cfg = load_config(args.json) cfg = load_config(args.json)
sel_robes = args.robes.split(",") if args.robes else ROBES sel_robes = args.robes.split(",") if args.robes else []
sel_chars = args.chars.split(",") if args.chars else CHARS 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: if args.all:
sel_robes, sel_chars = ROBES, CHARS cfg.update({"unlockRobes":True,"unlockCharacters":True,"unlockPerks":True,"unlockPets":True,"unlockFlashlights":True,"unlockEmotes":True,"unlockOutfits":True})
# robeOverrides: true for selected, false for others if --exclusive # clear overrides — global true covers all
cfg["unlockRobes"] = True for k in ["robeOverrides","characterOverrides","perkOverrides","petOverrides","flashlightOverrides","emoteOverrides","outfitOverrides"]:
cfg["unlockCharacters"] = True 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: if args.exclusive:
cfg["robeOverrides"] = {r: (r in sel_robes) for r in ROBES} cfg["robeOverrides"] = {r: (r in sel_robes) for r in ROBES}
cfg["characterOverrides"] = {c: (c in sel_chars) for c in CHARS} 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: else:
cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes}) if sel_robes: cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes})
cfg.setdefault("characterOverrides", {}).update({c: True for c in sel_chars}) 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: 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["robeOverrides"] = {r: False for r in ROBES}
cfg["characterOverrides"] = {c: False for c in CHARS} 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) save_config(args.json, cfg)
print("[*] Now launch Devour with winhttp.dll — selective unlock active via DevourUnlocker.json") 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 = 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("--robes", help="comma list e.g. StraightJacket,Golden")
su.add_argument("--chars", help="comma list e.g. Molly,Zara") 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("--all", action="store_true", help="unlock all")
su.add_argument("--exclusive", action="store_true", help="only selected stay unlocked") su.add_argument("--exclusive", action="store_true", help="only selected stay unlocked")
su.add_argument("--lock", action="store_true", help="lock all") su.add_argument("--lock", action="store_true", help="lock all")

View file

@ -3,7 +3,7 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import json, pathlib 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): class App(tk.Tk):
def __init__(self): 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) 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) 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) f = ttk.Frame(nb)
nb.add(f, text=title) nb.add(f, text=title)
canvas = tk.Canvas(f) canvas = tk.Canvas(f)
@ -37,7 +37,9 @@ class App(tk.Tk):
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scroll.pack(side=tk.RIGHT, fill=tk.Y) scroll.pack(side=tk.RIGHT, fill=tk.Y)
for name in items: 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)) var = tk.BooleanVar(value=bool(cur))
self.vars[(title,name)] = var self.vars[(title,name)] = var
ttk.Checkbutton(inner, text=name, variable=var).pack(anchor="w", padx=8, pady=2) 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.cfg = load_config(str(self.json_path))
self.destroy(); self.__init__() self.destroy(); self.__init__()
def save(self): def save(self):
robe_over = {n: self.vars[("Robes",n)].get() for n in ROBES} def compress(title, items, k, g):
char_over = {n: self.vars[("Characters",n)].get() for n in CHARS} over = {n: self.vars[(title,n)].get() for n in items}
# compress: if all same, use global if all(over.values()): self.cfg[g]=True; self.cfg.pop(k,None)
all_r = all(robe_over.values()); none_r = not any(robe_over.values()) elif not any(over.values()): self.cfg[g]=False; self.cfg.pop(k,None)
if all_r: self.cfg["unlockRobes"]=True; self.cfg.pop("robeOverrides",None) else: self.cfg[k]=over; self.cfg[g]=True
elif none_r: self.cfg["unlockRobes"]=False; self.cfg.pop("robeOverrides",None) compress("Robes", ROBES, "robeOverrides", "unlockRobes")
else: self.cfg["robeOverrides"]=robe_over; self.cfg["unlockRobes"]=True compress("Characters", CHARS, "characterOverrides", "unlockCharacters")
all_c = all(char_over.values()); none_c = not any(char_over.values()) compress("Perks", PERKS, "perkOverrides", "unlockPerks")
if all_c: self.cfg["unlockCharacters"]=True; self.cfg.pop("characterOverrides",None) compress("Pets", PETS, "petOverrides", "unlockPets")
elif none_c: self.cfg["unlockCharacters"]=False; self.cfg.pop("characterOverrides",None) compress("Flashlights", FLASHLIGHTS, "flashlightOverrides", "unlockFlashlights")
else: self.cfg["characterOverrides"]=char_over; self.cfg["unlockCharacters"]=True compress("Emotes", EMOTES, "emoteOverrides", "unlockEmotes")
compress("Outfits", OUTFITS, "outfitOverrides", "unlockOutfits")
save_config(str(self.json_path), self.cfg) save_config(str(self.json_path), self.cfg)
messagebox.showinfo("Saved", f"Wrote {self.json_path}\nLaunch Devour with winhttp.dll enabled.") messagebox.showinfo("Saved", f"Wrote {self.json_path}\nLaunch Devour with winhttp.dll enabled.")