v2.0.0: cross-platform winhttp proxy rewrite
- 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:
parent
242970a7d4
commit
422f47f25c
54 changed files with 1629 additions and 65 deletions
44
.github/workflows/build.yml
vendored
Normal file
44
.github/workflows/build.yml
vendored
Normal 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
39
.gitignore
vendored
Normal 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
139
BepInEx/DevourUnlocker.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
BepInEx/DevourUnlocker.csproj
Normal file
13
BepInEx/DevourUnlocker.csproj
Normal 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
95
CMakeLists.txt
Normal 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
9
DevourUnlocker.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"unlockRobes": true,
|
||||
"unlockCharacters": true,
|
||||
"unlockPerks": true,
|
||||
"unlockFlashlights": true,
|
||||
"unlockEmotes": true,
|
||||
"unlockPets": true,
|
||||
"guiVisible": true
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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\|
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\DevourUnlockerDirectX11\x64\Release\DevourUnlockerDirectX11.vcxproj.CopyComplete
|
||||
|
|
@ -1 +0,0 @@
|
|||
x64\Release\\_IsIncrementalBuild
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\x64\Release\minhook.x64.dll
|
||||
8
DirectX11 Edition/README_LEGACY.md
Normal file
8
DirectX11 Edition/README_LEGACY.md
Normal 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`.
|
||||
95
README.md
95
README.md
|
|
@ -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%
|
||||
```
|
||||
|
||||

|
||||
**Uninstall:**
|
||||
```bash
|
||||
bash scripts/install.sh --uninstall
|
||||
# or ps1: .\install.ps1 -Uninstall
|
||||
```
|
||||
|
||||

|
||||
Launch Devour → **F6** toggles GUI (pick robes/characters), **F5** unloads. No BepInEx needed.
|
||||
|
||||

|
||||
## 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
66
scripts/install.ps1
Normal 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
94
scripts/install.sh
Executable 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
82
src/config.cpp
Normal 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
36
src/config.hpp
Normal 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
134
src/gui/overlay.cpp
Normal 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
14
src/gui/overlay.hpp
Normal 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
158
src/il2cpp_resolver.cpp
Normal 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
38
src/il2cpp_resolver.hpp
Normal 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
86
src/main.cpp
Normal 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
121
src/patches.cpp
Normal 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
9
src/patches.hpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
namespace devour {
|
||||
|
||||
bool installPatches();
|
||||
void removePatches();
|
||||
bool patchesInstalled();
|
||||
|
||||
} // namespace devour
|
||||
7
src/platform/common.cpp
Normal file
7
src/platform/common.cpp
Normal 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
10
src/platform/linux.cpp
Normal 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
67
src/platform/win32.cpp
Normal 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
|
||||
28
src/platform/winhttp_exports.def
Normal file
28
src/platform/winhttp_exports.def
Normal 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
|
||||
37
src/platform/winhttp_proxy.cpp
Normal file
37
src/platform/winhttp_proxy.cpp
Normal 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
131
standalone/devour_unlocker.py
Executable 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
68
standalone/gui.py
Executable 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()
|
||||
3
standalone/requirements.txt
Normal file
3
standalone/requirements.txt
Normal 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
10
tools/dump_metadata.py
Executable 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)
|
||||
Loading…
Add table
Reference in a new issue