Improvements to try resolve the issues with login checksums.

This commit is contained in:
2026-08-28 22:54:46 +02:00
parent 06672589ad
commit 1e1f76a000
10 changed files with 740 additions and 33 deletions

View File

@@ -39,6 +39,10 @@ CURRENT_V21_BINARY_HASHES = V21_BINARY_HASHES
PATCHED_V21_GAME_DLL_SHA256 = (
"0be11b06ccf9be747e32b195c8d5980062f446d7d3a0a662ebfe0e602a01c538"
)
RETIRED_POST_AUTH_GAME_DLL_SHA256 = (
"93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd"
)
RUNTIME_VIEW_CONFIGURATION_VERSION = 1
class OverlayError(RuntimeError):
@@ -147,10 +151,28 @@ def prepare_runtime_view(
# ``use_world_fix`` is retained as a compatibility keyword. The retired
# helper is never selected and no code path may replace the publisher DLL.
_ = use_world_fix
binary_action, game_exe_source, game_dll_source = _runtime_game_binary_sources(
(
binary_action,
game_exe_source,
game_dll_source,
source_binary_hashes,
) = _runtime_game_binary_profile(
paths,
source,
)
reused = _reuse_runtime_view(
paths,
source,
inspection,
action,
game_save,
binary_action,
game_exe_source,
game_dll_source,
source_binary_hashes,
)
if reused is not None:
return reused
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
backup = paths.data / f".{paths.runtime_view.name}.old-{uuid.uuid4().hex}"
@@ -223,7 +245,13 @@ def prepare_runtime_view(
created_at=datetime.now().astimezone().isoformat(),
links_created=links_created,
)
_write_json(paths.runtime_manifest, asdict(result))
_write_json(
paths.runtime_manifest,
{
"configuration_version": RUNTIME_VIEW_CONFIGURATION_VERSION,
**asdict(result),
},
)
return result
@@ -271,6 +299,32 @@ def select_binary_action(
return "verified-source-pair"
if source_hashes == CURRENT_V21_BINARY_HASHES:
return "verified-v21-publisher-pair"
game_exe_sha256 = source_hashes.get("Game.exe")
game_dll_sha256 = source_hashes.get("game.dll")
if game_dll_sha256 in {
PATCHED_V21_GAME_DLL_SHA256,
RETIRED_POST_AUTH_GAME_DLL_SHA256,
}:
raise OverlayError(
"a retired experimental game.dll is installed; run update --repair "
"before launching"
)
known_game_exe_hashes = {
CURRENT_V21_BINARY_HASHES["Game.exe"],
COMPATIBILITY_BINARY_HASHES["Game.exe"],
}
known_game_dll_hashes = {
CURRENT_V21_BINARY_HASHES["game.dll"],
COMPATIBILITY_BINARY_HASHES["game.dll"],
}
if (
game_exe_sha256 in known_game_exe_hashes
or game_dll_sha256 in known_game_dll_hashes
):
raise OverlayError(
"Game.exe and game.dll are from different verified releases; run "
"update --repair before launching"
)
return "upstream-pair"
@@ -392,13 +446,14 @@ def _prepare_persistent_save(paths: AppPaths, source: Path) -> Path:
candidate = path.resolve(strict=True)
break
if candidate is not None:
selected = candidate
allowed_roots = (paths.data.resolve(strict=False), source.resolve(strict=True))
if not any(
candidate == root or candidate.is_relative_to(root)
selected == root or selected.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)
raise OverlayError(f"refusing to migrate game save outside managed roots: {selected}")
_reject_save_symlinks(selected)
target.parent.mkdir(parents=True, exist_ok=True)
staging = target.parent / f".{target.name}.build-{uuid.uuid4().hex}"
@@ -423,10 +478,213 @@ def _reject_save_symlinks(root: Path) -> None:
raise OverlayError(f"persistent game save contains a symbolic link: {path}")
def _reuse_runtime_view(
paths: AppPaths,
source: Path,
inspection: SceneInspection,
scene_action: str,
game_save: Path,
binary_action: str,
game_exe_source: Path,
game_dll_source: Path,
source_binary_hashes: Mapping[str, str],
) -> OverlayResult | None:
result = _read_overlay_result(paths.runtime_manifest)
if result is None:
return None
if result.source_game != str(source):
return None
if result.runtime_view != str(paths.runtime_view):
return None
if result.source_scene_sha256 != inspection.file_sha256:
return None
if result.scene_action != scene_action or result.binary_action != binary_action:
return None
if result.game_exe_sha256 != source_binary_hashes.get("Game.exe"):
return None
if result.game_dll_sha256 != source_binary_hashes.get("game.dll"):
return None
if not _runtime_layout_ready(
paths,
source,
game_save,
game_exe_source,
game_dll_source,
result.links_created,
):
return None
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
try:
if _sha256(runtime_scene) != result.runtime_scene_sha256:
return None
except OSError:
return None
return result
def _runtime_layout_ready(
paths: AppPaths,
source: Path,
game_save: Path,
game_exe_source: Path,
game_dll_source: Path,
expected_links: int,
) -> bool:
runtime = paths.runtime_view
if runtime.is_symlink() or not runtime.is_dir():
return False
top_level_exceptions = {"game", "save"}
top_level_special = {"game", "save"}
for name, binary_source in (
("Game.exe", game_exe_source),
("game.dll", game_dll_source),
):
if binary_source != source / name:
top_level_exceptions.add(name)
top_level_special.add(name)
top_level_ready, top_level_links = _linked_level_ready(
source,
runtime,
except_names=top_level_exceptions,
special_names=top_level_special,
)
if not top_level_ready:
return False
if not _symlink_points_to(runtime / "save", game_save):
return False
if game_exe_source != source / "Game.exe" and not _symlink_points_to(
runtime / "Game.exe",
game_exe_source,
):
return False
if game_dll_source != source / "game.dll" and not _symlink_points_to(
runtime / "game.dll",
game_dll_source,
):
return False
game_source = source / "game"
game_target = runtime / "game"
if game_target.is_symlink() or not game_target.is_dir():
return False
game_ready, game_links = _linked_level_ready(
game_source,
game_target,
except_names={"maps"},
special_names={"maps"},
)
if not game_ready:
return False
maps_source = game_source / "maps"
maps_target = game_target / "maps"
if maps_target.is_symlink() or not maps_target.is_dir():
return False
maps_ready, maps_links = _linked_level_ready(
maps_source,
maps_target,
except_names={"WhisperingVale.rovug"},
special_names={"WhisperingVale.rovug"},
)
if not maps_ready:
return False
runtime_scene = maps_target / "WhisperingVale.rovug"
if runtime_scene.is_symlink() or not runtime_scene.is_file():
return False
actual_links = top_level_links + game_links + maps_links + 1
if game_exe_source != source / "Game.exe":
actual_links += 1
if game_dll_source != source / "game.dll":
actual_links += 1
return actual_links == expected_links
def _linked_level_ready(
source: Path,
target: Path,
*,
except_names: set[str],
special_names: set[str],
) -> tuple[bool, int]:
try:
source_children = {
child.name: child
for child in source.iterdir()
if child.name not in except_names
}
target_names = {child.name for child in target.iterdir()}
except OSError:
return False, 0
if target_names != set(source_children) | special_names:
return False, 0
for name, child in source_children.items():
if not _symlink_points_to(target / name, child):
return False, 0
return True, len(source_children)
def _symlink_points_to(link: Path, expected_target: Path) -> bool:
if not link.is_symlink():
return False
try:
return link.resolve(strict=True) == expected_target.resolve(strict=True)
except (OSError, RuntimeError):
return False
def _read_overlay_result(path: Path) -> OverlayResult | None:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
return None
if not isinstance(value, dict):
return None
if value.get("configuration_version") != RUNTIME_VIEW_CONFIGURATION_VERSION:
return None
string_fields = (
"source_game",
"runtime_view",
"source_scene_sha256",
"runtime_scene_sha256",
"scene_action",
"binary_action",
"game_exe_sha256",
"game_dll_sha256",
"created_at",
)
if not all(isinstance(value.get(name), str) for name in string_fields):
return None
links_created = value.get("links_created")
if isinstance(links_created, bool) or not isinstance(links_created, int):
return None
if links_created < 0:
return None
return OverlayResult(
source_game=value["source_game"],
runtime_view=value["runtime_view"],
source_scene_sha256=value["source_scene_sha256"],
runtime_scene_sha256=value["runtime_scene_sha256"],
scene_action=value["scene_action"],
binary_action=value["binary_action"],
game_exe_sha256=value["game_exe_sha256"],
game_dll_sha256=value["game_dll_sha256"],
created_at=value["created_at"],
links_created=links_created,
)
def _runtime_game_binary_sources(
paths: AppPaths,
source: Path,
) -> tuple[str, Path, Path]:
action, game_exe, game_dll, _ = _runtime_game_binary_profile(paths, source)
return action, game_exe, game_dll
def _runtime_game_binary_profile(
paths: AppPaths,
source: Path,
) -> tuple[str, Path, Path, dict[str, str]]:
cache = paths.compatibility_binaries
cache_hashes = _binary_hashes(cache) if cache.is_dir() else None
source_hashes = _binary_hashes(source)
@@ -435,6 +693,7 @@ def _runtime_game_binary_sources(
action,
source / "Game.exe",
source / "game.dll",
source_hashes,
)