diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..684df02 --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..751c5ff --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/BepInEx/DevourUnlocker.cs b/BepInEx/DevourUnlocker.cs new file mode 100644 index 0000000..a3294f7 --- /dev/null +++ b/BepInEx/DevourUnlocker.cs @@ -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 robeOverrides = new(); + public static System.Collections.Generic.Dictionary 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(); + } + } +} diff --git a/BepInEx/DevourUnlocker.csproj b/BepInEx/DevourUnlocker.csproj new file mode 100644 index 0000000..99b6fcd --- /dev/null +++ b/BepInEx/DevourUnlocker.csproj @@ -0,0 +1,13 @@ + + + netstandard2.1 + 2.0.0 + true + latest + + + + + + + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..68ec85b --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/DevourUnlocker.json b/DevourUnlocker.json new file mode 100644 index 0000000..de2e444 --- /dev/null +++ b/DevourUnlocker.json @@ -0,0 +1,9 @@ +{ + "unlockRobes": true, + "unlockCharacters": true, + "unlockPerks": true, + "unlockFlashlights": true, + "unlockEmotes": true, + "unlockPets": true, + "guiVisible": true +} diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.command.1.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.command.1.tlog deleted file mode 100644 index 60f0178..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.command.1.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.read.1.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.read.1.tlog deleted file mode 100644 index bc581a1..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.read.1.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.write.1.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.write.1.tlog deleted file mode 100644 index 7bc15d1..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/CL.write.1.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/DevourUnlockerDirectX11.lastbuildstate b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/DevourUnlockerDirectX11.lastbuildstate deleted file mode 100644 index a56ad84..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/DevourUnlockerDirectX11.lastbuildstate +++ /dev/null @@ -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\| diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/DevourUnlockerDirectX11.write.1u.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/DevourUnlockerDirectX11.write.1u.tlog deleted file mode 100644 index 6d8d88e..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/DevourUnlockerDirectX11.write.1u.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.command.1.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.command.1.tlog deleted file mode 100644 index ff38be0..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.command.1.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.read.1.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.read.1.tlog deleted file mode 100644 index 49009d0..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.read.1.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.write.1.tlog b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.write.1.tlog deleted file mode 100644 index 093b8b3..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUn.5e484f81.tlog/link.write.1.tlog and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.Build.CppClean.log b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.Build.CppClean.log deleted file mode 100644 index 928f029..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.Build.CppClean.log +++ /dev/null @@ -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 diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.dll.recipe b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.dll.recipe deleted file mode 100644 index a2b1add..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.dll.recipe +++ /dev/null @@ -1,11 +0,0 @@ - - - - - C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\x64\Release\DevourUnlockerDirectX11.dll - - - - - - \ No newline at end of file diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.iobj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.iobj deleted file mode 100644 index 6efe7ba..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.iobj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.ipdb b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.ipdb deleted file mode 100644 index 9afed70..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.ipdb and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.log b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.log deleted file mode 100644 index daa5303..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.log +++ /dev/null @@ -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 diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.vcxproj.FileListAbsolute.txt b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.vcxproj.FileListAbsolute.txt deleted file mode 100644 index bf20ffb..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/DevourUnlockerDirectX11.vcxproj.FileListAbsolute.txt +++ /dev/null @@ -1 +0,0 @@ -C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\DevourUnlockerDirectX11\x64\Release\DevourUnlockerDirectX11.vcxproj.CopyComplete diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/_IsIncrementalBuild b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/_IsIncrementalBuild deleted file mode 100644 index 0e3fcce..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/_IsIncrementalBuild +++ /dev/null @@ -1 +0,0 @@ -x64\Release\\_IsIncrementalBuild diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/dllmain.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/dllmain.obj deleted file mode 100644 index 9ce8c42..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/dllmain.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui.obj deleted file mode 100644 index 1673430..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_demo.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_demo.obj deleted file mode 100644 index 49a67a7..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_demo.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_draw.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_draw.obj deleted file mode 100644 index 88f602c..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_draw.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_impl_dx11.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_impl_dx11.obj deleted file mode 100644 index 07d3bb7..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_impl_dx11.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_impl_win32.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_impl_win32.obj deleted file mode 100644 index 6b07105..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_impl_win32.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_tables.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_tables.obj deleted file mode 100644 index 08380e8..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_tables.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_widgets.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_widgets.obj deleted file mode 100644 index 539c052..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/imgui_widgets.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/kiero.obj b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/kiero.obj deleted file mode 100644 index fac0f78..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/kiero.obj and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/vc143.pdb b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/vc143.pdb deleted file mode 100644 index eabcd71..0000000 Binary files a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/vc143.pdb and /dev/null differ diff --git a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/vcpkg.applocal.log b/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/vcpkg.applocal.log deleted file mode 100644 index b36c32f..0000000 --- a/DirectX11 Edition/DevourUnlockerDirectX11/x64/Release/vcpkg.applocal.log +++ /dev/null @@ -1,2 +0,0 @@ - -C:\Users\RyfiusLeeker\source\repos\DevourUnlockerDirectX11\x64\Release\minhook.x64.dll diff --git a/DirectX11 Edition/README_LEGACY.md b/DirectX11 Edition/README_LEGACY.md new file mode 100644 index 0000000..9c40555 --- /dev/null +++ b/DirectX11 Edition/README_LEGACY.md @@ -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`. diff --git a/README.md b/README.md index a9165f0..cd8e5a3 100644 --- a/README.md +++ b/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% +``` -![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. diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..5501266 --- /dev/null +++ b/scripts/install.ps1 @@ -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 diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..3c2eab4 --- /dev/null +++ b/scripts/install.sh @@ -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" diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..dafa438 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,82 @@ +#include "config.hpp" +#include +#include + +#ifdef _WIN32 +#include +#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 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 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(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 diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 0000000..a06eb39 --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include + +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 robeOverrides; + std::unordered_map 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 allRobes(); + static std::vector allCharacters(); +}; + +} // namespace devour diff --git a/src/gui/overlay.cpp b/src/gui/overlay.cpp new file mode 100644 index 0000000..d3da269 --- /dev/null +++ b/src/gui/overlay.cpp @@ -0,0 +1,134 @@ +#include "overlay.hpp" +#include "../config.hpp" +#include "../patches.hpp" +#include +#ifdef _WIN32 +#include +#include +#include +#else +// SDL backends are only used when SDL2 is available; stubs otherwise +#if __has_include() +#include +#include +#endif +#endif +#include + +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 diff --git a/src/gui/overlay.hpp b/src/gui/overlay.hpp new file mode 100644 index 0000000..ee5a637 --- /dev/null +++ b/src/gui/overlay.hpp @@ -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 diff --git a/src/il2cpp_resolver.cpp b/src/il2cpp_resolver.cpp new file mode 100644 index 0000000..5375b1a --- /dev/null +++ b/src/il2cpp_resolver.cpp @@ -0,0 +1,158 @@ +#include "il2cpp_resolver.hpp" +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +namespace devour { + +#ifdef _WIN32 +static HMODULE g_gameAsm = nullptr; + +uintptr_t Il2CppResolver::gameAssemblyBase() { + return reinterpret_cast(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(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 Il2CppResolver::scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size) { + std::vector 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(start + i)[j] != pattern[j]) { ok = false; break; } + } + if (ok) out.push_back(reinterpret_cast(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(getProc("il2cpp_domain_get")); + auto il2cpp_thread_attach = reinterpret_cast(getProc("il2cpp_thread_attach")); + auto klass_from_name = reinterpret_cast(getProc("il2cpp_class_from_name")); + auto class_get_method = reinterpret_cast(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(reinterpret_cast(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(base + i), str, slen) == 0) { + // Check null terminator + if (*reinterpret_cast(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(base + i); + if (p[0] == 0x48 && p[1] == 0x8D && p[2] == 0x0D) { + int32_t disp = *reinterpret_cast(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(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 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 diff --git a/src/il2cpp_resolver.hpp b/src/il2cpp_resolver.hpp new file mode 100644 index 0000000..63e1992 --- /dev/null +++ b/src/il2cpp_resolver.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include + +// 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 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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..eaedc18 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,86 @@ +#include "config.hpp" +#include "patches.hpp" +#include "il2cpp_resolver.hpp" +#ifdef _WIN32 +#include +#include +#include +#include +#include +#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 +__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 diff --git a/src/patches.cpp b/src/patches.cpp new file mode 100644 index 0000000..5196dbf --- /dev/null +++ b/src/patches.cpp @@ -0,0 +1,121 @@ +#include "patches.hpp" +#include "il2cpp_resolver.hpp" +#include "config.hpp" +#include + +#ifdef _WIN32 +#include +#include +#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(reinterpret_cast(ilStr) + 0x10); + wchar_t* chars = reinterpret_cast(reinterpret_cast(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 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 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 diff --git a/src/patches.hpp b/src/patches.hpp new file mode 100644 index 0000000..73b3a04 --- /dev/null +++ b/src/patches.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace devour { + +bool installPatches(); +void removePatches(); +bool patchesInstalled(); + +} // namespace devour diff --git a/src/platform/common.cpp b/src/platform/common.cpp new file mode 100644 index 0000000..c33f068 --- /dev/null +++ b/src/platform/common.cpp @@ -0,0 +1,7 @@ +#include +#ifdef _WIN32 +#include +#endif +// common helpers + +void logInfo(const char* msg) { std::cout << "[DevourUnlocker] " << msg << "\n"; } diff --git a/src/platform/linux.cpp b/src/platform/linux.cpp new file mode 100644 index 0000000..0959c83 --- /dev/null +++ b/src/platform/linux.cpp @@ -0,0 +1,10 @@ +#ifndef _WIN32 +#include +// 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 diff --git a/src/platform/win32.cpp b/src/platform/win32.cpp new file mode 100644 index 0000000..29e4703 --- /dev/null +++ b/src/platform/win32.cpp @@ -0,0 +1,67 @@ +#ifdef _WIN32 +#include +#include +#include +#include "../gui/overlay.hpp" +#include "../config.hpp" +#include "../patches.hpp" +#include + +// 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 diff --git a/src/platform/winhttp_exports.def b/src/platform/winhttp_exports.def new file mode 100644 index 0000000..2a23536 --- /dev/null +++ b/src/platform/winhttp_exports.def @@ -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 diff --git a/src/platform/winhttp_proxy.cpp b/src/platform/winhttp_proxy.cpp new file mode 100644 index 0000000..16fb6fe --- /dev/null +++ b/src/platform/winhttp_proxy.cpp @@ -0,0 +1,37 @@ +#ifdef _WIN32 +#include +#include + +// 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 diff --git a/standalone/devour_unlocker.py b/standalone/devour_unlocker.py new file mode 100755 index 0000000..48fc82e --- /dev/null +++ b/standalone/devour_unlocker.py @@ -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() diff --git a/standalone/gui.py b/standalone/gui.py new file mode 100755 index 0000000..756a7f0 --- /dev/null +++ b/standalone/gui.py @@ -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("", 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() diff --git a/standalone/requirements.txt b/standalone/requirements.txt new file mode 100644 index 0000000..5c053d3 --- /dev/null +++ b/standalone/requirements.txt @@ -0,0 +1,3 @@ +# no external deps for CLI; Tkinter is stdlib +# For PyInstaller bundle: +# pyinstaller --onefile gui.py --name DevourUnlocker-GUI diff --git a/tools/dump_metadata.py b/tools/dump_metadata.py new file mode 100755 index 0000000..e134745 --- /dev/null +++ b/tools/dump_metadata.py @@ -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)