Files
MHW-Mod-Manager/squashfs-root/usr/share/mhwmm/core/loader.py
T

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)