More fixes to support arch distros.

This commit is contained in:
2026-08-28 20:15:32 +02:00
parent d2c66b6316
commit b37b5291ae
5 changed files with 309 additions and 219 deletions

View File

@@ -19,7 +19,11 @@ from ptdiag.assbin import (
)
from .paths import AppPaths
from .v21_texture_reclaim import TextureReclaimPatchError, build_texture_reclaim_patch
from .v21_post_auth_reclaim import (
PATCHED_SHA256 as POST_AUTH_RECLAIM_GAME_DLL_SHA256,
PostAuthReclaimPatchError,
build_post_auth_reclaim_patch,
)
WHISPERING_VALE_RELATIVE = Path("game/maps/WhisperingVale.rovug")
@@ -71,17 +75,11 @@ class CompatibilityCacheResult:
created_at: str
@dataclass(frozen=True)
class RuntimePatchCacheResult:
source: str
destination: str
game_dll_sha256: str
created_at: str
def prepare_runtime_view(
paths: AppPaths,
source_game: Path,
*,
use_world_fix: bool = True,
) -> OverlayResult:
"""Build a symlink view and keep the launcher-managed source read-only."""
@@ -91,9 +89,11 @@ def prepare_runtime_view(
source_scene = source / WHISPERING_VALE_RELATIVE
inspection = inspect_wrapped_assbin(source_scene)
action = _scene_action(inspection)
game_save = _prepare_persistent_save(paths, source)
binary_action, game_exe_source, game_dll_source = _runtime_game_binary_sources(
paths,
source,
use_world_fix=use_world_fix,
)
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
@@ -101,7 +101,7 @@ def prepare_runtime_view(
links_created = 0
try:
build.mkdir(parents=True, exist_ok=False)
top_level_exceptions = {"game"}
top_level_exceptions = {"game", "save"}
for name, binary_source in (
("Game.exe", game_exe_source),
("game.dll", game_dll_source),
@@ -113,6 +113,8 @@ def prepare_runtime_view(
build,
except_names=top_level_exceptions,
)
(build / "save").symlink_to(game_save, target_is_directory=True)
links_created += 1
if game_exe_source != source / "Game.exe":
(build / "Game.exe").symlink_to(game_exe_source)
links_created += 1
@@ -202,41 +204,14 @@ def cache_compatibility_binaries(
return result
def cache_runtime_patch(
paths: AppPaths,
source_dll: Path,
) -> RuntimePatchCacheResult:
source = source_dll.expanduser().resolve(strict=True)
if _sha256(source) != PATCHED_V21_GAME_DLL_SHA256:
raise OverlayError("runtime patch does not match the verified V21 profile")
target = paths.runtime_patch_dll
if target.exists() or target.is_symlink():
if target.is_file() and _sha256(target) == PATCHED_V21_GAME_DLL_SHA256:
return _runtime_patch_result(paths, source)
raise OverlayError(f"runtime patch cache has an unknown profile: {target}")
target.parent.mkdir(parents=True, exist_ok=True)
temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
try:
shutil.copy2(source, temporary)
if _sha256(temporary) != PATCHED_V21_GAME_DLL_SHA256:
raise OverlayError("copied runtime patch failed hash verification")
temporary.replace(target)
except BaseException:
temporary.unlink(missing_ok=True)
raise
result = _runtime_patch_result(paths, source)
_write_json(paths.runtime_patch_manifest, asdict(result))
return result
def select_binary_action(
cache_hashes: dict[str, str] | None,
source_hashes: dict[str, str],
runtime_patch_hash: str | None = None,
) -> str:
if runtime_patch_hash is not None:
if runtime_patch_hash != PATCHED_V21_GAME_DLL_SHA256:
raise OverlayError("runtime patch cache has an unknown binary profile")
if runtime_patch_hash != POST_AUTH_RECLAIM_GAME_DLL_SHA256:
raise OverlayError("post-authentication runtime patch has an unknown profile")
if cache_hashes is not None:
if cache_hashes != COMPATIBILITY_BINARY_HASHES:
raise OverlayError("compatibility cache has an unknown binary profile")
@@ -244,8 +219,10 @@ def select_binary_action(
return "verified-source-pair"
if runtime_patch_hash is not None:
if source_hashes != CURRENT_V21_BINARY_HASHES:
raise OverlayError("V21 runtime patch does not match the installed client profile")
return "verified-v21-texture-reclaim"
raise OverlayError(
"post-authentication runtime patch does not match the installed client profile"
)
return "verified-v21-post-auth-world-fix"
return "upstream-pair"
@@ -349,23 +326,87 @@ def _link_level(source: Path, target: Path, *, except_names: set[str]) -> int:
return count
def _runtime_game_binary_sources(paths: AppPaths, source: Path) -> tuple[str, Path, Path]:
def _prepare_persistent_save(paths: AppPaths, source: Path) -> Path:
"""Migrate game-created settings and logs before replacing the runtime view."""
target = paths.game_save
if target.is_symlink():
raise OverlayError(f"persistent game save must not be a symbolic link: {target}")
if target.exists():
if not target.is_dir():
raise OverlayError(f"persistent game save is not a directory: {target}")
_reject_save_symlinks(target)
return target
candidate = None
for path in (paths.runtime_view / "save", source / "save"):
if path.is_dir():
candidate = path.resolve(strict=True)
break
if candidate is not None:
allowed_roots = (paths.data.resolve(strict=False), source.resolve(strict=True))
if not any(
candidate == root or candidate.is_relative_to(root)
for root in allowed_roots
):
raise OverlayError(f"refusing to migrate game save outside managed roots: {candidate}")
_reject_save_symlinks(candidate)
target.parent.mkdir(parents=True, exist_ok=True)
staging = target.parent / f".{target.name}.build-{uuid.uuid4().hex}"
try:
if candidate is None:
staging.mkdir(parents=False, exist_ok=False)
else:
shutil.copytree(candidate, staging, symlinks=True)
staging.replace(target)
except BaseException:
_remove_runtime_tree(staging)
raise
return target
def _reject_save_symlinks(root: Path) -> None:
for directory, directory_names, file_names in os.walk(root, followlinks=False):
parent = Path(directory)
for name in (*directory_names, *file_names):
path = parent / name
if path.is_symlink():
raise OverlayError(f"persistent game save contains a symbolic link: {path}")
def _runtime_game_binary_sources(
paths: AppPaths,
source: Path,
*,
use_world_fix: bool = True,
) -> tuple[str, Path, Path]:
cache = paths.compatibility_binaries
cache_hashes = _binary_hashes(cache) if cache.is_dir() else None
source_hashes = _binary_hashes(source)
patch = paths.runtime_patch_dll
if source_hashes == CURRENT_V21_BINARY_HASHES and not patch.exists():
try:
result = build_texture_reclaim_patch(source / "game.dll", patch)
except TextureReclaimPatchError as exc:
raise OverlayError(f"could not build the verified V21 runtime patch: {exc}") from exc
_write_json(patch.parent / "patch.json", {"ok": True, **asdict(result)})
patch_hash = _sha256(patch) if patch.is_file() else None
patch_hash = None
if source_hashes == CURRENT_V21_BINARY_HASHES and use_world_fix:
if patch.exists() or patch.is_symlink():
if not patch.is_file() or patch.is_symlink():
raise OverlayError(f"runtime patch cache is not a regular file: {patch}")
patch_hash = _sha256(patch)
else:
try:
result = build_post_auth_reclaim_patch(source / "game.dll", patch)
except PostAuthReclaimPatchError as exc:
raise OverlayError(
f"could not build the verified post-authentication world fix: {exc}"
) from exc
patch_hash = result.destination_sha256
payload = {"ok": True, **asdict(result)}
_write_json(patch.parent / "patch.json", payload)
_write_json(paths.runtime_patch_manifest, payload)
action = select_binary_action(cache_hashes, source_hashes, patch_hash)
return (
action,
source / "Game.exe",
patch if action == "verified-v21-texture-reclaim" else source / "game.dll",
patch if action == "verified-v21-post-auth-world-fix" else source / "game.dll",
)
@@ -387,15 +428,6 @@ def _compatibility_result(paths: AppPaths, source: Path) -> CompatibilityCacheRe
)
def _runtime_patch_result(paths: AppPaths, source: Path) -> RuntimePatchCacheResult:
return RuntimePatchCacheResult(
source=str(source),
destination=str(paths.runtime_patch_dll),
game_dll_sha256=PATCHED_V21_GAME_DLL_SHA256,
created_at=datetime.now().astimezone().isoformat(),
)
def _remove_runtime_tree(path: Path) -> None:
if path.is_symlink() or path.is_file():
path.unlink(missing_ok=True)