137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
"""REFramework setup: dinput8.dll, re2_fw_config.txt and the Proton launch option."""
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import re
|
|
import urllib.request
|
|
import zipfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from . import steam
|
|
from .util import atomic_write_bytes, atomic_write_text
|
|
|
|
# The filename keeps its RE2 origin across all RE Engine games.
|
|
CONFIG_NAME = "re2_fw_config.txt"
|
|
LOOSE_KEY = "LooseFileLoader_Enabled"
|
|
RELEASES_API = "https://api.github.com/repos/praydog/REFramework-nightly/releases/latest"
|
|
ASSET_NAME = "REFramework.zip"
|
|
MAX_DOWNLOAD = 200 * 1024 * 1024
|
|
|
|
|
|
@dataclass
|
|
class Check:
|
|
label: str
|
|
ok: bool
|
|
detail: str = ""
|
|
|
|
|
|
@dataclass
|
|
class LoaderStatus:
|
|
checks: list[Check] = field(default_factory=list)
|
|
launch_options: str | None = None
|
|
suggested_launch_options: str = steam.REQUIRED_LAUNCH_OPTIONS
|
|
|
|
@property
|
|
def all_ok(self) -> bool:
|
|
return all(c.ok for c in self.checks)
|
|
|
|
|
|
# -- config file ------------------------------------------------------------------
|
|
|
|
def read_config_value(config: Path, key: str) -> str | None:
|
|
try:
|
|
text = config.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return None
|
|
for line in text.splitlines():
|
|
k, sep, v = line.partition("=")
|
|
if sep and k.strip().lower() == key.lower():
|
|
return v.strip()
|
|
return None
|
|
|
|
|
|
def set_config_value(config: Path, key: str, value: str) -> None:
|
|
"""Key-wise patch: keep every unrelated line, create the file if absent."""
|
|
try:
|
|
text = config.read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
text = ""
|
|
nl = "\r\n" if "\r\n" in text else "\n"
|
|
lines = text.splitlines()
|
|
for i, line in enumerate(lines):
|
|
k, sep, _ = line.partition("=")
|
|
if sep and k.strip().lower() == key.lower():
|
|
lines[i] = f"{k.strip()}={value}"
|
|
break
|
|
else:
|
|
lines.append(f"{key}={value}")
|
|
atomic_write_text(config, nl.join(lines) + nl)
|
|
|
|
|
|
def loose_loader_enabled(game_dir: Path) -> bool:
|
|
return (read_config_value(game_dir / CONFIG_NAME, LOOSE_KEY) or "").lower() == "true"
|
|
|
|
|
|
def enable_loose_loader(game_dir: Path) -> None:
|
|
set_config_value(game_dir / CONFIG_NAME, LOOSE_KEY, "true")
|
|
|
|
|
|
# -- status -------------------------------------------------------------------------
|
|
|
|
def status(game_dir: Path, launch_options: str | None) -> LoaderStatus:
|
|
st = LoaderStatus(launch_options=launch_options,
|
|
suggested_launch_options=steam.suggest_launch_options(launch_options))
|
|
has_dll = (game_dir / "dinput8.dll").is_file()
|
|
st.checks.append(Check("REFramework (dinput8.dll) installed", has_dll,
|
|
"" if has_dll else "dinput8.dll not found in the game folder"))
|
|
loose = loose_loader_enabled(game_dir)
|
|
st.checks.append(Check(f"{LOOSE_KEY}=true in {CONFIG_NAME}", loose,
|
|
"" if loose else "Must be set before launching the game"))
|
|
lo = steam.launch_options_ok(launch_options)
|
|
st.checks.append(Check("Steam launch option overrides dinput8", lo,
|
|
"" if lo else "Proton needs WINEDLLOVERRIDES to load dinput8.dll"))
|
|
return st
|
|
|
|
|
|
# -- install ------------------------------------------------------------------------
|
|
|
|
Fetch = Callable[[str], bytes]
|
|
|
|
|
|
def _default_fetch(url: str) -> bytes:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "mhwilds-mod-manager",
|
|
"Accept": "application/vnd.github+json"})
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
data = r.read(MAX_DOWNLOAD + 1)
|
|
if len(data) > MAX_DOWNLOAD:
|
|
raise RuntimeError("download is unexpectedly large")
|
|
return data
|
|
|
|
|
|
def install_reframework(game_dir: Path, fetch: Fetch = _default_fetch, backup_dir: Path | None = None) -> str:
|
|
"""Download the latest nightly MHWILDS.zip and extract dinput8.dll. Returns the release tag."""
|
|
release = json.loads(fetch(RELEASES_API))
|
|
asset = next((a for a in release.get("assets", []) if a.get("name") == ASSET_NAME), None)
|
|
if not asset:
|
|
raise RuntimeError(f"latest REFramework release has no {ASSET_NAME}")
|
|
blob = fetch(asset["browser_download_url"])
|
|
with zipfile.ZipFile(io.BytesIO(blob)) as zf:
|
|
member = next((n for n in zf.namelist() if n.lower() == "dinput8.dll"), None)
|
|
if member is None:
|
|
raise RuntimeError("dinput8.dll not found in the release archive")
|
|
dll = zf.read(member)
|
|
target = game_dir / "dinput8.dll"
|
|
if target.exists() and backup_dir is not None:
|
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
(backup_dir / "dinput8.dll.prev").write_bytes(target.read_bytes())
|
|
atomic_write_bytes(target, dll, 0o755)
|
|
return str(release.get("tag_name", ""))
|
|
|
|
|
|
def setup_all(game_dir: Path) -> None:
|
|
"""Everything except the Steam launch option (that is verify-only by default)."""
|
|
enable_loose_loader(game_dir)
|