More fixes to support arch distros.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user