diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b1cb2e..2269a7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,10 +7,9 @@ on: workflow_dispatch: jobs: - # media-server is the only runner on git.mithraic.cloud (no windows-latest/ubuntu-latest). Forgejo Act: runs-on must exactly match runner labels. + # git.mithraic.cloud Forgejo runner: name media-server, labels [docker] (v6.4.0). Use `docker` to match. windows-cross: - runs-on: media-server - continue-on-error: true + runs-on: docker steps: - uses: actions/checkout@v4 with: { submodules: recursive } @@ -28,16 +27,17 @@ jobs: - name: Package run: | mkdir -p dist - cp build/winhttp.dll dist/ 2>/dev/null || cp build/*.dll dist/ 2>/dev/null || echo "no dll built (expected on cross without win32 SDK)" + cp build/version.dll dist/ 2>/dev/null || echo "no version.dll" + cp build/winhttp.dll dist/ 2>/dev/null || true cp build/DevourUnlocker.dll dist/ 2>/dev/null || true cp DevourUnlocker.json dist/ 2>/dev/null || echo '{"unlockRobes":true,"unlockCharacters":true,"unlockPerks":true,"unlockPets":true,"unlockFlashlights":true,"unlockEmotes":true,"unlockOutfits":true}' > dist/DevourUnlocker.json - zip -j DevourUnlocker_winhttp_${{ github.ref_name }}.zip dist/* || true + zip -j DevourUnlocker_version_${{ github.ref_name }}.zip dist/* || zip -j DevourUnlocker_winhttp_${{ github.ref_name }}.zip dist/* || true ls -lh dist/ - uses: actions/upload-artifact@v4 - with: { name: winhttp-cross, path: DevourUnlocker_winhttp_*.zip } + with: { name: version-cross, path: DevourUnlocker_version_*.zip } linux: - runs-on: media-server + runs-on: docker steps: - uses: actions/checkout@v4 - name: Install deps if apt @@ -51,7 +51,7 @@ jobs: with: { name: linux-so, path: build/*.so } standalone: - runs-on: media-server + runs-on: docker steps: - uses: actions/checkout@v4 - run: python3 -m py_compile standalone/devour_unlocker.py standalone/gui.py tools/dump_metadata.py standalone/injector.py 2>&1 || python3 -m py_compile standalone/devour_unlocker.py standalone/gui.py tools/dump_metadata.py diff --git a/.gitignore b/.gitignore index 751c5ff..c94e6db 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,10 @@ BepInEx/config/ standalone/dist/ standalone/build/ +# BepInEx build +BepInEx/obj/ +BepInEx/bin/ + # IDE *.user *.suo diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e6048e..f8f67dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,16 +79,16 @@ if(BUILD_WINHTTP_PROXY AND WIN32) target_link_options(winhttp PRIVATE -static-libgcc -static-libstdc++ -Wl,-Bstatic -lwinpthread -Wl,-Bdynamic) endif() - # version.dll is minimal — just to satisfy loader, unlock is via collectables.json (no IL2CPP hook, no crash) + # version.dll is primary unlocker — full hooks (all categories) + proxy, no WINEDLLOVERRIDES needed for user (installer handles it silently) add_library(version SHARED src/platform/version_proxy.cpp src/main_version.cpp ) - target_link_libraries(version PRIVATE kernel32) + target_link_libraries(version PRIVATE devour_core kernel32) set_target_properties(version PROPERTIES OUTPUT_NAME "version" PREFIX "" SUFFIX ".dll") target_compile_definitions(version PRIVATE VERSION_PROXY=1) if(MINGW) - target_link_options(version PRIVATE -static-libgcc -static-libstdc++) + target_link_options(version PRIVATE -static-libgcc -static-libstdc++ -Wl,-Bstatic -lwinpthread -Wl,-Bdynamic) endif() add_library(DevourUnlocker SHARED src/main.cpp) diff --git a/scripts/install.sh b/scripts/install.sh index 77e5363..6fde2a0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -120,9 +120,9 @@ for dll in version.dll winhttp.dll; do fi done -# Prefer version.dll (less flagged) — copy both for compat -if [[ -f "$TMP/version.dll" ]]; then cp -v "$TMP/version.dll" "$GAME_DIR/version.dll"; echo "[*] Installed version.dll (primary, less detectable)"; fi -if [[ -f "$TMP/winhttp.dll" ]]; then cp -v "$TMP/winhttp.dll" "$GAME_DIR/winhttp.dll"; echo "[*] Installed winhttp.dll (compat)"; fi +# version.dll is sole unlocker (full hooks, less flagged than winhttp) — winhttp kept only as optional compat +if [[ -f "$TMP/version.dll" ]]; then cp -v "$TMP/version.dll" "$GAME_DIR/version.dll"; echo "[*] Installed version.dll (primary unlocker — all categories)"; fi +if [[ -f "$TMP/winhttp.dll" ]]; then cp -v "$TMP/winhttp.dll" "$GAME_DIR/winhttp.dll"; echo "[*] Installed winhttp.dll (optional compat, not needed)"; fi cp -v "$TMP/DevourUnlocker.dll" "$GAME_DIR/DevourUnlocker.dll" 2>/dev/null || true # Record GameAssembly hash for future update checks GA_SHA=$(sha1sum "$GAME_DIR/GameAssembly.dll" 2>/dev/null | cut -d' ' -f1 || echo "") @@ -137,36 +137,29 @@ echo "[*] Patching Steam Cloud collectables.json (proper game call: HasCollected python3 "$(dirname "$0")/../standalone/devour_unlocker.py" unlock --all 2>&1 | tail -n 20 || echo "[!] Could not patch collectables.json — will still unlock via runtime hook" echo "[*] Installed to $GAME_DIR" -# Auto-set Steam launch options for Proton (so user doesn't need to do it manually) +# Auto-set Steam launch options for Proton silently (so user never needs manual WINEDLLOVERRIDES) if [[ "$OS" == "linux" ]]; then - # Try to set LaunchOptions in localconfig.vdf for 1274570 to include WINEDLLOVERRIDES for lc in "$HOME/.local/share/Steam/userdata/"*/config/localconfig.vdf; do if [[ -f "$lc" ]] && grep -q "1274570" "$lc"; then - # Backup cp "$lc" "$lc.bak.$(date +%s)" 2>/dev/null || true - # Use python to edit VDF (VDF is not JSON, but we can do crude sed for LaunchOptions) python3 -c " -import pathlib, re +import pathlib p=pathlib.Path('$lc') t=p.read_text(errors='ignore') -# If LaunchOptions already contains WINEDLLOVERRIDES, leave it if 'WINEDLLOVERRIDES' not in t: - # Find \"1274570\" block and add LaunchOptions - # VDF snippet for 1274570 is like \"1274570\" { ... \"LaunchOptions\" \"...\" } - # If LaunchOptions not present, add it if '\"LaunchOptions\"' not in t.split('\"1274570\"')[1].split('}')[0]: - # Insert LaunchOptions after 1274570 block start t=t.replace('\"1274570\"', '\"1274570\"\n\t\t\t\t\"LaunchOptions\"\t\t\"WINEDLLOVERRIDES=version=n,b %command%\"', 1) - open('$lc','w').write(t) - print('[*] Auto-set Steam launch options for Devour to WINEDLLOVERRIDES=\"version=n,b\" %command%') + pathlib.Path('$lc').write_text(t) + print('[*] Auto-set Steam launch options for Devour to WINEDLLOVERRIDES=\"version=n,b\" %command% (silent, no user action needed)') " 2>&1 | head -n 5 break fi done - echo "[*] Linux/Proton: version.dll is auto-loaded beside DEVOUR.exe (if LaunchOptions not auto-set, set Steam → Devour → Properties → Launch Options: WINEDLLOVERRIDES=\"version=n,b\" %command% or just use scripts/launch.sh)" - echo "[*] Or run: bash $(dirname "$0")/launch.sh (auto sets WINEDLLOVERRIDES and launches via Proton)" + echo "[*] Linux/Proton: version.dll is sole unlocker beside DEVOUR.exe — Steam Launch Options now auto-set, just press Play. No manual WINEDLLOVERRIDES needed." + echo "[*] Fallback: bash $(dirname "$0")/launch.sh (also auto-sets WINEDLLOVERRIDES)" else - echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded — no launch option needed. Just launch Devour." + echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded (UnityPlayer.dll imports VERSION.dll) — no launch option needed. Just launch Devour." fi +echo "[*] version.dll = full unlock (robes/characters/perks/pets/flashlights/emotes/outfits) — go in game, everything unlocked" echo "[*] Verify: python $(dirname "$0")/../standalone/devour_unlocker.py diagnose" rm -rf "$TMP" diff --git a/scripts/launch.sh b/scripts/launch.sh index 964b512..a755350 100755 --- a/scripts/launch.sh +++ b/scripts/launch.sh @@ -10,20 +10,20 @@ 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..." +if [[ ! -f "$GAME_DIR/version.dll" ]]; then + echo "[!] version.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" +if [[ ! -f "$GAME_DIR/version.dll" ]]; then + echo "[!] Still no version.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%" +echo "[*] Launching with version.dll (primary unlocker) — no manual Steam Launch Options needed if installed via install.sh" +echo "[*] Check $GAME_DIR/DevourUnlocker.log after" if [[ "${1:-}" == "--diagnose" ]]; then python3 "$(dirname "$0")/../standalone/devour_unlocker.py" diagnose @@ -34,7 +34,13 @@ 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" +# version.dll is primary unlocker (UnityPlayer.dll imports VERSION.dll, so native version.dll in game dir is auto-loaded on Windows; +# on Proton/Wine we still set WINEDLLOVERRIDES silently so user never needs to set it manually) +if [[ -z "${WINEDLLOVERRIDES:-}" ]]; then + export WINEDLLOVERRIDES="version=n,b" +else + export WINEDLLOVERRIDES="version=n,b;${WINEDLLOVERRIDES}" +fi export WINEDEBUG=-all # Kill stale wineserver diff --git a/src/main_version.cpp b/src/main_version.cpp index b3f3d91..9f16a6f 100644 --- a/src/main_version.cpp +++ b/src/main_version.cpp @@ -1,10 +1,27 @@ +#include "config.hpp" +#include "patches.hpp" #ifdef _WIN32 #include +#include +static void logVer(const char* m){ + char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH); + char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/'); + std::string p = s? std::string(exe,s-exe)+"\\DevourUnlocker.log":"DevourUnlocker.log"; + std::ofstream f(p,std::ios::app); f<')}") - 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("Steam launch options are stored in localconfig.vdf — check Steam UI if not set (install.sh now auto-sets it for version.dll)") + print("\n[Hint] version.dll is sole unlocker — just launch Devour from Steam after install.sh. No manual WINEDLLOVERRIDES needed.") print("Or run: python standalone/injector.py (auto-deploys + launches) or bash scripts/launch.sh") def main(): diff --git a/standalone/injector.py b/standalone/injector.py index 6d5ef94..caacffa 100755 --- a/standalone/injector.py +++ b/standalone/injector.py @@ -44,6 +44,8 @@ def find_built_dlls(): ] dlls = {} for r in roots: + if (r / "version.dll").exists(): + dlls["version"] = r / "version.dll" if (r / "winhttp.dll").exists(): dlls["winhttp"] = r / "winhttp.dll" if (r / "DevourUnlocker.dll").exists(): @@ -57,13 +59,13 @@ def ensure_mod_deployed(game_dir: pathlib.Path = None, verbose=True): if not game_dir or not game_dir.exists(): print(f"[!] Could not find Devour at {game_dir}") return False - # Need winhttp.dll + DevourUnlocker.dll — build if missing + # Need version.dll (primary) — build if missing dlls = find_built_dlls() # If no built DLLs, try to build or fetch - if "winhttp" not in dlls: + if "version" not in dlls and "winhttp" not in dlls: # Try to build via mingw if available if verbose: - print("[*] No winhttp.dll found, attempting mingw cross-build...") + print("[*] No version.dll found, attempting mingw cross-build...") try: # Use dedicated win_build dir to avoid clobbering linux build subprocess.run(["cmake", "-B", "/tmp/devour_win_build", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_SYSTEM_NAME=Windows", @@ -71,13 +73,13 @@ def ensure_mod_deployed(game_dir: pathlib.Path = None, verbose=True): cwd=pathlib.Path(__file__).parent.parent, check=False, capture_output=True, timeout=60) subprocess.run(["cmake", "--build", "/tmp/devour_win_build"], cwd=pathlib.Path(__file__).parent.parent, check=False, capture_output=True, timeout=180) dlls = find_built_dlls() - if "winhttp" in dlls and verbose: - print(f"[*] Built {dlls['winhttp']}") + if ("version" in dlls or "winhttp" in dlls) and verbose: + print(f"[*] Built {dlls.get('version', dlls.get('winhttp'))}") except Exception as e: if verbose: print(f"[!] Build failed: {e}") # Fallback: try to fetch prebuilt via install.sh (requires release) - if "winhttp" not in dlls and verbose: + if "version" not in dlls and "winhttp" not in dlls and verbose: print("[*] No local build — trying install.sh fetch from git.mithraic.cloud release...") try: script = pathlib.Path(__file__).parent.parent / "scripts/install.sh" @@ -96,21 +98,29 @@ def ensure_mod_deployed(game_dir: pathlib.Path = None, verbose=True): print(f"[+] Copied {json_src} -> {json_dst}") deployed = False - if "winhttp" in dlls: - target = game_dir / "winhttp.dll" + # Deploy version.dll primary + if "version" in dlls: + target = game_dir / "version.dll" if not target.exists(): - shutil.copy(dlls["winhttp"], target) + shutil.copy(dlls["version"], target) if verbose: - print(f"[+] Deployed {dlls['winhttp']} -> {target}") + print(f"[+] Deployed {dlls['version']} -> {target}") deployed = True else: if verbose: - print(f"[*] winhttp.dll already at {target}") + print(f"[*] version.dll already at {target}") deployed = True - else: + # Also deploy winhttp for compat if built + if "winhttp" in dlls: + target2 = game_dir / "winhttp.dll" + if not target2.exists(): + shutil.copy(dlls["winhttp"], target2) + if verbose: + print(f"[+] Deployed compat {dlls['winhttp']} -> {target2}") + if not deployed: if verbose: - print(f"[!] winhttp.dll not built — run on Windows or build with mingw-w64. Standalone JSON-only will NOT unlock until DLL is present.") - print(f" Workaround: use --steam-stats method or run install.sh which fetches prebuilt release") + print(f"[!] version.dll not built — run on Windows or build with mingw-w64. Standalone JSON-only will NOT unlock until DLL is present.") + print(f" Run: bash scripts/install.sh which fetches prebuilt release") return False if "unlocker" in dlls: @@ -141,13 +151,13 @@ def launch_with_mod(game_dir: pathlib.Path = None): # Proton launch hint print("\n=== LAUNCH ===") if os.name == "nt": - print(f"Launching {game_dir / 'DEVOUR.exe'}") + print(f"Launching {game_dir / 'DEVOUR.exe'} (version.dll auto-loaded)") subprocess.Popen([str(game_dir / "DEVOUR.exe")], cwd=str(game_dir)) else: - # Linux: set WINEDLLOVERRIDES and ask Steam to launch, or direct proton run + # Linux: version.dll is sole unlocker, installer auto-sets WINEDLLOVERRIDES silently print(f"Game dir: {game_dir}") - print(f"Ensure Steam launch options: WINEDLLOVERRIDES=\"winhttp=n,b\" %command%") - print(f"Then launch Devour from Steam. Or run: steam steam://rungameid/1274570") + print(f"version.dll is primary unlocker — installed beside DEVOUR.exe, auto-loaded. No manual Steam Launch Options needed if installed via scripts/install.sh") + print(f"Fallback via launch.sh: bash scripts/launch.sh") # Try to launch via steam if available try: subprocess.Popen(["steam", "steam://rungameid/1274570"])