diff --git a/CMakeLists.txt b/CMakeLists.txt index 8119395..ec89636 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/scripts/launch.sh b/scripts/launch.sh new file mode 100755 index 0000000..964b512 --- /dev/null +++ b/scripts/launch.sh @@ -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 diff --git a/src/il2cpp_resolver.cpp b/src/il2cpp_resolver.cpp index 768c65a..da975dd 100644 --- a/src/il2cpp_resolver.cpp +++ b/src/il2cpp_resolver.cpp @@ -1,5 +1,6 @@ #include "il2cpp_resolver.hpp" #include +#include #include #include @@ -42,6 +43,17 @@ bool Il2CppResolver::init() { return false; } +static bool waitForDomain(int timeoutMs = 30000){ + auto domain_get = reinterpret_cast(Il2CppResolver::getProc("il2cpp_domain_get")); + if(!domain_get) return false; + for(int i=0;i Il2CppResolver::scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size) { std::vector out; @@ -57,59 +69,86 @@ std::vector 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(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(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")); + auto domain_get_assemblies = reinterpret_cast(getProc("il2cpp_domain_get_assemblies")); + auto assembly_get_image = reinterpret_cast(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(reinterpret_cast(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(reinterpret_cast(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(getProc("il2cpp_image_get_class_count")); + auto image_get_class = reinterpret_cast(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(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 + // 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(base + i), str, slen) == 0) { - // Check null terminator - if (*reinterpret_cast(base + i + slen) == '\0') { - if (occurrence-- == 0) { strAddr = base + i; break; } - } + const char* start = reinterpret_cast(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(p); break; } } + p++; } if (!strAddr) return {str, nullptr}; diff --git a/src/main.cpp b/src/main.cpp index 35a6baa..143ff8d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,8 +7,18 @@ #include #include #include +#include +#include #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); diff --git a/src/patches.cpp b/src/patches.cpp index 097d934..50fbc1d 100644 --- a/src/patches.cpp +++ b/src/patches.cpp @@ -2,12 +2,24 @@ #include "il2cpp_resolver.hpp" #include "config.hpp" #include +#include +#include #ifdef _WIN32 #include #include #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; } diff --git a/src/platform/win32.cpp b/src/platform/win32.cpp index 75077ba..3b130b9 100644 --- a/src/platform/win32.cpp +++ b/src/platform/win32.cpp @@ -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 diff --git a/src/platform/winhttp_proxy.cpp b/src/platform/winhttp_proxy.cpp index 48e9f6e..b775686 100644 --- a/src/platform/winhttp_proxy.cpp +++ b/src/platform/winhttp_proxy.cpp @@ -1,37 +1,43 @@ #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. +#include 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 diff --git a/standalone/devour_unlocker.py b/standalone/devour_unlocker.py index ec3711b..81fad1f 100755 --- a/standalone/devour_unlocker.py +++ b/standalone/devour_unlocker.py @@ -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','')}") + 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)