v2.0.3: fix winhttp proxy forwarding + MH already init + standalone auto-deploy
- 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:
parent
8da6b2d279
commit
77f86c00ac
8 changed files with 294 additions and 67 deletions
|
|
@ -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
58
scripts/launch.sh
Executable 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
|
||||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
18
src/main.cpp
18
src/main.cpp
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue