v2.0.3: fix winhttp proxy forwarding + MH already init + standalone auto-deploy
Some checks are pending
Build DevourUnlocker / windows-cross (push) Waiting to run
Build DevourUnlocker / linux (push) Waiting to run
Build DevourUnlocker / standalone (push) Waiting to run

- winhttp_proxy: runtime forwarding via GetProcAddress to C:\Windows\System32\winhttp.dll (fixes unimplemented WinHttpGetIEProxyConfigForCurrentUser crash, was forwarding to winhttp_real.dll which doesn't exist)
- patches: handle MH_ERROR_ALREADY_INITIALIZED, log to DevourUnlocker.log, background retry for UpdateLocked* (fixes pets not found when IL2CPP not ready)
- config: all 7 categories (pets/perks etc) global toggles, standalone now auto-deploys DLLs via injector.ensure_mod_deployed on unlock --all
- injector: find_built_dlls includes /tmp/devour_win_build, auto mingw cross-build + fallback to install.sh fetch
- scripts/launch.sh: sets WINEDLLOVERRIDES="winhttp=n,b" and launches via Proton
- diagnose: checks winhttp.dll presence, DevourUnlocker.log, WINEDLLOVERRIDES hint
- Verified: winhttp.dll 1.5M exports WinHttpGetIEProxyConfigForCurrentUser correctly forwarded, no longer crashes, Present hooked, but UpdateLockedPets still not found — needs IL2CPP enumeration fix
This commit is contained in:
MiTHRAL 2026-08-21 22:46:42 -04:00
parent 8da6b2d279
commit 77f86c00ac
8 changed files with 294 additions and 67 deletions

View file

@ -65,16 +65,16 @@ else()
target_compile_definitions(devour_core PUBLIC DEVOUR_LINUX=1)
endif()
# --- winhttp.dll proxy (Windows) ---
# --- winhttp.dll proxy (Windows) — no static forwarding to winhttp_real (that DLL doesn't exist, caused load fail).
# We ship a minimal winhttp.dll that only exports DllMain for hooking; WinHttp APIs are not needed for Devour's unlock.
# If you need full WinHttp proxy, install winhttp_orig.dll alongside and use runtime forwarding in winhttp_proxy.cpp.
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)

58
scripts/launch.sh Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Launch Devour with DevourUnlocker via Proton (sets WINEDLLOVERRIDES so winhttp.dll is loaded)
# Usage: ./scripts/launch.sh or bash scripts/launch.sh --diagnose
set -euo pipefail
GAME_DIR="${GAME_DIR:-$HOME/.local/share/Steam/steamapps/common/Devour}"
PREFX="${WINEPREFIX:-$HOME/.local/share/Steam/steamapps/compatdata/1274570/pfx}"
PROTON="${PROTON:-$HOME/.local/share/Steam/compatibilitytools.d/GE-Proton10-34/proton}"
if [[ ! -f "$GAME_DIR/DEVOUR.exe" ]]; then
echo "[!] No DEVOUR.exe at $GAME_DIR — set GAME_DIR=/path"
exit 1
fi
if [[ ! -f "$GAME_DIR/winhttp.dll" ]]; then
echo "[!] winhttp.dll not found beside DEVOUR.exe — running injector --deploy-only..."
python3 "$(dirname "$0")/../standalone/injector.py" --deploy-only || true
fi
if [[ ! -f "$GAME_DIR/winhttp.dll" ]]; then
echo "[!] Still no winhttp.dll — build or run scripts/install.sh"
exit 1
fi
echo "[*] Game: $GAME_DIR"
echo "[*] Prefix: $PREFX"
echo "[*] Proton: $PROTON"
echo "[*] Launching with WINEDLLOVERRIDES=\"winhttp=n,b\" — check $GAME_DIR/DevourUnlocker.log after"
echo "[*] If Steam launch, instead set: Steam → Devour → Properties → Launch Options: WINEDLLOVERRIDES=\"winhttp=n,b\" %command%"
if [[ "${1:-}" == "--diagnose" ]]; then
python3 "$(dirname "$0")/../standalone/devour_unlocker.py" diagnose
exit 0
fi
export WINEPREFIX="$PREFX"
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/1274570"
export STEAM_COMPAT_CLIENT_INSTALL_PATH="$HOME/.local/share/Steam"
export SteamAppId=1274570
export WINEDLLOVERRIDES="winhttp=n,b"
export WINEDEBUG=-all
# Kill stale wineserver
wineserver -k 2>/dev/null || true
sleep 1
rm -f "$GAME_DIR/DevourUnlocker.log"
# Launch via proton (background, then wait 5s and show log)
timeout 30 "$PROTON" run "$GAME_DIR/DEVOUR.exe" &
PROTON_PID=$!
echo "[*] Proton pid $PROTON_PID — waiting 15s for DevourUnlocker.log..."
sleep 15
if [[ -f "$GAME_DIR/DevourUnlocker.log" ]]; then
echo "--- DevourUnlocker.log ---"
cat "$GAME_DIR/DevourUnlocker.log"
else
echo "[!] No DevourUnlocker.log — DLL not loaded. Check WINEDLLOVERRIDES and that libgcc etc are beside winhttp.dll"
ls -lh "$GAME_DIR"/*.dll 2>&1 | head -n 20
fi
echo "[*] Game should be running — press F6 for GUI, check in-game wardrobe. This script will exit but game keeps running."
wait $PROTON_PID 2>/dev/null || true

View file

@ -1,5 +1,6 @@
#include "il2cpp_resolver.hpp"
#include <string>
#include <cstring>
#include <thread>
#include <chrono>
@ -42,6 +43,17 @@ bool Il2CppResolver::init() {
return false;
}
static bool waitForDomain(int timeoutMs = 30000){
auto domain_get = reinterpret_cast<void*(*)()>(Il2CppResolver::getProc("il2cpp_domain_get"));
if(!domain_get) return false;
for(int i=0;i<timeoutMs/100;i++){
void* d = domain_get();
if(d) 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;
@ -57,59 +69,86 @@ std::vector<void*> Il2CppResolver::scanPattern(const char* pattern, const char*
}
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"));
// Wait for IL2CPP domain to be ready — early MainThread may call before Unity inits IL2CPP
if (!waitForDomain(15000)) {
return resolveByStringXref(methodName);
}
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(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"));
auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto assembly_get_image = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
if (!il2cpp_domain_get || !klass_from_name || !class_get_method) {
if (!il2cpp_domain_get || !klass_from_name || !class_get_method || !domain_get_assemblies || !assembly_get_image) {
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;
size_t nAssemblies = 0;
const void** assemblies = nullptr;
for(int w=0; w<100; w++){
assemblies = domain_get_assemblies(domain, &nAssemblies);
if(assemblies && nAssemblies>0) break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!assemblies || nAssemblies==0) return resolveByStringXref(methodName);
const char* candidates[] = {klassName, "NolanRankController", "LobbyController", "InventoryController", "GameController", nullptr};
for (size_t ai = 0; ai < nAssemblies; ++ai) {
void* image = assembly_get_image((void*)assemblies[ai]);
if (!image) continue;
for (int ci = 0; candidates[ci]; ++ci) {
void* klass = klass_from_name(image, "", candidates[ci]);
if (!klass) continue;
void* method = class_get_method(klass, methodName);
if (method) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(method) + 0x18);
if (ptr) return {methodName, ptr};
}
}
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};
}
// Brute force: enumerate all classes in all images for methodName (UpdateLocked* etc live in unknown class)
auto image_get_class_count = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_image_get_class_count"));
auto image_get_class = reinterpret_cast<void*(*)(void*, int)>(getProc("il2cpp_image_get_class"));
if (image_get_class_count && image_get_class) {
for (size_t ai = 0; ai < nAssemblies; ++ai) {
void* image = assembly_get_image((void*)assemblies[ai]);
if (!image) continue;
int count = image_get_class_count(image);
for (int ci = 0; ci < count; ++ci) {
void* klass = image_get_class(image, ci);
if (!klass) continue;
void* method = class_get_method(klass, methodName);
if (method) {
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
// Fast find via memchr + memcmp — don't require null terminator (IsRobeUnlocked appears inside longer log string)
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; }
}
const char* start = reinterpret_cast<const char*>(base);
const char* end = start + sz - slen - 1;
const char* p = start;
while (p < end) {
p = (const char*)memchr(p, str[0], end - p);
if (!p) break;
if (memcmp(p, str, slen)==0) {
if (occurrence--==0) { strAddr = reinterpret_cast<uintptr_t>(p); break; }
}
p++;
}
if (!strAddr) return {str, nullptr};

View file

@ -7,8 +7,18 @@
#include <d3d11.h>
#include <dxgi.h>
#include <iostream>
#include <fstream>
#include <filesystem>
#include "gui/overlay.hpp"
static void logFile(const char* msg){
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr, exe, MAX_PATH);
std::filesystem::path p(exe); p = p.parent_path() / "DevourUnlocker.log";
std::ofstream f(p, std::ios::app); f << msg << std::endl;
}catch(...){}
}
using PresentFn = HRESULT(__stdcall*)(IDXGISwapChain*, UINT, UINT);
extern HRESULT __stdcall hkPresent(IDXGISwapChain*, UINT, UINT);
extern PresentFn oPresent;
@ -16,11 +26,14 @@ extern PresentFn oPresent;
static HMODULE g_self = nullptr;
DWORD WINAPI MainThread(LPVOID) {
logFile("[DevourUnlocker] v2 winhttp proxy loaded — DllMain");
AllocConsole();
SetConsoleTitleA("DevourUnlocker v2");
FILE* f; freopen_s(&f, "CONOUT$", "w", stdout);
std::cout << "[DevourUnlocker] v2 winhttp proxy loaded\n";
logFile("[DevourUnlocker] MainThread start");
devour::Config::load(devour::Config::defaultPath());
logFile("[DevourUnlocker] Config loaded");
// Wait for d3d11 then hook Present via vtable
HMODULE d3d11 = nullptr;
@ -42,11 +55,16 @@ DWORD WINAPI MainThread(LPVOID) {
MH_CreateHook(target, (void*)hkPresent, (void**)&oPresent);
MH_EnableHook(target);
std::cout << "[DevourUnlocker] Present hooked at " << target << "\n";
logFile("[DevourUnlocker] Present hooked");
if (swap) swap->Release();
if (dev) dev->Release();
if (ctx) ctx->Release();
// Also install il2cpp patches immediately (not only on Present)
devour::installPatches();
logFile("[DevourUnlocker] Patches installed from MainThread");
} else {
std::cout << "[DevourUnlocker] D3D11 dummy failed hr=" << std::hex << hr << " — patches still active\n";
logFile("[DevourUnlocker] D3D11 dummy failed, installing patches");
devour::installPatches();
}
DestroyWindow(hwnd);

View file

@ -2,12 +2,24 @@
#include "il2cpp_resolver.hpp"
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <windows.h>
#include <MinHook.h>
#endif
static void logPatch(const char* s){
#ifdef _WIN32
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr, exe, MAX_PATH);
std::filesystem::path p(exe); p = p.parent_path() / "DevourUnlocker.log";
std::ofstream f(p, std::ios::app); f << s << std::endl;
}catch(...){}
#endif
}
namespace devour {
#ifdef _WIN32
@ -114,31 +126,39 @@ static void __fastcall hUpdateLockedRobes(void* thiz, void* method) {
}
static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*)) {
if (!addr) { std::cerr << "[DevourUnlocker] " << name << " not found\n"; return false; }
if (!addr) { std::cerr << "[DevourUnlocker] " << name << " not found\n"; logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; }
if (!shouldUnlock) return false;
void* orig = nullptr;
if (MH_CreateHook(addr, (void*)hook, &orig) == MH_OK) {
MH_EnableHook(addr);
std::cout << "[DevourUnlocker] Hooked " << name << "\n";
logPatch((std::string("[DevourUnlocker] Hooked ")+name).c_str());
return true;
}
std::cerr << "[DevourUnlocker] Hook " << name << " failed\n";
logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed").c_str());
return false;
}
bool installPatches() {
if (g_installed) return true;
logPatch("[DevourUnlocker] installPatches enter");
if (g_installed) { logPatch("[DevourUnlocker] already installed"); return true; }
if (!g_mhInit) {
if (MH_Initialize() != MH_OK) {
std::cerr << "[DevourUnlocker] MH_Initialize failed\n";
MH_STATUS st = MH_Initialize();
if (st != MH_OK && st != MH_ERROR_ALREADY_INITIALIZED) {
std::cerr << "[DevourUnlocker] MH_Initialize failed " << st << "\n";
logPatch(("[DevourUnlocker] MH_Initialize failed "+std::to_string(st)).c_str());
return false;
}
g_mhInit = true;
logPatch("[DevourUnlocker] MH_Initialize ok (or already)");
}
if (!Il2CppResolver::init()) {
std::cerr << "[DevourUnlocker] GameAssembly not found\n";
logPatch("[DevourUnlocker] GameAssembly not found");
return false;
}
logPatch("[DevourUnlocker] GameAssembly found");
auto robe = Il2CppResolver::isRobeUnlocked();
auto ch = Il2CppResolver::isCharacterUnlocked();
auto hasPerk = Il2CppResolver::hasPerk();
@ -193,7 +213,34 @@ bool installPatches() {
any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes);
any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits);
any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes);
// If UpdateLocked* not found, it may be because IL2CPP not yet ready — retry in background (fixes pets not unlocking)
bool needRetry = !updPets.valid() || !updPerks.valid() || !updFlash.valid() || !updEmotes.valid() || !updOutfits.valid() || !updRobes.valid();
if (needRetry) {
logPatch("[DevourUnlocker] Some UpdateLocked* not found — scheduling retry in 3s (IL2CPP may not be ready)");
CreateThread(nullptr, 0, [](LPVOID)->DWORD{
for(int attempt=0; attempt<5; ++attempt){
Sleep(3000);
auto p1 = Il2CppResolver::updateLockedPets();
auto p2 = Il2CppResolver::updateLockedPerks();
auto p3 = Il2CppResolver::updateLockedFlashlights();
auto p4 = Il2CppResolver::updateLockedEmotes();
auto p5 = Il2CppResolver::updateLockedOutfits();
auto p6 = Il2CppResolver::updateLockedRobes();
bool any2=false;
if(p1.valid()) any2 |= hookUpdateLocked(p1.address, "UpdateLockedPets[retry]", Config::get().unlockPets, hUpdateLockedPets);
if(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks);
if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights);
if(p4.valid()) any2 |= hookUpdateLocked(p4.address, "UpdateLockedEmotes[retry]", Config::get().unlockEmotes, hUpdateLockedEmotes);
if(p5.valid()) any2 |= hookUpdateLocked(p5.address, "UpdateLockedOutfits[retry]", Config::get().unlockOutfits, hUpdateLockedOutfits);
if(p6.valid()) any2 |= hookUpdateLocked(p6.address, "UpdateLockedRobes[retry]", Config::get().unlockRobes, hUpdateLockedRobes);
if(any2){ logPatch("[DevourUnlocker] Retry hooked some UpdateLocked"); break; }
}
return 0;
}, nullptr, 0, nullptr);
// Also try IsRobe/HasPerk etc retry
}
g_installed = any;
logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed");
return any;
}

View file

@ -40,10 +40,11 @@ HRESULT __stdcall hkPresent(IDXGISwapChain* swap, UINT si, UINT flags) {
}
oWndProc = (WNDPROC)SetWindowLongPtr(g_hWnd, GWLP_WNDPROC, (LONG_PTR)WndProc);
devour::Config::load(devour::Config::defaultPath());
devour::installPatches();
// Don't block Present thread with heavy il2cpp scan — do in background
CreateThread(nullptr, 0, [](LPVOID)->DWORD{ devour::installPatches(); return 0; }, nullptr, 0, nullptr);
devour::gui::init(g_device, g_ctx, swap, g_hWnd);
g_init = true;
std::cout << "[DevourUnlocker] Present hook init ok\n";
std::cout << "[DevourUnlocker] Present hook init ok (patches background)\n";
} else return oPresent(swap, si, flags);
}
// Hotkeys

View file

@ -1,37 +1,43 @@
#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.
#include <winhttp.h>
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());
g_real = LoadLibraryW(L"C:\\Windows\\System32\\winhttp.dll");
if (!g_real) g_real = LoadLibraryW(L"C:\\Windows\\SysWOW64\\winhttp.dll");
if (!g_real) g_real = LoadLibraryA("winhttp.dll");
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;
#define GET(name) ((decltype(&name))GetProcAddress(getReal(), #name))
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpAddRequestHeaders(HINTERNET a0, LPCWSTR a1, DWORD a2, DWORD a3){
auto p = GET(WinHttpAddRequestHeaders); if(!p) return FALSE; return p(a0,a1,a2,a3);
}
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpCheckPlatform(){ auto p=GET(WinHttpCheckPlatform); if(!p) return FALSE; return p(); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpCloseHandle(HINTERNET a0){ auto p=GET(WinHttpCloseHandle); if(!p) return FALSE; return p(a0); }
extern "C" __declspec(dllexport) HINTERNET WINAPI WinHttpConnect(HINTERNET a0, LPCWSTR a1, INTERNET_PORT a2, DWORD a3){ auto p=GET(WinHttpConnect); if(!p) return nullptr; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpCrackUrl(LPCWSTR a0, DWORD a1, DWORD a2, LPURL_COMPONENTS a3){ auto p=GET(WinHttpCrackUrl); if(!p) return FALSE; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpCreateUrl(LPURL_COMPONENTS a0, DWORD a1, LPWSTR a2, LPDWORD a3){ auto p=GET(WinHttpCreateUrl); if(!p) return FALSE; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpDetectAutoProxyConfigUrl(DWORD a0, LPWSTR* a1){ auto p=GET(WinHttpDetectAutoProxyConfigUrl); if(!p) return FALSE; return p(a0,a1); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpGetDefaultProxyConfiguration(WINHTTP_PROXY_INFO* a0){ auto p=GET(WinHttpGetDefaultProxyConfiguration); if(!p) return FALSE; return p(a0); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpGetIEProxyConfigForCurrentUser(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG* a0){ auto p=GET(WinHttpGetIEProxyConfigForCurrentUser); if(!p) return FALSE; return p(a0); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpGetProxyForUrl(HINTERNET a0, LPCWSTR a1, WINHTTP_AUTOPROXY_OPTIONS* a2, WINHTTP_PROXY_INFO* a3){ auto p=GET(WinHttpGetProxyForUrl); if(!p) return FALSE; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) HINTERNET WINAPI WinHttpOpen(LPCWSTR a0, DWORD a1, LPCWSTR a2, LPCWSTR a3, DWORD a4){ auto p=GET(WinHttpOpen); if(!p) return nullptr; return p(a0,a1,a2,a3,a4); }
extern "C" __declspec(dllexport) HINTERNET WINAPI WinHttpOpenRequest(HINTERNET a0, LPCWSTR a1, LPCWSTR a2, LPCWSTR a3, LPCWSTR a4, LPCWSTR* a5, DWORD a6){ auto p=GET(WinHttpOpenRequest); if(!p) return nullptr; return p(a0,a1,a2,a3,a4,a5,a6); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpQueryAuthSchemes(HINTERNET a0, LPDWORD a1, LPDWORD a2, LPDWORD a3){ auto p=GET(WinHttpQueryAuthSchemes); if(!p) return FALSE; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpQueryDataAvailable(HINTERNET a0, LPDWORD a1){ auto p=GET(WinHttpQueryDataAvailable); if(!p) return FALSE; return p(a0,a1); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpQueryHeaders(HINTERNET a0, DWORD a1, LPCWSTR a2, LPVOID a3, LPDWORD a4, LPDWORD a5){ auto p=GET(WinHttpQueryHeaders); if(!p) return FALSE; return p(a0,a1,a2,a3,a4,a5); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpReadData(HINTERNET a0, LPVOID a1, DWORD a2, LPDWORD a3){ auto p=GET(WinHttpReadData); if(!p) return FALSE; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpReceiveResponse(HINTERNET a0, LPVOID a1){ auto p=GET(WinHttpReceiveResponse); if(!p) return FALSE; return p(a0,a1); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpSendRequest(HINTERNET a0, LPCWSTR a1, DWORD a2, LPVOID a3, DWORD a4, DWORD a5, DWORD_PTR a6){ auto p=GET(WinHttpSendRequest); if(!p) return FALSE; return p(a0,a1,a2,a3,a4,a5,a6); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpSetCredentials(HINTERNET a0, DWORD a1, DWORD a2, LPCWSTR a3, LPCWSTR a4, LPVOID a5){ auto p=GET(WinHttpSetCredentials); if(!p) return FALSE; return p(a0,a1,a2,a3,a4,a5); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpSetOption(HINTERNET a0, DWORD a1, LPVOID a2, DWORD a3){ auto p=GET(WinHttpSetOption); if(!p) return FALSE; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) WINHTTP_STATUS_CALLBACK WINAPI WinHttpSetStatusCallback(HINTERNET a0, WINHTTP_STATUS_CALLBACK a1, DWORD a2, DWORD_PTR a3){ auto p=GET(WinHttpSetStatusCallback); if(!p) return nullptr; return p(a0,a1,a2,a3); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpSetTimeouts(HINTERNET a0, int a1, int a2, int a3, int a4){ auto p=GET(WinHttpSetTimeouts); if(!p) return FALSE; return p(a0,a1,a2,a3,a4); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpTimeFromSystemTime(const SYSTEMTIME* a0, LPWSTR a1){ auto p=GET(WinHttpTimeFromSystemTime); if(!p) return FALSE; return p(a0,a1); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpTimeToSystemTime(LPCWSTR a0, SYSTEMTIME* a1){ auto p=GET(WinHttpTimeToSystemTime); if(!p) return FALSE; return p(a0,a1); }
extern "C" __declspec(dllexport) BOOL WINAPI WinHttpWriteData(HINTERNET a0, LPCVOID a1, DWORD a2, LPDWORD a3){ auto p=GET(WinHttpWriteData); if(!p) return FALSE; return p(a0,a1,a2,a3); }
#endif

View file

@ -100,11 +100,17 @@ def cmd_unlock(args):
sel_pets = args.pets.split(",") if args.pets else []
if args.all:
cfg.update({"unlockRobes":True,"unlockCharacters":True,"unlockPerks":True,"unlockPets":True,"unlockFlashlights":True,"unlockEmotes":True,"unlockOutfits":True})
# clear overrides — global true covers all
for k in ["robeOverrides","characterOverrides","perkOverrides","petOverrides","flashlightOverrides","emoteOverrides","outfitOverrides"]:
cfg.pop(k,None)
save_config(args.json, cfg)
print("[*] Unlocked ALL categories (robes/chars/perks/pets/flashlights/emotes/outfits)")
# Auto-deploy mod so standalone actually works (was JSON-only before → "does nothing")
try:
import injector
injector.ensure_mod_deployed(verbose=True)
print("[*] Mod DLLs ensured — launch with WINEDLLOVERRIDES=\"winhttp=n,b\" %command% or run: python standalone/injector.py")
except Exception as e:
print(f"[!] Could not auto-deploy mod: {e}")
return
# selective
cfg["unlockRobes"] = cfg["unlockCharacters"] = cfg["unlockPerks"] = cfg["unlockPets"] = cfg["unlockFlashlights"] = cfg["unlockEmotes"] = cfg["unlockOutfits"] = True
@ -130,15 +136,63 @@ def cmd_unlock(args):
cfg["petOverrides"] = {p: False for p in PETS}
save_config(args.json, cfg)
print("[*] Now launch Devour with winhttp.dll — selective unlock active via DevourUnlocker.json")
try:
import injector
ok = injector.ensure_mod_deployed(verbose=False)
if not ok:
print("[!] winhttp.dll not deployed — run: python standalone/injector.py --deploy-only")
else:
# also ensure launch hint
print("[*] Mod deployed — ensure Steam launch options: WINEDLLOVERRIDES=\"winhttp=n,b\" %command%")
except Exception as e:
print(f"[!] diagnose: {e}")
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 cmd_diagnose(args):
print("=== DevourUnlocker diagnose ===")
for p in devour_paths():
print(f"path {p} exists={p.exists()}")
gj = default_json_path()
print(f"json {gj} exists={gj.exists()}")
if gj.exists():
txt=gj.read_text()
print(txt[:2000])
try:
j=json.loads(txt)
print(f" unlocks: { {k:j[k] for k in j if 'unlock' in k} }")
except: pass
gd=None
for c in devour_paths():
if (c / "DEVOUR.exe").exists():
gd=c; break
for dll in ["winhttp.dll","DevourUnlocker.dll","libgcc_s_seh-1.dll","libstdc++-6.dll"]:
if gd:
print(f"{dll} at {gd/dll} exists={(gd/dll).exists()}")
if (gd/dll).exists():
print(f" size {(gd/dll).stat().st_size}")
log = gd / "DevourUnlocker.log" if gd else None
if log and log.exists():
print(f"\n--- {log} ({log.stat().st_size} bytes) ---")
print(log.read_text()[-6000:])
if "UpdateLockedPets not found" in log.read_text():
print("\n[!] Pets not hooked — IL2CPP not ready at first try. The retry thread should handle it, but if you see 'not found' after 10s, try restarting game or use BepInEx.")
else:
print(f"\n[!] No DevourUnlocker.log at {log} — DLL never loaded.")
print(" Fixes: 1) Ensure winhttp.dll is beside DEVOUR.exe (run: python standalone/injector.py --deploy-only)")
print(" 2) Set Steam launch options: WINEDLLOVERRIDES=\"winhttp=n,b\" %command%")
print(" 3) Or run: bash scripts/launch.sh (auto sets WINEDLLOVERRIDES and launches via Proton)")
# env check
print(f"\nENV WINEDLLOVERRIDES={os.environ.get('WINEDLLOVERRIDES','<not set>')}")
print("Steam launch options are stored in localconfig.vdf — check Steam UI if not set")
print("\n[Hint] For Proton, set Steam → Devour → Properties → Launch Options: WINEDLLOVERRIDES=\"winhttp=n,b\" %command%")
print("Or run: python standalone/injector.py (auto-deploys + launches) or bash scripts/launch.sh")
def main():
ap = argparse.ArgumentParser(description="Devour Standalone Unlocker")
ap.add_argument("--json", default=str(default_json_path()), help="path to DevourUnlocker.json")
@ -157,6 +211,10 @@ def main():
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)
sd = sub.add_parser("diagnose", help="why standalone does nothing — checks DLL + log")
sd.set_defaults(func=cmd_diagnose)
sl = sub.add_parser("launch", help="deploy DLLs + launch with WINEDLLOVERRIDES")
sl.set_defaults(func=lambda a: __import__("injector").launch_with_mod())
args = ap.parse_args()
args.func(args)