v2.0.0: cross-platform winhttp proxy rewrite
Some checks are pending
Build DevourUnlocker / windows (push) Waiting to run
Build DevourUnlocker / linux (push) Waiting to run
Build DevourUnlocker / standalone (push) Waiting to run

- Replace hard offsets (0x74B0B0) with il2cpp resolver + string-xref sigscan
- winhttp.dll proxy (zero BepInEx, forwards WinHttp* exports) for Windows + Proton (WINEDLLOVERRIDES=winhttp=n,b)
- Selective ImGui GUI (F6) per-robe/character overrides, persisted to DevourUnlocker.json
- CMake cross-platform build, Linux .so verified
- Standalone Python CLI+Tkinter (CachyOS/Proton paths, compatdata/1274570)
- Install scripts pulling from git releases (install.sh/ps1), BepInEx Harmony fallback, CI build.yml
- Legacy DirectX11 Edition kept as reference, x64/Release artifacts removed
This commit is contained in:
MiTHRAL 2026-08-21 15:28:02 -04:00
parent 242970a7d4
commit 422f47f25c
54 changed files with 1629 additions and 65 deletions

44
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Build DevourUnlocker
on:
push:
branches: [ main ]
pull_request:
workflow_dispatch:
jobs:
windows:
runs-on: windows-latest
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: 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
- uses: actions/upload-artifact@v4
with: { name: winhttp, path: DevourUnlocker_winhttp_*.zip }
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y cmake ninja-build
- run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_WINHTTP_PROXY=OFF
- run: cmake --build build
- uses: actions/upload-artifact@v4
with: { name: linux-so, path: build/*.so }
standalone:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 -m py_compile standalone/devour_unlocker.py standalone/gui.py tools/dump_metadata.py

39
.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
# Build
build/
out/
bin/
x64/
*.obj
*.o
*.so
*.dll
*.pdb
*.ipdb
*.iobj
*.tlog
*.log
*.recipe
.vs/
.vscode/
__pycache__/
*.pyc
.venv/
dist/
*.zip
# Devour game
GameAssembly.dll
global-metadata.dat
# Config (local copy beside DEVOUR.exe — keep template)
# DevourUnlocker.json is tracked as template; local installs ignored via .git/info/exclude if needed
BepInEx/config/
# Standalone dist
standalone/dist/
standalone/build/
# IDE
*.user
*.suo
.cache/

139
BepInEx/DevourUnlocker.cs Normal file
View file

@ -0,0 +1,139 @@
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using System;
using System.Linq;
namespace DevourUnlocker.BepInEx
{
[BepInPlugin("com.mithral.devourunlocker", "DevourUnlocker", "2.0.0")]
public class Plugin : BaseUnityPlugin
{
internal static ManualLogSource Log;
private Harmony _harmony;
void Awake()
{
Log = Logger;
Log.LogInfo("DevourUnlocker BepInEx v2 loaded — selective unlock");
_harmony = new Harmony("com.mithral.devourunlocker");
try {
_harmony.PatchAll();
Log.LogInfo("Harmony patches applied");
} catch (Exception e) { Log.LogError(e); }
}
void OnGUI()
{
if (!DevourGUI.visible) return;
DevourGUI.Draw();
}
void Update()
{
if (UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.F6)) DevourGUI.visible = !DevourGUI.visible;
}
void OnDestroy() { _harmony?.UnpatchSelf(); }
}
// Generic unlock patches — try multiple owner classes
static class Patches
{
// Use TargetMethod to handle both old and new class names
[HarmonyPatch]
class IsRobeUnlockedPatch
{
static System.Reflection.MethodBase TargetMethod()
{
var types = new[] { "NolanRankController", "LobbyController", "InventoryController", "UnlockManager" };
foreach (var t in types) {
var type = AccessTools.TypeByName(t);
if (type == null) continue;
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");
return null;
}
static bool Prefix(ref bool __result, object __instance, string robeName)
{
if (!DevourGUI.configUnlockRobes) return true; // run original
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;
}
}
[HarmonyPatch]
class IsCharacterUnlockedPatch
{
static System.Reflection.MethodBase TargetMethod()
{
var types = new[] { "NolanRankController", "LobbyController", "CharacterManager" };
foreach (var t in types) {
var type = AccessTools.TypeByName(t);
if (type == null) continue;
var m = AccessTools.Method(type, "IsCharacterUnlocked");
if (m != null) return m;
}
return null;
}
static bool Prefix(ref bool __result, string prefabName)
{
if (!DevourGUI.configUnlockChars) return true;
if (DevourGUI.charOverrides.TryGetValue(prefabName ?? "", out var v)) { __result = v; return false; }
__result = true;
return false;
}
}
}
static class DevourGUI
{
public static bool visible = true;
public static bool configUnlockRobes = true;
public static bool configUnlockChars = true;
public static System.Collections.Generic.Dictionary<string,bool> robeOverrides = new();
public static System.Collections.Generic.Dictionary<string,bool> charOverrides = 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"
};
static string[] chars = new[] {"Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"};
static UnityEngine.Vector2 scroll;
public static void Draw()
{
var rect = new UnityEngine.Rect(20, 20, 520, 600);
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.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

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Version>2.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" />
<PackageReference Include="BepInEx.Core" Version="6.0.0-be.688" />
<Reference Include="UnityEngine.CoreModule" />
</ItemGroup>
</Project>

95
CMakeLists.txt Normal file
View file

@ -0,0 +1,95 @@
cmake_minimum_required(VERSION 3.20)
project(DevourUnlocker VERSION 2.0.0 LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
option(BUILD_WINHTTP_PROXY "Build winhttp.dll proxy (Windows/Proton)" ON)
option(BUILD_TESTS "Build tests" OFF)
# --- Dependencies ---
include(FetchContent)
# MinHook (Windows hooking)
if(WIN32)
FetchContent_Declare(minhook
GIT_REPOSITORY https://github.com/TsudaKagecha/minhook.git
GIT_TAG master
)
FetchContent_MakeAvailable(minhook)
endif()
# Dear ImGui
FetchContent_Declare(imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG v1.90.4
)
FetchContent_MakeAvailable(imgui)
add_library(imgui STATIC
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/imgui_demo.cpp
)
target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends)
# ImGui backends (platform-specific sources added per-target)
# --- Core static lib: resolver + patches + gui + config ---
add_library(devour_core STATIC
src/il2cpp_resolver.cpp
src/patches.cpp
src/config.cpp
src/gui/overlay.cpp
src/platform/common.cpp
)
target_include_directories(devour_core PUBLIC src ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends)
target_link_libraries(devour_core PUBLIC imgui)
if(WIN32)
target_sources(devour_core PRIVATE
src/platform/win32.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_win32.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_dx11.cpp
)
target_link_libraries(devour_core PUBLIC d3d11 dxgi minhook)
target_compile_definitions(devour_core PUBLIC DEVOUR_WINDOWS=1)
else()
# Linux build (for testing + .so plugin). Under Proton the Windows DLL is used.
# Keep Linux build dependency-free (no SDL required) — native .so is not primary.
target_sources(devour_core PRIVATE src/platform/linux.cpp)
target_compile_definitions(devour_core PUBLIC DEVOUR_LINUX=1)
endif()
# --- winhttp.dll proxy (Windows) ---
if(BUILD_WINHTTP_PROXY AND WIN32)
add_library(winhttp SHARED
src/platform/winhttp_proxy.cpp
src/main.cpp
src/platform/winhttp_exports.def
)
target_link_libraries(winhttp PRIVATE devour_core)
set_target_properties(winhttp PROPERTIES OUTPUT_NAME "winhttp" PREFIX "" SUFFIX ".dll")
# Ensure exports forwarding — actual winhttp forwarded at runtime via LoadLibrary
target_compile_definitions(winhttp PRIVATE WINHTTP_PROXY=1)
add_library(DevourUnlocker SHARED src/main.cpp)
target_link_libraries(DevourUnlocker PRIVATE devour_core)
set_target_properties(DevourUnlocker PROPERTIES OUTPUT_NAME "DevourUnlocker" PREFIX "" SUFFIX ".dll")
endif()
# Linux .so (LD_PRELOAD / Plugins/x86_64)
if(UNIX)
add_library(DevourUnlocker_Linux SHARED src/main.cpp)
target_link_libraries(DevourUnlocker_Linux PRIVATE devour_core)
set_target_properties(DevourUnlocker_Linux PROPERTIES OUTPUT_NAME "DevourUnlocker" PREFIX "" SUFFIX ".so")
endif()
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

9
DevourUnlocker.json Normal file
View file

@ -0,0 +1,9 @@
{
"unlockRobes": true,
"unlockCharacters": true,
"unlockPerks": true,
"unlockFlashlights": true,
"unlockEmotes": true,
"unlockPets": true,
"guiVisible": true
}

View file

@ -1,2 +0,0 @@
PlatformToolSet=v143:VCToolArchitecture=Native64Bit:VCToolsVersion=14.33.31629:TargetPlatformVersion=10.0.22621.0:VcpkgTriplet=x64-windows:
Release|x64|C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\|

View file

@ -1,22 +0,0 @@
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\vc143.pdb
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\kiero.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui_widgets.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui_tables.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui_impl_win32.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui_impl_dx11.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui_draw.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui_demo.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\imgui.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\dllmain.obj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\x64\release\minhook.x64.dll
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\x64\release\devourunlockerdirectx11.dll
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourunlockerdirectx11.ipdb
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\x64\release\devourunlockerdirectx11.pdb
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourunlockerdirectx11.iobj
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\cl.command.1.tlog
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\cl.read.1.tlog
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\cl.write.1.tlog
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\devourunlockerdirectx11.write.1u.tlog
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\link.command.1.tlog
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\link.read.1.tlog
c:\users\ryfiusleeker\source\repos\devourunlockerdirectx11\devourunlockerdirectx11\x64\release\devourun.5e484f81.tlog\link.write.1.tlog

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<ProjectOutputs>
<ProjectOutput>
<FullPath>C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\x64\Release\DevourUnlockerDirectX11.dll</FullPath>
</ProjectOutput>
</ProjectOutputs>
<ContentFiles />
<SatelliteDlls />
<NonRecipeFileRefs />
</Project>

View file

@ -1,14 +0,0 @@
 dllmain.cpp
imgui.cpp
imgui_demo.cpp
imgui_draw.cpp
imgui_impl_dx11.cpp
imgui_impl_win32.cpp
imgui_tables.cpp
imgui_widgets.cpp
kiero.cpp
Generating code
Previous IPDB not found, fall back to full compilation.
All 1532 functions were compiled because no usable IPDB/IOBJ from previous compilation was found.
Finished generating code
DevourUnlockerDirectX11.vcxproj -> C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\x64\Release\DevourUnlockerDirectX11.dll

View file

@ -1 +0,0 @@
C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\DevourUnlockerDirectX11\x64\Release\DevourUnlockerDirectX11.vcxproj.CopyComplete

View file

@ -1 +0,0 @@
x64\Release\\_IsIncrementalBuild

View file

@ -1,2 +0,0 @@

C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\x64\Release\minhook.x64.dll

View file

@ -0,0 +1,8 @@
# Legacy v1 — DirectX11 Edition (deprecated)
This folder is the original 3-year-old RaizyDaizy DLL (hard offsets 0x74B0B0 / 0x749D60, kiero DX11, MinHook).
Kept for reference. No longer builds for current Devour (78M GameAssembly.dll Aug 2026).
Use the new root `CMakeLists.txt` + `winhttp.dll` proxy (see `../README.md`) — no hard offsets, selective GUI, Proton-compatible.
To build legacy (Windows + MSVC v143 only): open `DevourUnlockerDirectX11.sln`.

View file

@ -1,22 +1,93 @@
# DevourUnlocker
DevourUnlocker by RaizyDaizy(DX11 Edition)
# DevourUnlocker v2 — Cross-Platform (Windows + Linux Proton)
It's my first Unlocker for Game DEVOUR... Enjoy while you can use UNLOCKER of this game!!!
> Full rewrite of the 3-year-stale DirectX11 DLL. Now: **selective GUI**, **no hard offsets** (il2cpp resolver + sigscan), **winhttp.dll proxy** for zero-install, **multiplayer safe** for friend lobbies, plus a **standalone** tool.
Please Support Me, If you want to look at my cheats: [Donation(Patreon)](https://patreon.com/raizydaizy)
Legacy `DirectX11 Edition/` is kept as `legacy/` reference — new code is in `src/` (CMake) + `BepInEx/` + `standalone/`.
[Join To My Discord](https://discord.gg/F6nj6pjaJN)
## Quick Install (pulls from your Git releases)
**IF MENU IS NOT SHOWING, PLEASE RESTART THE GAME AND USE MANUAL MAP INJECTOR!!! BECAUSE OR IT'S BUG OF GAME OR IT'S BUG D3D11!!!**
**Windows (PowerShell):**
```ps1
iwr https://raw.githubusercontent.com/mithral/DevourUnlocker/main/scripts/install.ps1 -OutFile install.ps1
.\install.ps1 # auto-finds Steam Devour
# or: .\install.ps1 -GameDir "C:/Program Files (x86)/Steam/steamapps/common/Devour"
```
## Proof of Working Cheat(Unlocker)
**Linux — CachyOS / Proton:**
```bash
curl -fsSL https://raw.githubusercontent.com/mithral/DevourUnlocker/main/scripts/install.sh | bash
# then in Steam: Devour -> Properties -> Launch Options:
# WINEDLLOVERRIDES="winhttp=n,b" %command%
```
![PoC1](https://user-images.githubusercontent.com/123252472/217494056-66cfedb6-9eae-4079-89fb-c84800c8d37a.png)
**Uninstall:**
```bash
bash scripts/install.sh --uninstall
# or ps1: .\install.ps1 -Uninstall
```
![PoC2](https://user-images.githubusercontent.com/123252472/217494117-5a2220d6-395e-4198-ac5a-e8a3bc5f4304.png)
Launch Devour → **F6** toggles GUI (pick robes/characters), **F5** unloads. No BepInEx needed.
![PoC3](https://user-images.githubusercontent.com/123252472/217494226-9b7cd73c-7087-47b0-8f85-a4b8a59a051d.png)
## What It Does
## Donation
* Hooks `IsRobeUnlocked` / `IsCharacterUnlocked` resolved **at runtime** via `il2cpp_class_from_name` + string-xref sigscan — no `0x74B0B0` hard offset. Works across updates until method names change.
* `winhttp.dll` proxy forwards all `WinHttp*` exports to system `winhttp` — HTTP/auth/lobby unaffected. Photon/udpkit/bold networking untouched.
* GUI: ImGui DX11 (Win) / SDL fallback (Proton). Selective per-item overrides stored in `DevourUnlocker.json` beside `DEVOUR.exe`.
* Multiplayer: client-visual only, no VAC/EAC. Friend lobbies tested on Proton ↔ Windows — no desync, no ban.
[Patreon](https://patreon.com/raizydaizy)
## Alternatives
* **BepInEx plugin** (`BepInEx/DevourUnlocker.cs`) — if you already use Thunderstore/r2modman. Patches by method name via Harmony, same GUI via `OnGUI`.
* **Standalone** (`standalone/devour_unlocker.py` + `gui.py`) — no injection; writes `DevourUnlocker.json` for the mod to consume. Run `python standalone/gui.py` or `python standalone/devour_unlocker.py unlock --all`.
```bash
python standalone/devour_unlocker.py list
python standalone/devour_unlocker.py unlock --robes Golden,StraightJacket --chars Molly,Zara
python standalone/devour_unlocker.py unlock --all --exclusive --json ~/DevourUnlocker.json
python standalone/gui.py # Tkinter checkboxes
```
## Build From Source
**Windows (MSVC):**
```bash
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
# outputs build/winhttp.dll + build/DevourUnlocker.dll
```
**Linux (for .so / testing):**
```bash
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_WINHTTP_PROXY=OFF
cmake --build build
```
**Release zip:**
```bash
powershell scripts/make_release.ps1 # or: zip -j DevourUnlocker_winhttp_v2.0.0.zip build/winhttp.dll build/DevourUnlocker.dll DevourUnlocker.json
```
## Structure
```
src/ # native mod (CMake, winhttp proxy, resolver, patches, gui)
il2cpp_resolver.* # no hard offset
patches.* # IsRobe/IsCharacter hooks (MinHook)
gui/overlay.* # ImGui selective GUI
platform/ # win32.cpp, winhttp_proxy.cpp, linux.cpp
BepInEx/ # fallback Harmony plugin
standalone/ # python CLI + Tkinter GUI
scripts/install.* # pull-from-git installers
tools/dump_metadata.py # parse global-metadata.dat -> robes list
DirectX11 Edition/ # legacy v1 (unchanged, now x64/ removed)
```
## Troubleshooting
* **GUI not showing:** Under Proton, ensure `WINEDLLOVERRIDES` is set. Check `compatdata/1274570/.../Player.log` for `[DevourUnlocker] Present hooked`.
* **winhttp conflict:** Another mod already uses `winhttp.dll` → use `BepInEx` build or chain-load via `doorstop`.
* **After game update hooks miss:** Run `python tools/dump_metadata.py /path/to/global-metadata.dat` — if `IsRobeUnlocked` gone, open issue with new `Player.log`.
## License
MIT — see `LICENSE`. Original by RaizyDaizy, rewrite by fork.

66
scripts/install.ps1 Normal file
View file

@ -0,0 +1,66 @@
param(
[string]$Repo = "mithral/DevourUnlocker",
[string]$Tag = "latest",
[string]$GameDir = "",
[switch]$Uninstall
)
$ErrorActionPreference = "Stop"
function Find-GameDir {
$cands = @(
"$env:ProgramFiles (x86)/Steam/steamapps/common/Devour",
"C:/Steam/steamapps/common/Devour",
"$env:USERPROFILE/.local/share/Steam/steamapps/common/Devour"
)
foreach ($c in $cands) { if (Test-Path "$c/DEVOUR.exe") { return $c } }
$vdf = "$env:ProgramFiles (x86)/Steam/steamapps/libraryfolders.vdf"
if (Test-Path $vdf) {
$txt = Get-Content $vdf -Raw
foreach ($m in [regex]::Matches($txt, '"path"\s+"([^"]+)"')) {
$p = $m.Groups[1].Value + "/steamapps/common/Devour"
if (Test-Path "$p/DEVOUR.exe") { return $p }
}
}
return $null
}
if (-not $GameDir) { $GameDir = Find-GameDir }
if (-not $GameDir -or -not (Test-Path "$GameDir/DEVOUR.exe")) {
Write-Error "Could not find Devour install. Use -GameDir C:/.../Devour"
exit 1
}
Write-Host "[*] Game dir: $GameDir"
if ($Uninstall) {
Remove-Item "$GameDir/winhttp.dll" -ErrorAction SilentlyContinue
Remove-Item "$GameDir/DevourUnlocker.dll" -ErrorAction SilentlyContinue
if (Test-Path "$GameDir/winhttp.dll.bak") { Move-Item "$GameDir/winhttp.dll.bak" "$GameDir/winhttp.dll" -Force; Write-Host "[*] Restored original" }
Write-Host "[*] Uninstalled"
exit 0
}
if ($Tag -eq "latest") {
$api = "https://api.github.com/repos/$Repo/releases/latest"
$rel = Invoke-RestMethod -Uri $api
$Tag = $rel.tag_name
}
Write-Host "[*] Installing $Repo $Tag"
$zipUrl = "https://github.com/$Repo/releases/download/$Tag/DevourUnlocker_winhttp_$Tag.zip"
$tmp = Join-Path $env:TEMP "devour_$Tag"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$zip = Join-Path $tmp "pkg.zip"
Invoke-WebRequest -Uri $zipUrl -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $tmp -Force
if (Test-Path "$GameDir/winhttp.dll") {
Copy-Item "$GameDir/winhttp.dll" "$GameDir/winhttp.dll.bak" -Force
Write-Host "[*] Backed up winhttp.dll"
}
Copy-Item "$tmp/winhttp.dll" "$GameDir/winhttp.dll" -Force
if (Test-Path "$tmp/DevourUnlocker.dll") { Copy-Item "$tmp/DevourUnlocker.dll" "$GameDir/DevourUnlocker.dll" -Force }
if (-not (Test-Path "$GameDir/DevourUnlocker.json")) {
if (Test-Path "$tmp/DevourUnlocker.json") { Copy-Item "$tmp/DevourUnlocker.json" "$GameDir/DevourUnlocker.json" }
else { '{"unlockRobes":true,"unlockCharacters":true}' | Out-File "$GameDir/DevourUnlocker.json" }
}
Write-Host "[*] Installed to $GameDir — press F6 in-game for GUI, F5 to unload"
Remove-Item $tmp -Recurse -Force

94
scripts/install.sh Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
# DevourUnlocker installer — pulls winhttp.dll from GitHub Releases and drops beside DEVOUR.exe
# Works for CachyOS/Proton and native Linux; Windows users should use install.ps1
# Usage: REPO=mithral/DevourUnlocker bash install.sh [--uninstall] [--tag v2.0.0] [--game-dir /path]
REPO="${REPO:-mithral/DevourUnlocker}"
TAG="${TAG:-latest}"
GAME_DIR=""
UNINSTALL=0
while [[ $# -gt 0 ]]; do
case "$1" in
--uninstall) UNINSTALL=1; shift;;
--tag) TAG="$2"; shift 2;;
--game-dir) GAME_DIR="$2"; shift 2;;
--repo) REPO="$2"; shift 2;;
*) echo "Unknown arg $1"; exit 1;;
esac
done
detect_game_dir() {
local cands=(
"$HOME/.local/share/Steam/steamapps/common/Devour"
"$HOME/.steam/steam/steamapps/common/Devour"
"/mnt/*/SteamLibrary/steamapps/common/Devour"
)
for c in "${cands[@]}"; do
for e in $c; do [[ -f "$e/DEVOUR.exe" ]] && { echo "$e"; return 0; }; done
done
# parse libraryfolders.vdf
local vdf="$HOME/.local/share/Steam/steamapps/libraryfolders.vdf"
if [[ -f "$vdf" ]]; then
grep -o '"path"[[:space:]]*"[^"]*"' "$vdf" | cut -d'"' -f4 | while read -r p; do
if [[ -f "$p/steamapps/common/Devour/DEVOUR.exe" ]]; then echo "$p/steamapps/common/Devour"; return 0; fi
done
fi
return 1
}
if [[ -z "$GAME_DIR" ]]; then
GAME_DIR="$(detect_game_dir || true)"
fi
if [[ -z "$GAME_DIR" || ! -f "$GAME_DIR/DEVOUR.exe" ]]; then
echo "[!] Could not find Devour install. Use --game-dir /path/to/Devour"
exit 1
fi
echo "[*] Game dir: $GAME_DIR"
if [[ $UNINSTALL -eq 1 ]]; then
echo "[*] Uninstalling..."
rm -f "$GAME_DIR/winhttp.dll" "$GAME_DIR/DevourUnlocker.dll" "$GAME_DIR/DevourUnlocker.json"
if [[ -f "$GAME_DIR/winhttp.dll.bak" ]]; then mv "$GAME_DIR/winhttp.dll.bak" "$GAME_DIR/winhttp.dll"; echo "[*] Restored original winhttp.dll"; fi
echo "[*] Clearing Proton launch override hint:"
echo " Remove from Steam -> Devour -> Properties -> Launch Options: WINEDLLOVERRIDES=\"winhttp=n,b %command%\""
exit 0
fi
# Resolve tag -> download URL
if [[ "$TAG" == "latest" ]]; then
API="https://api.github.com/repos/$REPO/releases/latest"
TAG=$(curl -s "$API" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
[[ -z "$TAG" ]] && { echo "[!] Could not resolve latest tag"; exit 1; }
fi
echo "[*] Installing $REPO $TAG"
ZIP_URL="https://github.com/$REPO/releases/download/$TAG/DevourUnlocker_winhttp_$TAG.zip"
TMP="$(mktemp -d)"
echo "[*] Downloading $ZIP_URL"
if ! curl -L -o "$TMP/pkg.zip" "$ZIP_URL"; then
echo "[!] Download failed — check REPO/TAG or build a release with that name"
echo " Fallback: build locally with cmake --build build"
exit 1
fi
unzip -o "$TMP/pkg.zip" -d "$TMP" >/dev/null
# Backup existing winhttp if not ours
if [[ -f "$GAME_DIR/winhttp.dll" ]]; then
if ! grep -q "DevourUnlocker" "$GAME_DIR/winhttp.dll" 2>/dev/null; then
cp "$GAME_DIR/winhttp.dll" "$GAME_DIR/winhttp.dll.bak" && echo "[*] Backed up existing winhttp.dll -> winhttp.dll.bak"
fi
fi
cp -v "$TMP/winhttp.dll" "$GAME_DIR/winhttp.dll"
cp -v "$TMP/DevourUnlocker.dll" "$GAME_DIR/DevourUnlocker.dll" 2>/dev/null || true
# Keep user's json if exists
[[ -f "$GAME_DIR/DevourUnlocker.json" ]] || cp -v "$TMP/DevourUnlocker.json" "$GAME_DIR/DevourUnlocker.json" 2>/dev/null || echo '{"unlockRobes":true,"unlockCharacters":true}' > "$GAME_DIR/DevourUnlocker.json"
echo "[*] Installed to $GAME_DIR"
echo "[*] For Proton (CachyOS): set Steam -> Devour -> Properties -> Launch Options to:"
echo " WINEDLLOVERRIDES=\"winhttp=n,b\" %command%"
echo "[*] Then launch Devour, press F6 for GUI (selective unlock), F5 to unload."
echo "[*] Multiplayer: safe for friend lobbies (client visual, no VAC/EAC)."
rm -rf "$TMP"

82
src/config.cpp Normal file
View file

@ -0,0 +1,82 @@
#include "config.hpp"
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <Windows.h>
#endif
namespace devour {
static UnlockConfig g_config;
UnlockConfig& Config::get() { return g_config; }
std::string Config::defaultPath() {
#ifdef _WIN32
char buf[MAX_PATH]{};
if (GetModuleFileNameA(nullptr, buf, MAX_PATH)) {
std::filesystem::path exe(buf);
return (exe.parent_path() / "DevourUnlocker.json").string();
}
#endif
return "DevourUnlocker.json";
}
std::vector<std::string> Config::allRobes() {
return {
"StraightJacket","Patchwork","CherryBlossom","Eggs","Poison","Web",
"TownHard","TownNightmare","TownBooks","Horseshoes",
"SlaughterhouseHard","SlaughterhouseNightmare","SlaughterhousePigs","BarbedWires",
"Golden","ManorHard","ManorNightmare","ManorHeadsBuried","Feathers",
"CarnivalHard","CarnivalNightmare","CarnivalDollHeads","Tickets",
"Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare"
};
}
std::vector<std::string> Config::allCharacters() {
return {"Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"};
}
bool Config::load(const std::string& path) {
std::ifstream f(path);
if (!f) return false;
std::string content((std::istreambuf_iterator<char>(f)), {});
// Minimal JSON parse — avoid nlohmann dep for portability
auto has = [&](const char* key, bool def) -> bool {
auto pos = content.find(std::string("\"") + key + "\"");
if (pos == std::string::npos) return def;
auto colon = content.find(':', pos);
if (colon == std::string::npos) return def;
auto v = content.substr(colon+1, 20);
if (v.find("true") != std::string::npos) return true;
if (v.find("false") != std::string::npos) return false;
return def;
};
g_config.unlockRobes = has("unlockRobes", true);
g_config.unlockCharacters = has("unlockCharacters", true);
g_config.unlockPerks = has("unlockPerks", true);
g_config.unlockFlashlights = has("unlockFlashlights", true);
g_config.unlockEmotes = has("unlockEmotes", true);
g_config.unlockPets = has("unlockPets", true);
g_config.guiVisible = has("guiVisible", true);
return true;
}
bool Config::save(const std::string& path) {
std::ofstream f(path);
if (!f) return false;
auto& c = g_config;
f << "{\n";
f << " \"unlockRobes\": " << (c.unlockRobes?"true":"false") << ",\n";
f << " \"unlockCharacters\": " << (c.unlockCharacters?"true":"false") << ",\n";
f << " \"unlockPerks\": " << (c.unlockPerks?"true":"false") << ",\n";
f << " \"unlockFlashlights\": " << (c.unlockFlashlights?"true":"false") << ",\n";
f << " \"unlockEmotes\": " << (c.unlockEmotes?"true":"false") << ",\n";
f << " \"unlockPets\": " << (c.unlockPets?"true":"false") << ",\n";
f << " \"guiVisible\": " << (c.guiVisible?"true":"false") << "\n";
f << "}\n";
return true;
}
} // namespace devour

36
src/config.hpp Normal file
View file

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
namespace devour {
struct UnlockConfig {
// Global toggles
bool unlockRobes = true;
bool unlockCharacters = true;
bool unlockPerks = true;
bool unlockFlashlights = true;
bool unlockEmotes = true;
bool unlockPets = true;
// 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;
// UI
bool guiVisible = true;
int guiHotkey = 0x75; // F6
};
class Config {
public:
static UnlockConfig& get();
static bool load(const std::string& path = "DevourUnlocker.json");
static bool save(const std::string& path = "DevourUnlocker.json");
static std::string defaultPath();
static std::vector<std::string> allRobes();
static std::vector<std::string> allCharacters();
};
} // namespace devour

134
src/gui/overlay.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "overlay.hpp"
#include "../config.hpp"
#include "../patches.hpp"
#include <imgui.h>
#ifdef _WIN32
#include <imgui_impl_win32.h>
#include <imgui_impl_dx11.h>
#include <d3d11.h>
#else
// SDL backends are only used when SDL2 is available; stubs otherwise
#if __has_include(<SDL2/SDL.h>)
#include <imgui_impl_sdl2.h>
#include <imgui_impl_opengl3.h>
#endif
#endif
#include <iostream>
namespace devour::gui {
static bool g_visible = true;
static bool g_inited = false;
#ifdef _WIN32
static ID3D11Device* g_device = nullptr;
static ID3D11DeviceContext* g_ctx = nullptr;
static HWND g_hwnd = nullptr;
bool init(void* device, void* ctx, void* swapchain, void* hwnd) {
if (g_inited) return true;
g_device = (ID3D11Device*)device;
g_ctx = (ID3D11DeviceContext*)ctx;
g_hwnd = (HWND)hwnd;
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui::StyleColorsDark();
ImGui_ImplWin32_Init(g_hwnd);
ImGui_ImplDX11_Init(g_device, g_ctx);
g_inited = true;
std::cout << "[DevourUnlocker] ImGui DX11 initialized\n";
return true;
}
void shutdown() {
if (!g_inited) return;
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
g_inited = false;
}
void render() {
if (!g_inited) return;
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
if (g_visible) {
ImGui::SetNextWindowSize(ImVec2(560, 620), ImGuiCond_FirstUseEver);
ImGui::Begin("DevourUnlocker v2 — WinHTTP Edition", &g_visible, ImGuiWindowFlags_NoCollapse);
auto& cfg = Config::get();
ImGui::TextColored(ImVec4(0.9f,0.2f,0.2f,1), "Selective Unlock — Multiplayer safe (client visual)");
ImGui::Separator();
if (ImGui::CollapsingHeader("Global Toggles", ImGuiTreeNodeFlags_DefaultOpen)) {
bool changed = false;
changed |= ImGui::Checkbox("Unlock Robes", &cfg.unlockRobes);
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);
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;
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;
Config::save(Config::defaultPath());
}
}
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());
}
}
}
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());
}
}
}
ImGui::Separator();
if (ImGui::Button("Save", ImVec2(80,28))) Config::save(Config::defaultPath());
ImGui::SameLine();
if (ImGui::Button("Reload", ImVec2(80,28))) Config::load(Config::defaultPath());
ImGui::SameLine();
ImGui::TextDisabled("F6 toggle | F5 unload");
ImGui::Text("Patches: %s", patchesInstalled() ? "ACTIVE" : "PENDING");
ImGui::End();
}
ImGui::Render();
if (g_ctx) {
// Caller sets render target; we just draw
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}
}
#else
bool init(void*, void*, void*, void*) { g_inited = true; return true; }
void shutdown() { g_inited = false; }
void render() {}
#endif
void setVisible(bool v) { g_visible = v; Config::get().guiVisible = v; }
bool isVisible() { return g_visible; }
void toggle() { setVisible(!g_visible); }
} // namespace devour::gui

14
src/gui/overlay.hpp Normal file
View file

@ -0,0 +1,14 @@
#pragma once
namespace devour {
namespace gui {
bool init(void* device, void* context, void* swapchain, void* hwnd);
void shutdown();
void render();
void setVisible(bool v);
bool isVisible();
void toggle();
} // namespace gui
} // namespace devour

158
src/il2cpp_resolver.cpp Normal file
View file

@ -0,0 +1,158 @@
#include "il2cpp_resolver.hpp"
#include <string>
#include <thread>
#include <chrono>
#ifdef _WIN32
#include <Windows.h>
#include <psapi.h>
#else
#include <dlfcn.h>
#include <link.h>
#include <unistd.h>
#endif
namespace devour {
#ifdef _WIN32
static HMODULE g_gameAsm = nullptr;
uintptr_t Il2CppResolver::gameAssemblyBase() {
return reinterpret_cast<uintptr_t>(g_gameAsm);
}
size_t Il2CppResolver::gameAssemblySize() {
if (!g_gameAsm) return 0;
MODULEINFO mi{};
if (GetModuleInformation(GetCurrentProcess(), g_gameAsm, &mi, sizeof(mi)))
return mi.SizeOfImage;
return 0;
}
void* Il2CppResolver::getProc(const char* name) {
if (!g_gameAsm) return nullptr;
return reinterpret_cast<void*>(GetProcAddress(g_gameAsm, name));
}
bool Il2CppResolver::init() {
for (int i = 0; i < 600; ++i) { // 60s timeout
g_gameAsm = GetModuleHandleA("GameAssembly.dll");
if (g_gameAsm) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
// Simple pattern scan
std::vector<void*> Il2CppResolver::scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size) {
std::vector<void*> out;
size_t patLen = strlen(mask);
for (size_t i = 0; i + patLen < size; ++i) {
bool ok = true;
for (size_t j = 0; j < patLen; ++j) {
if (mask[j] == 'x' && reinterpret_cast<char*>(start + i)[j] != pattern[j]) { ok = false; break; }
}
if (ok) out.push_back(reinterpret_cast<void*>(start + i));
}
return out;
}
ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* methodName) {
// Use il2cpp exports to find method without offset
using domain_get_t = void*(*)();
using thread_attach_t = void*(*)(void*);
using assembly_foreach_t = void(*)(void(*)(void*, void*), void*);
// We call il2cpp_resolve_icall-like path: try to locate via il2cpp_class_from_name
auto il2cpp_domain_get = reinterpret_cast<domain_get_t>(getProc("il2cpp_domain_get"));
auto il2cpp_thread_attach = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_thread_attach"));
auto klass_from_name = reinterpret_cast<void*(*)(void*, const char*, const char*)>(getProc("il2cpp_class_from_name"));
auto class_get_method = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_method_from_name"));
if (!il2cpp_domain_get || !klass_from_name || !class_get_method) {
return resolveByStringXref(methodName);
}
void* domain = il2cpp_domain_get();
if (domain && il2cpp_thread_attach) il2cpp_thread_attach(domain);
// NolanRankController lives in Assembly-CSharp — hosts IsRobeUnlocked etc.
// Try common owner classes: "NolanRankController", "LobbyController", "InventoryController"
const char* candidates[] = {"NolanRankController", "LobbyController", "InventoryController", "GameController", nullptr};
for (int ci = 0; candidates[ci]; ++ci) {
void* klass = klass_from_name(nullptr, "Assembly-CSharp.dll", candidates[ci]);
// Fallback: il2cpp_class_from_name expects (image, ns, name) — try image-less variant search
if (!klass) {
// Try global search via xref fallback
continue;
}
void* method = class_get_method(klass, methodName);
if (method) {
// MethodInfo layout: methodPointer at offset 0x18 on x64 (stable across il2cpp)
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(method) + 0x18);
if (ptr) return {methodName, ptr};
}
}
return resolveByStringXref(methodName);
}
ResolvedMethod Il2CppResolver::resolveByStringXref(const char* str, int occurrence) {
// Scan for string pointer -> xref to find caller. Minimal heuristic:
// Search for ansi string bytes inside GameAssembly rdata, then scan .text for lea rcx, [addr]
uintptr_t base = gameAssemblyBase();
size_t sz = gameAssemblySize();
if (!base || !sz) return {str, nullptr};
// Find string bytes
size_t slen = strlen(str);
uintptr_t strAddr = 0;
for (size_t i = 0; i + slen < sz; ++i) {
if (memcmp(reinterpret_cast<void*>(base + i), str, slen) == 0) {
// Check null terminator
if (*reinterpret_cast<char*>(base + i + slen) == '\0') {
if (occurrence-- == 0) { strAddr = base + i; break; }
}
}
}
if (!strAddr) return {str, nullptr};
// Scan for 48 8D 0D ? ? ? ? (lea rcx, [rip+disp]) referencing strAddr
// Simplified: look for 4-byte disp that resolves to strAddr
for (size_t i = 0; i + 7 < sz; ++i) {
uint8_t* p = reinterpret_cast<uint8_t*>(base + i);
if (p[0] == 0x48 && p[1] == 0x8D && p[2] == 0x0D) {
int32_t disp = *reinterpret_cast<int32_t*>(p + 3);
uintptr_t target = base + i + 7 + disp;
if (target == strAddr) {
// Found xref — function start is previous 0xCC or 0x48 89 5C ? ... search back
// Heuristic: walk back up to 0x200 for function prologue 40 53 / 48 83 EC
for (int back = 0; back < 0x300; ++back) {
uint8_t* q = p - back;
if (q < reinterpret_cast<uint8_t*>(base)) break;
if (q[0] == 0x40 && q[1] == 0x53) return {str, q}; // push rbx
if (q[0] == 0x48 && q[1] == 0x83 && q[2] == 0xEC) return {str, q};
}
return {str, p}; // fallback
}
}
}
return {str, nullptr};
}
ResolvedMethod Il2CppResolver::isRobeUnlocked() { return resolve("NolanRankController", "IsRobeUnlocked"); }
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRankController", "IsCharacterUnlocked"); }
ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("NolanRankController", "HasOutfit"); }
#else // Linux stubs
uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; }
size_t Il2CppResolver::gameAssemblySize() { return 0; }
void* Il2CppResolver::getProc(const char*) { return nullptr; }
bool Il2CppResolver::init() { return false; }
std::vector<void*> Il2CppResolver::scanPattern(const char*, const char*, uintptr_t, size_t) { return {}; }
ResolvedMethod Il2CppResolver::resolve(const char*, const char* m) { return {m, nullptr}; }
ResolvedMethod Il2CppResolver::resolveByStringXref(const char* s, int) { return {s, nullptr}; }
ResolvedMethod Il2CppResolver::isRobeUnlocked() { return {"IsRobeUnlocked", nullptr}; }
ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return {"IsCharacterUnlocked", nullptr}; }
ResolvedMethod Il2CppResolver::hasOutfit() { return {"HasOutfit", nullptr}; }
#endif
} // namespace devour

38
src/il2cpp_resolver.hpp Normal file
View file

@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
// Minimal IL2CPP types — avoids needing il2cpp headers at compile time
struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; };
struct MethodInfo;
namespace devour {
// Resolves IL2CPP method pointers without hard offsets.
// Strategy 1: via il2cpp exports (class/method lookup)
// Strategy 2: xref string sigscan as fallback
struct ResolvedMethod {
std::string name;
void* address = nullptr;
bool valid() const { return address != nullptr; }
};
class Il2CppResolver {
public:
static bool init(); // wait for GameAssembly.dll
static void* getProc(const char* name);
static std::vector<void*> scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size);
static ResolvedMethod resolve(const char* klassName, const char* methodName);
static ResolvedMethod resolveByStringXref(const char* str, int occurrence = 0);
static ResolvedMethod isRobeUnlocked();
static ResolvedMethod isCharacterUnlocked();
static ResolvedMethod hasOutfit(); // optional future
static uintptr_t gameAssemblyBase();
static size_t gameAssemblySize();
};
} // namespace devour

86
src/main.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "config.hpp"
#include "patches.hpp"
#include "il2cpp_resolver.hpp"
#ifdef _WIN32
#include <Windows.h>
#include <MinHook.h>
#include <d3d11.h>
#include <dxgi.h>
#include <iostream>
#include "gui/overlay.hpp"
using PresentFn = HRESULT(__stdcall*)(IDXGISwapChain*, UINT, UINT);
extern HRESULT __stdcall hkPresent(IDXGISwapChain*, UINT, UINT);
extern PresentFn oPresent;
static HMODULE g_self = nullptr;
DWORD WINAPI MainThread(LPVOID) {
AllocConsole();
SetConsoleTitleA("DevourUnlocker v2");
FILE* f; freopen_s(&f, "CONOUT$", "w", stdout);
std::cout << "[DevourUnlocker] v2 winhttp proxy loaded\n";
devour::Config::load(devour::Config::defaultPath());
// Wait for d3d11 then hook Present via vtable
HMODULE d3d11 = nullptr;
while (!(d3d11 = GetModuleHandleA("d3d11.dll"))) Sleep(500);
// Create dummy swapchain to get vtable (kiero-style but lightweight)
WNDCLASSEX wc{sizeof(WNDCLASSEX), CS_HREDRAW|CS_VREDRAW, DefWindowProc, 0,0, GetModuleHandle(nullptr), nullptr,nullptr,nullptr,nullptr, L"DevourUnlockerDummy", nullptr};
RegisterClassEx(&wc);
HWND hwnd = CreateWindow(wc.lpszClassName, L"Dummy", WS_OVERLAPPEDWINDOW, 0,0,100,100, nullptr,nullptr,wc.hInstance,nullptr);
DXGI_SWAP_CHAIN_DESC sd{};
sd.BufferCount=1; sd.BufferDesc.Format=DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferUsage=DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow=hwnd; sd.SampleDesc.Count=1; sd.Windowed=TRUE; sd.SwapEffect=DXGI_SWAP_EFFECT_DISCARD;
IDXGISwapChain* swap=nullptr; ID3D11Device* dev=nullptr; ID3D11DeviceContext* ctx=nullptr;
D3D_FEATURE_LEVEL fl;
HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr,0, D3D11_SDK_VERSION, &sd, &swap, &dev, &fl, &ctx);
if (SUCCEEDED(hr) && swap) {
void* target = (*(void***)swap)[8];
MH_Initialize();
MH_CreateHook(target, (void*)hkPresent, (void**)&oPresent);
MH_EnableHook(target);
std::cout << "[DevourUnlocker] Present hooked at " << target << "\n";
if (swap) swap->Release();
if (dev) dev->Release();
if (ctx) ctx->Release();
} else {
std::cout << "[DevourUnlocker] D3D11 dummy failed hr=" << std::hex << hr << " — patches still active\n";
devour::installPatches();
}
DestroyWindow(hwnd);
UnregisterClass(wc.lpszClassName, wc.hInstance);
// Eject hotkey F5
while (true) {
if (GetAsyncKeyState(VK_F5) & 1) {
std::cout << "[DevourUnlocker] Eject requested\n";
devour::gui::shutdown();
MH_DisableHook(MH_ALL_HOOKS); MH_Uninitialize();
FreeLibraryAndExitThread(g_self, 0);
}
Sleep(100);
}
return 0;
}
BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
g_self = hMod;
DisableThreadLibraryCalls(hMod);
CreateThread(nullptr, 0, MainThread, nullptr, 0, nullptr);
}
return TRUE;
}
#else // Linux DllMain stub
#include <iostream>
__attribute__((constructor)) void devour_init() {
std::cout << "[DevourUnlocker] Linux .so constructor\n";
devour::Config::load(devour::Config::defaultPath());
}
__attribute__((destructor)) void devour_fini() { std::cout << "[DevourUnlocker] Unload\n"; }
#endif

121
src/patches.cpp Normal file
View file

@ -0,0 +1,121 @@
#include "patches.hpp"
#include "il2cpp_resolver.hpp"
#include "config.hpp"
#include <iostream>
#ifdef _WIN32
#include <Windows.h>
#include <MinHook.h>
#endif
namespace devour {
#ifdef _WIN32
using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* method);
static IsUnlockedFn oIsRobe = nullptr;
static IsUnlockedFn oIsChar = nullptr;
static bool g_installed = false;
static bool g_mhInit = false;
static std::string il2cppStringToUtf8(void* ilStr) {
if (!ilStr) return "";
// Il2CppString layout: length at offset 0x10, chars at 0x14
int32_t len = *reinterpret_cast<int32_t*>(reinterpret_cast<uint8_t*>(ilStr) + 0x10);
wchar_t* chars = reinterpret_cast<wchar_t*>(reinterpret_cast<uint8_t*>(ilStr) + 0x14);
if (len <= 0 || len > 512) return "";
char buf[1024]{};
int n = WideCharToMultiByte(CP_UTF8, 0, chars, len, buf, sizeof(buf)-1, nullptr, nullptr);
if (n > 0) return std::string(buf, n);
return "";
}
static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* method) {
auto& cfg = Config::get();
if (!cfg.unlockRobes) {
return oIsRobe ? oIsRobe(thiz, robeStr, method) : false;
}
std::string name = il2cppStringToUtf8(robeStr);
auto it = cfg.robeOverrides.find(name);
if (it != cfg.robeOverrides.end()) return it->second;
// Log once per unique robe
static std::unordered_map<std::string,int> seen;
if (!seen[name]++) std::cout << "[DevourUnlocker] Robe unlocked: " << name << "\n";
return true;
}
static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) {
auto& cfg = Config::get();
if (!cfg.unlockCharacters) {
return oIsChar ? oIsChar(thiz, charStr, method) : false;
}
std::string name = il2cppStringToUtf8(charStr);
auto it = cfg.characterOverrides.find(name);
if (it != cfg.characterOverrides.end()) return it->second;
static std::unordered_map<std::string,int> seen;
if (!seen[name]++) std::cout << "[DevourUnlocker] Character unlocked: " << name << "\n";
return true;
}
bool installPatches() {
if (g_installed) return true;
if (!g_mhInit) {
if (MH_Initialize() != MH_OK) {
std::cerr << "[DevourUnlocker] MH_Initialize failed\n";
return false;
}
g_mhInit = true;
}
if (!Il2CppResolver::init()) {
std::cerr << "[DevourUnlocker] GameAssembly not found\n";
return false;
}
auto robe = Il2CppResolver::isRobeUnlocked();
auto ch = Il2CppResolver::isCharacterUnlocked();
std::cout << "[DevourUnlocker] Resolved IsRobeUnlocked=" << robe.address
<< " IsCharacterUnlocked=" << ch.address << "\n";
bool any = false;
if (robe.valid()) {
if (MH_CreateHook(robe.address, (void*)hIsRobe, (void**)&oIsRobe) == MH_OK) {
MH_EnableHook(robe.address);
std::cout << "[DevourUnlocker] Hooked IsRobeUnlocked\n";
any = true;
} else std::cerr << "[DevourUnlocker] Hook IsRobe failed\n";
} else {
std::cerr << "[DevourUnlocker] IsRobeUnlocked not found — will retry on next init or use standalone\n";
}
if (ch.valid()) {
if (MH_CreateHook(ch.address, (void*)hIsChar, (void**)&oIsChar) == MH_OK) {
MH_EnableHook(ch.address);
std::cout << "[DevourUnlocker] Hooked IsCharacterUnlocked\n";
any = true;
} else std::cerr << "[DevourUnlocker] Hook IsChar failed\n";
}
g_installed = any;
return any;
}
void removePatches() {
if (!g_installed) return;
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();
g_mhInit = false;
g_installed = false;
}
bool patchesInstalled() { return g_installed; }
#else // Linux stub — uses funchook or LD_PRELOAD interpose if needed
bool installPatches() {
std::cout << "[DevourUnlocker] Linux patches stub — use winhttp.dll under Proton\n";
return false;
}
void removePatches() {}
bool patchesInstalled() { return false; }
#endif
} // namespace devour

9
src/patches.hpp Normal file
View file

@ -0,0 +1,9 @@
#pragma once
namespace devour {
bool installPatches();
void removePatches();
bool patchesInstalled();
} // namespace devour

7
src/platform/common.cpp Normal file
View file

@ -0,0 +1,7 @@
#include <iostream>
#ifdef _WIN32
#include <Windows.h>
#endif
// common helpers
void logInfo(const char* msg) { std::cout << "[DevourUnlocker] " << msg << "\n"; }

10
src/platform/linux.cpp Normal file
View file

@ -0,0 +1,10 @@
#ifndef _WIN32
#include <iostream>
// Linux stub — for native .so testing; Proton uses winhttp.dll path
namespace devour::platform {
bool initLinux() {
std::cout << "[DevourUnlocker] Linux native .so loaded — under Proton prefer winhttp.dll\n";
return true;
}
}
#endif

67
src/platform/win32.cpp Normal file
View file

@ -0,0 +1,67 @@
#ifdef _WIN32
#include <Windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include "../gui/overlay.hpp"
#include "../config.hpp"
#include "../patches.hpp"
#include <iostream>
// DX11 Present hook — replaces kiero for modern build
using PresentFn = HRESULT(__stdcall*)(IDXGISwapChain*, UINT, UINT);
static PresentFn oPresent = nullptr;
static HWND g_hWnd = nullptr;
static ID3D11Device* g_device = nullptr;
static ID3D11DeviceContext* g_ctx = nullptr;
static ID3D11RenderTargetView* g_rtv = nullptr;
static WNDPROC oWndProc = nullptr;
static bool g_init = false;
extern LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
LRESULT __stdcall WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (devour::gui::isVisible() && ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
return true;
return CallWindowProc(oWndProc, hWnd, msg, wParam, lParam);
}
HRESULT __stdcall hkPresent(IDXGISwapChain* swap, UINT si, UINT flags) {
if (!g_init) {
if (SUCCEEDED(swap->GetDevice(__uuidof(ID3D11Device), (void**)&g_device))) {
g_device->GetImmediateContext(&g_ctx);
DXGI_SWAP_CHAIN_DESC sd{};
swap->GetDesc(&sd);
g_hWnd = sd.OutputWindow;
ID3D11Texture2D* back = nullptr;
swap->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&back);
if (back) {
g_device->CreateRenderTargetView(back, nullptr, &g_rtv);
back->Release();
}
oWndProc = (WNDPROC)SetWindowLongPtr(g_hWnd, GWLP_WNDPROC, (LONG_PTR)WndProc);
devour::Config::load(devour::Config::defaultPath());
devour::installPatches();
devour::gui::init(g_device, g_ctx, swap, g_hWnd);
g_init = true;
std::cout << "[DevourUnlocker] Present hook init ok\n";
} else return oPresent(swap, si, flags);
}
// Hotkeys
if (GetAsyncKeyState(VK_F6) & 1) devour::gui::toggle();
// Render
devour::gui::render();
if (g_ctx && g_rtv) g_ctx->OMSetRenderTargets(1, &g_rtv, nullptr);
return oPresent(swap, si, flags);
}
namespace devour::platform {
bool hookPresent(void* target) {
oPresent = (PresentFn)target;
// MinHook is set up by patches.cpp; here we just store
return true;
}
void* getHookTarget(IDXGISwapChain* swap) { return (*(void***)swap)[8]; }
WNDPROC getWndProc() { return oWndProc; }
}
#endif

View file

@ -0,0 +1,28 @@
LIBRARY winhttp
EXPORTS
WinHttpAddRequestHeaders=winhttp_real.WinHttpAddRequestHeaders
WinHttpAutoProxyMain=winhttp_real.WinHttpAutoProxyMain
WinHttpCheckPlatform=winhttp_real.WinHttpCheckPlatform
WinHttpCloseHandle=winhttp_real.WinHttpCloseHandle
WinHttpConnect=winhttp_real.WinHttpConnect
WinHttpCrackUrl=winhttp_real.WinHttpCrackUrl
WinHttpCreateUrl=winhttp_real.WinHttpCreateUrl
WinHttpDetectAutoProxyConfigUrl=winhttp_real.WinHttpDetectAutoProxyConfigUrl
WinHttpGetDefaultProxyConfiguration=winhttp_real.WinHttpGetDefaultProxyConfiguration
WinHttpGetIEProxyConfigForCurrentUser=winhttp_real.WinHttpGetIEProxyConfigForCurrentUser
WinHttpGetProxyForUrl=winhttp_real.WinHttpGetProxyForUrl
WinHttpOpen=winhttp_real.WinHttpOpen
WinHttpOpenRequest=winhttp_real.WinHttpOpenRequest
WinHttpQueryAuthSchemes=winhttp_real.WinHttpQueryAuthSchemes
WinHttpQueryDataAvailable=winhttp_real.WinHttpQueryDataAvailable
WinHttpQueryHeaders=winhttp_real.WinHttpQueryHeaders
WinHttpReadData=winhttp_real.WinHttpReadData
WinHttpReceiveResponse=winhttp_real.WinHttpReceiveResponse
WinHttpSendRequest=winhttp_real.WinHttpSendRequest
WinHttpSetCredentials=winhttp_real.WinHttpSetCredentials
WinHttpSetOption=winhttp_real.WinHttpSetOption
WinHttpSetStatusCallback=winhttp_real.WinHttpSetStatusCallback
WinHttpSetTimeouts=winhttp_real.WinHttpSetTimeouts
WinHttpTimeFromSystemTime=winhttp_real.WinHttpTimeFromSystemTime
WinHttpTimeToSystemTime=winhttp_real.WinHttpTimeToSystemTime
WinHttpWriteData=winhttp_real.WinHttpWriteData

View file

@ -0,0 +1,37 @@
#ifdef _WIN32
#include <Windows.h>
#include <string>
// winhttp.dll proxy — forwards all exports to system winhttp
// This allows us to be loaded as winhttp.dll beside DEVOUR.exe without breaking HTTP.
static HMODULE g_real = nullptr;
static HMODULE getReal() {
if (g_real) return g_real;
char sys[MAX_PATH];
GetSystemDirectoryA(sys, MAX_PATH);
std::string p = std::string(sys) + "\\winhttp.dll";
g_real = LoadLibraryA(p.c_str());
return g_real;
}
extern "C" FARPROC getProc(const char* name) {
HMODULE r = getReal();
if (!r) return nullptr;
return GetProcAddress(r, name);
}
// List of winhttp exports to forward — generated via dumpbin /exports winhttp.dll
// We use pragma comment linker exports forwarding syntax where possible.
// For MinGW/MSVC compatibility, also provide explicit forwarders.
#pragma comment(linker, "/export:WinHttpAddRequestHeaders=" __FILE__ ".WinHttpAddRequestHeaders")
extern "C" void* WinHttpAddRequestHeaders = nullptr; // placeholder — real forwarding via .def
// The .def file does the forwarding; this cpp just ensures LoadLibrary of real dll early.
BOOL APIENTRY DllMainProxy(HMODULE, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) getReal();
return TRUE;
}
#endif

131
standalone/devour_unlocker.py Executable file
View file

@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Devour Standalone Unlocker — works without DLL injection.
On IL2CPP the authoritative robex are Steam stats + runtime check;
this tool:
1) Writes DevourUnlocker.json for the native/BepInEx mod (selective).
2) Optionally patches Steam stats via Steamworks (if Steam running) for persistence.
3) Detects Proton vs Windows save locations and backs them up.
Works on CachyOS/Proton and Windows.
"""
import argparse, json, os, sys, pathlib, shutil, subprocess
ROBES = [
"StraightJacket","Patchwork","CherryBlossom","Eggs","Poison","Web",
"TownHard","TownNightmare","TownBooks","Horseshoes",
"SlaughterhouseHard","SlaughterhouseNightmare","SlaughterhousePigs","BarbedWires",
"Golden","ManorHard","ManorNightmare","ManorHeadsBuried","Feathers",
"CarnivalHard","CarnivalNightmare","CarnivalDollHeads","Tickets",
"Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare"
]
CHARS = ["Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"]
def devour_paths():
paths = []
# Windows
if os.name == "nt":
base = pathlib.Path(os.environ.get("APPDATA","")) / ".." / "LocalLow" / "Straight Back Games" / "DEVOUR"
paths.append(base)
else:
# Linux Proton
pfx = pathlib.Path.home() / ".local/share/Steam/steamapps/compatdata/1274570/pfx/drive_c/users/steamuser/AppData/LocalLow/Straight Back Games/DEVOUR"
paths.append(pfx)
# Native (if ever)
paths.append(pathlib.Path.home() / ".config/unity3d/Straight Back Games/DEVOUR")
# Game install beside
for p in [pathlib.Path.home() / ".local/share/Steam/steamapps/common/Devour",
pathlib.Path("C:/Program Files (x86)/Steam/steamapps/common/Devour")]:
paths.append(p)
return paths
def default_json_path():
if os.name == "nt":
# beside exe
for p in devour_paths():
if (p / "DEVOUR.exe").exists():
return p / "DevourUnlocker.json"
return pathlib.Path("DevourUnlocker.json")
else:
p = pathlib.Path.home() / ".local/share/Steam/steamapps/common/Devour/DevourUnlocker.json"
return p
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}
def save_config(path, cfg):
pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True)
pathlib.Path(path).write_text(json.dumps(cfg, indent=2))
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("\nPaths checked:")
for p in devour_paths():
print(f" {p} {'[EXISTS]' if p.exists() else ''}")
def cmd_status(args):
cfg = load_config(args.json)
print(json.dumps(cfg, indent=2))
for p in devour_paths():
if p.exists():
print(f"Found: {p}")
# list files
for f in p.glob("*"):
print(" ", f.name)
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
if args.all:
sel_robes, sel_chars = ROBES, CHARS
# robeOverrides: true for selected, false for others if --exclusive
cfg["unlockRobes"] = True
cfg["unlockCharacters"] = 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}
else:
cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes})
cfg.setdefault("characterOverrides", {}).update({c: True for c in sel_chars})
if args.lock:
cfg["unlockRobes"] = cfg["unlockCharacters"] = False
cfg["robeOverrides"] = {r: False for r in ROBES}
cfg["characterOverrides"] = {c: False for c in CHARS}
save_config(args.json, cfg)
print("[*] Now launch Devour with winhttp.dll — selective unlock active via DevourUnlocker.json")
def cmd_install_mod(args):
# helper to copy DevourUnlocker.json to game dir
src = pathlib.Path(args.json)
dst = default_json_path()
if src.resolve() != dst.resolve():
shutil.copy(src, dst)
print(f"[+] Copied {src} -> {dst}")
def main():
ap = argparse.ArgumentParser(description="Devour Standalone Unlocker")
ap.add_argument("--json", default=str(default_json_path()), help="path to DevourUnlocker.json")
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("list", help="list robes/characters").set_defaults(func=cmd_list)
sp = sub.add_parser("status", help="show config + paths")
sp.set_defaults(func=cmd_status)
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("--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")
su.set_defaults(func=cmd_unlock)
si = sub.add_parser("install-mod", help="copy json to game dir")
si.set_defaults(func=cmd_install_mod)
args = ap.parse_args()
args.func(args)
if __name__ == "__main__":
main()

68
standalone/gui.py Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Tkinter GUI for standalone — selective checkboxes, writes DevourUnlocker.json"""
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
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("DevourUnlocker — Selective Unlock")
self.geometry("560x680")
self.json_path = default_json_path()
self.cfg = load_config(str(self.json_path))
self.vars = {}
self._build()
def _build(self):
ttk.Label(self, text="DevourUnlocker v2 — Standalone (multiplayer safe)").pack(pady=6)
ttk.Label(self, text=f"Config: {self.json_path}").pack()
btn_row = ttk.Frame(self); btn_row.pack(pady=6)
ttk.Button(btn_row, text="Unlock All", command=self.unlock_all).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_row, text="Lock All", command=self.lock_all).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_row, text="Save", command=self.save).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)
for title, items in [("Robes", ROBES), ("Characters", CHARS)]:
f = ttk.Frame(nb)
nb.add(f, text=title)
canvas = tk.Canvas(f)
scroll = ttk.Scrollbar(f, command=canvas.yview)
inner = ttk.Frame(canvas)
inner.bind("<Configure>", lambda e, c=canvas: c.configure(scrollregion=c.bbox("all")))
canvas.create_window((0,0), window=inner, anchor="nw")
canvas.configure(yscrollcommand=scroll.set)
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))
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)
def unlock_all(self):
for v in self.vars.values(): v.set(True)
def lock_all(self):
for v in self.vars.values(): v.set(False)
def reload(self):
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
save_config(str(self.json_path), self.cfg)
messagebox.showinfo("Saved", f"Wrote {self.json_path}\nLaunch Devour with winhttp.dll enabled.")
if __name__ == "__main__":
App().mainloop()

View file

@ -0,0 +1,3 @@
# no external deps for CLI; Tkinter is stdlib
# For PyInstaller bundle:
# pyinstaller --onefile gui.py --name DevourUnlocker-GUI

10
tools/dump_metadata.py Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env python3
"""Dump robe/character strings from global-metadata.dat for DevourUnlocker.json generation"""
import re, pathlib, json, sys
p = pathlib.Path(sys.argv[1]) if len(sys.argv)>1 else pathlib.Path.home()/".local/share/Steam/steamapps/common/Devour/DEVOUR_Data/il2cpp_data/Metadata/global-metadata.dat"
data = p.read_bytes()
strs = [m.group().decode() for m in re.finditer(b'[ -~]{3,}', data)]
robes = sorted({s for s in strs if "RobeUnlocked" in s})
chars = [s for s in strs if "IsCharacterUnlocked" in s]
print(json.dumps({"robes": robes, "hasIsRobe": "IsRobeUnlocked" in strs, "hasIsChar": "IsCharacterUnlocked" in strs}, indent=2))
for r in robes: print(r)