Reverted experimental fix as game checks dll byte changes.
This commit is contained in:
@@ -4,4 +4,4 @@ from .paths import AppPaths
|
||||
|
||||
__all__ = ["AppPaths", "__version__"]
|
||||
|
||||
__version__ = "0.5.10"
|
||||
__version__ = "0.5.11"
|
||||
|
||||
@@ -49,10 +49,6 @@ from .self_update import (
|
||||
rollback_launcher_update,
|
||||
update_check_to_dict,
|
||||
)
|
||||
from .world_fix import (
|
||||
create_world_fix_status_target,
|
||||
read_world_fix_status,
|
||||
)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
@@ -147,8 +143,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
action="store_true",
|
||||
dest="publisher_dll",
|
||||
help=(
|
||||
"diagnostic fallback: disable the in-memory post-login world fix "
|
||||
"(the publisher DLL is always used)"
|
||||
"compatibility option retained for older scripts; publisher code is "
|
||||
"always used without the retired world fix"
|
||||
),
|
||||
)
|
||||
play.add_argument("--dry-run", action="store_true")
|
||||
@@ -357,10 +353,10 @@ def _login_diagnostics(paths: AppPaths, timeout_seconds: float, as_json: bool) -
|
||||
print(f"game.dll: {report.game_dll}")
|
||||
if report.game_dll_sha256:
|
||||
print(f"DLL SHA-256: {report.game_dll_sha256}")
|
||||
world_fix_status = "ready" if report.world_fix_helper_ready else "not ready"
|
||||
print(f"World fix: {world_fix_status} ({report.world_fix_helper})")
|
||||
if report.world_fix_helper_sha256:
|
||||
print(f"Helper SHA: {report.world_fix_helper_sha256}")
|
||||
world_fix_status = "enabled" if report.world_fix_enabled else "retired"
|
||||
print(f"World fix: {world_fix_status}")
|
||||
if report.world_fix_disabled_reason:
|
||||
print(f"Reason: {report.world_fix_disabled_reason}")
|
||||
if report.runtime_kind:
|
||||
print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})")
|
||||
else:
|
||||
@@ -634,6 +630,9 @@ def _play_locked(
|
||||
skip_runtime_setup: bool,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
# The legacy flag is retained for command-line compatibility. V21 performs
|
||||
# periodic module-code checks, so every production launch is publisher-pristine.
|
||||
_ = publisher_dll
|
||||
settings = _load_or_default_settings(paths, persist=not dry_run)
|
||||
source = _source_game(paths, settings, game_dir)
|
||||
overlay = None
|
||||
@@ -654,12 +653,6 @@ def _play_locked(
|
||||
patterns=("log_*.txt",),
|
||||
)
|
||||
game_started_ns = time.time_ns()
|
||||
world_fix_status_target = None
|
||||
if not dry_run and not publisher_dll:
|
||||
world_fix_status_target = create_world_fix_status_target(
|
||||
paths.game_save,
|
||||
game_started_ns,
|
||||
)
|
||||
try:
|
||||
outcome = run_game(
|
||||
paths,
|
||||
@@ -667,8 +660,7 @@ def _play_locked(
|
||||
selection,
|
||||
runner,
|
||||
use_gamebox=not no_box,
|
||||
use_world_fix=not publisher_dll,
|
||||
world_fix_status_target=world_fix_status_target,
|
||||
use_world_fix=False,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
current_game_log = _current_game_log(
|
||||
@@ -678,26 +670,13 @@ def _play_locked(
|
||||
)
|
||||
if current_game_log is not None and not as_json:
|
||||
_print_interrupted_game_log(current_game_log)
|
||||
world_fix_status = read_world_fix_status(
|
||||
world_fix_status_target,
|
||||
game_started_ns,
|
||||
)
|
||||
if world_fix_status is not None and not as_json:
|
||||
print(
|
||||
f"World-fix status: {str(world_fix_status['status'])} "
|
||||
f"({str(world_fix_status['path'])})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
current_game_log = _current_game_log(
|
||||
native_log_directory,
|
||||
native_log_baseline,
|
||||
game_started_ns,
|
||||
)
|
||||
world_fix_status = read_world_fix_status(
|
||||
world_fix_status_target,
|
||||
game_started_ns,
|
||||
)
|
||||
world_fix_status = None
|
||||
_emit(
|
||||
{
|
||||
"ok": outcome.returncode == 0,
|
||||
@@ -738,6 +717,11 @@ def _current_game_log(
|
||||
"import_scene_threw": analysis.import_scene_threw,
|
||||
"failed_import_scenes": analysis.failed_import_scenes,
|
||||
"stage_without_new_stage": analysis.stage_without_new_stage,
|
||||
"module_code_sync_error": analysis.module_code_sync_error,
|
||||
"module_code_sync_left": analysis.module_code_sync_left,
|
||||
"module_code_sync_right": analysis.module_code_sync_right,
|
||||
"module_code_sync_delta": analysis.module_code_sync_delta,
|
||||
"banned_code_1": analysis.banned_code_1,
|
||||
"stage_without_new_stage_events": [
|
||||
event.to_dict()
|
||||
for event in analysis.events
|
||||
@@ -754,7 +738,13 @@ def _current_game_log(
|
||||
|
||||
def _print_interrupted_game_log(report: dict[str, object]) -> None:
|
||||
print(f"\nCurrent game log: {report['path']}", file=sys.stderr)
|
||||
if report["load_texture_to_material_failed"]:
|
||||
if report["module_code_sync_error"]:
|
||||
print(
|
||||
"Detected a publisher module-integrity mismatch; the game was launched "
|
||||
"with publisher-pristine code in this launcher version.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif report["load_texture_to_material_failed"]:
|
||||
print(
|
||||
"Detected a texture/material failure while loading the game world.",
|
||||
file=sys.stderr,
|
||||
|
||||
@@ -23,5 +23,7 @@ def build_gamebox_command(
|
||||
]
|
||||
if settings.borderless:
|
||||
command.append("--borderless")
|
||||
if settings.suppress_unresponsive_warnings:
|
||||
command.append("--suppress-wm-ping")
|
||||
command.extend(("--", *[str(value) for value in game_command]))
|
||||
return command
|
||||
|
||||
@@ -44,6 +44,14 @@ def _select_game_window(
|
||||
return None
|
||||
|
||||
|
||||
def _protocols_without(
|
||||
protocols: Sequence[int],
|
||||
blocked_protocol: int,
|
||||
) -> tuple[int, ...]:
|
||||
"""Remove only the selected atom while preserving every other protocol."""
|
||||
return tuple(protocol for protocol in protocols if protocol != blocked_protocol)
|
||||
|
||||
|
||||
class GameboxRuntimeError(RuntimeError):
|
||||
"""Raised when the game command or X11 mapper cannot be started."""
|
||||
|
||||
@@ -127,6 +135,58 @@ class _X11:
|
||||
self.library.XMapRaised(self.display, window)
|
||||
self.library.XFlush(self.display)
|
||||
|
||||
def suppress_wm_ping(self, window: int) -> bool:
|
||||
"""Stop the WM hang prompt for one window, preserving its other protocols."""
|
||||
ping_atom = int(self.library.XInternAtom(self.display, b"_NET_WM_PING", 0))
|
||||
if ping_atom == 0:
|
||||
return False
|
||||
protocols = self._wm_protocols(window)
|
||||
if protocols is None:
|
||||
return False
|
||||
filtered_protocols = _protocols_without(protocols, ping_atom)
|
||||
if filtered_protocols == protocols:
|
||||
return True
|
||||
if filtered_protocols:
|
||||
protocol_values = (ctypes.c_ulong * len(filtered_protocols))(
|
||||
*filtered_protocols
|
||||
)
|
||||
status = self.library.XSetWMProtocols(
|
||||
self.display,
|
||||
window,
|
||||
protocol_values,
|
||||
len(filtered_protocols),
|
||||
)
|
||||
else:
|
||||
status = self.library.XSetWMProtocols(
|
||||
self.display,
|
||||
window,
|
||||
ctypes.POINTER(ctypes.c_ulong)(),
|
||||
0,
|
||||
)
|
||||
if status == 0:
|
||||
return False
|
||||
self.library.XSync(self.display, 0)
|
||||
return self._wm_protocols(window) == filtered_protocols
|
||||
|
||||
def _wm_protocols(self, window: int) -> tuple[int, ...] | None:
|
||||
protocols = ctypes.POINTER(ctypes.c_ulong)()
|
||||
protocol_count = ctypes.c_int()
|
||||
status = self.library.XGetWMProtocols(
|
||||
self.display,
|
||||
window,
|
||||
ctypes.byref(protocols),
|
||||
ctypes.byref(protocol_count),
|
||||
)
|
||||
if status == 0:
|
||||
return None
|
||||
try:
|
||||
return tuple(
|
||||
int(protocols[index]) for index in range(protocol_count.value)
|
||||
)
|
||||
finally:
|
||||
if protocols:
|
||||
self.library.XFree(ctypes.cast(protocols, ctypes.c_void_p))
|
||||
|
||||
def _remove_decorations(self, window: int) -> None:
|
||||
atom = self.library.XInternAtom(self.display, b"_MOTIF_WM_HINTS", 0)
|
||||
if atom == 0:
|
||||
@@ -205,8 +265,24 @@ class _X11:
|
||||
ctypes.c_int,
|
||||
]
|
||||
self.library.XChangeProperty.restype = ctypes.c_int
|
||||
self.library.XGetWMProtocols.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_ulong,
|
||||
ctypes.POINTER(ctypes.POINTER(ctypes.c_ulong)),
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
]
|
||||
self.library.XGetWMProtocols.restype = ctypes.c_int
|
||||
self.library.XSetWMProtocols.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_ulong,
|
||||
ctypes.POINTER(ctypes.c_ulong),
|
||||
ctypes.c_int,
|
||||
]
|
||||
self.library.XSetWMProtocols.restype = ctypes.c_int
|
||||
self.library.XMapRaised.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
||||
self.library.XMapRaised.restype = ctypes.c_int
|
||||
self.library.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
self.library.XSync.restype = ctypes.c_int
|
||||
self.library.XFlush.argtypes = [ctypes.c_void_p]
|
||||
self.library.XFlush.restype = ctypes.c_int
|
||||
self.library.XFree.argtypes = [ctypes.c_void_p]
|
||||
@@ -215,11 +291,46 @@ class _X11:
|
||||
self.library.XCloseDisplay.restype = ctypes.c_int
|
||||
|
||||
|
||||
def _prepare_game_window(
|
||||
mapper: _X11,
|
||||
window: int,
|
||||
*,
|
||||
borderless: bool,
|
||||
suppress_wm_ping: bool,
|
||||
) -> None:
|
||||
if suppress_wm_ping:
|
||||
try:
|
||||
ping_suppressed = mapper.suppress_wm_ping(window)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"Warning: could not suppress game-window hang warnings ({exc}); "
|
||||
"continuing normally",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
if ping_suppressed:
|
||||
print(
|
||||
"Game-window hang warnings suppressed for this launch.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"Warning: could not verify game-window hang-warning "
|
||||
"suppression; continuing normally",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
mapper.map_window(window, borderless=borderless)
|
||||
|
||||
|
||||
def run_game_window(
|
||||
command: Sequence[str],
|
||||
window_title: str = WINDOW_TITLE,
|
||||
*,
|
||||
borderless: bool = False,
|
||||
suppress_wm_ping: bool = False,
|
||||
) -> int:
|
||||
if not command:
|
||||
raise GameboxRuntimeError("game command cannot be empty")
|
||||
@@ -270,7 +381,12 @@ def run_game_window(
|
||||
and game_window_created_at is not None
|
||||
and now - game_window_created_at >= WINDOW_READY_DELAY_SECONDS
|
||||
):
|
||||
mapper.map_window(game_window, borderless=borderless)
|
||||
_prepare_game_window(
|
||||
mapper,
|
||||
game_window,
|
||||
borderless=borderless,
|
||||
suppress_wm_ping=suppress_wm_ping,
|
||||
)
|
||||
print(
|
||||
f"Game window ready: {game_title or window_title}",
|
||||
file=sys.stderr,
|
||||
@@ -285,7 +401,12 @@ def run_game_window(
|
||||
flush=True,
|
||||
)
|
||||
if game_window is not None:
|
||||
mapper.map_window(game_window, borderless=borderless)
|
||||
_prepare_game_window(
|
||||
mapper,
|
||||
game_window,
|
||||
borderless=borderless,
|
||||
suppress_wm_ping=suppress_wm_ping,
|
||||
)
|
||||
return int(process.wait())
|
||||
finally:
|
||||
mapper.close()
|
||||
@@ -295,6 +416,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--window-title", default=WINDOW_TITLE)
|
||||
parser.add_argument("--borderless", action="store_true")
|
||||
parser.add_argument("--suppress-wm-ping", action="store_true")
|
||||
parser.add_argument("command", nargs=argparse.REMAINDER)
|
||||
arguments = parser.parse_args(argv)
|
||||
command = arguments.command
|
||||
@@ -305,6 +427,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
command,
|
||||
arguments.window_title,
|
||||
borderless=arguments.borderless,
|
||||
suppress_wm_ping=arguments.suppress_wm_ping,
|
||||
)
|
||||
except (GameboxRuntimeError, OSError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
|
||||
@@ -27,7 +27,12 @@ from .runtime import (
|
||||
select_runtime,
|
||||
)
|
||||
from .settings import Settings
|
||||
from .world_fix import WORLD_FIX_HELPER_SHA256, packaged_world_fix_helper
|
||||
from .world_fix import (
|
||||
WORLD_FIX_DISABLED_REASON,
|
||||
WORLD_FIX_ENABLED,
|
||||
WORLD_FIX_HELPER_SHA256,
|
||||
packaged_world_fix_helper,
|
||||
)
|
||||
|
||||
|
||||
LOGIN_SERVER_HOST = "5.135.35.188"
|
||||
@@ -81,6 +86,8 @@ class LoginDiagnosticReport:
|
||||
world_fix_helper: str
|
||||
world_fix_helper_sha256: str | None
|
||||
world_fix_helper_ready: bool
|
||||
world_fix_enabled: bool
|
||||
world_fix_disabled_reason: str | None
|
||||
runtime_kind: str | None
|
||||
runtime_path: str | None
|
||||
runtime_error: str | None
|
||||
@@ -164,7 +171,10 @@ def inspect_login_environment(
|
||||
game_profile = classify_game_profile(game_exe_sha256, game_dll_sha256)
|
||||
world_fix_helper = packaged_world_fix_helper()
|
||||
world_fix_helper_sha256 = _sha256_if_file(world_fix_helper)
|
||||
world_fix_helper_ready = world_fix_helper_sha256 == WORLD_FIX_HELPER_SHA256
|
||||
world_fix_helper_ready = (
|
||||
WORLD_FIX_ENABLED
|
||||
and world_fix_helper_sha256 == WORLD_FIX_HELPER_SHA256
|
||||
)
|
||||
prefix = _runtime_prefix(paths, selection)
|
||||
prefix_ready = _prefix_ready(prefix)
|
||||
directx_ready = native_d3d_ready(prefix) if prefix is not None else False
|
||||
@@ -176,8 +186,6 @@ def inspect_login_environment(
|
||||
prefix_ready,
|
||||
directx_ready,
|
||||
latest_log,
|
||||
world_fix_required=game_profile == "v21.75.83",
|
||||
world_fix_helper_ready=world_fix_helper_ready,
|
||||
)
|
||||
return LoginDiagnosticReport(
|
||||
preflight_ok=(
|
||||
@@ -186,7 +194,6 @@ def inspect_login_environment(
|
||||
and selection is not None
|
||||
and prefix_ready
|
||||
and directx_ready
|
||||
and (game_profile != "v21.75.83" or world_fix_helper_ready)
|
||||
),
|
||||
checked_at=datetime.now().astimezone().isoformat(),
|
||||
launcher_version=__version__,
|
||||
@@ -205,6 +212,10 @@ def inspect_login_environment(
|
||||
world_fix_helper=str(world_fix_helper),
|
||||
world_fix_helper_sha256=world_fix_helper_sha256,
|
||||
world_fix_helper_ready=world_fix_helper_ready,
|
||||
world_fix_enabled=WORLD_FIX_ENABLED,
|
||||
world_fix_disabled_reason=(
|
||||
None if WORLD_FIX_ENABLED else WORLD_FIX_DISABLED_REASON
|
||||
),
|
||||
runtime_kind=selection.kind if selection is not None else None,
|
||||
runtime_path=_runtime_path(selection),
|
||||
runtime_error=runtime_error,
|
||||
@@ -320,9 +331,6 @@ def _diagnostic_warnings(
|
||||
prefix_ready: bool,
|
||||
directx_ready: bool,
|
||||
latest_log: Path | None,
|
||||
*,
|
||||
world_fix_required: bool,
|
||||
world_fix_helper_ready: bool,
|
||||
) -> tuple[str, ...]:
|
||||
warnings: list[str] = []
|
||||
if not probe.reachable:
|
||||
@@ -348,11 +356,6 @@ def _diagnostic_warnings(
|
||||
"The retired 0.5.8 disk-patched game.dll is active and can stall login; "
|
||||
"run prepare or launch with the current launcher to restore the publisher DLL."
|
||||
)
|
||||
if world_fix_required and not world_fix_helper_ready:
|
||||
warnings.append(
|
||||
"The verified post-login world-fix helper is missing or failed integrity "
|
||||
"verification; reinstall the launcher."
|
||||
)
|
||||
if runtime_error is not None:
|
||||
warnings.append(runtime_error)
|
||||
elif not prefix_ready:
|
||||
|
||||
@@ -106,7 +106,7 @@ def inspect_runtime_compatibility(
|
||||
source_scene_sha256 = inspection.file_sha256
|
||||
scene_action = _scene_action(inspection)
|
||||
# Retain the keyword for callers from the 0.5.8 candidate. The runtime
|
||||
# view is now always publisher-pristine; activation happens in memory.
|
||||
# view and loaded process are now always publisher-pristine.
|
||||
_ = use_world_fix
|
||||
binary_action = _inspect_runtime_binary_action(paths, source)
|
||||
except (OSError, ValueError, OverlayError) as exc:
|
||||
@@ -144,8 +144,8 @@ def prepare_runtime_view(
|
||||
inspection = inspect_wrapped_assbin(source_scene)
|
||||
action = _scene_action(inspection)
|
||||
game_save = _prepare_persistent_save(paths, source)
|
||||
# ``use_world_fix`` is retained as a compatibility keyword. No code path may
|
||||
# replace the publisher DLL; the post-login fix is selected by ``run_game``.
|
||||
# ``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(
|
||||
paths,
|
||||
|
||||
@@ -637,7 +637,6 @@ def _validate_release_directory(release: Path) -> str:
|
||||
Path("ptlinux/cli.py"),
|
||||
Path("ptlinux/self_update.py"),
|
||||
Path("ptlinux") / PUBLIC_KEY_FILENAME,
|
||||
Path("ptlinux/native/pt-world-fix.exe"),
|
||||
Path("ptdiag/__init__.py"),
|
||||
Path("ptdiag/assbin.py"),
|
||||
Path("ptdiag/logs.py"),
|
||||
|
||||
@@ -19,6 +19,7 @@ class Settings:
|
||||
refresh_rate: int = 60
|
||||
fullscreen: bool = False
|
||||
borderless: bool = True
|
||||
suppress_unresponsive_warnings: bool = True
|
||||
scaler: str = "fit"
|
||||
filter: str = "linear"
|
||||
runtime: str = "auto"
|
||||
@@ -48,6 +49,11 @@ def load_settings(paths: AppPaths) -> Settings:
|
||||
refresh_rate=_integer(value, "refresh_rate", 60),
|
||||
fullscreen=_boolean(value, "fullscreen", False),
|
||||
borderless=_boolean(value, "borderless", True),
|
||||
suppress_unresponsive_warnings=_boolean(
|
||||
value,
|
||||
"suppress_unresponsive_warnings",
|
||||
True,
|
||||
),
|
||||
scaler=_string(value, "scaler", "fit"),
|
||||
filter=_string(value, "filter", "linear"),
|
||||
runtime=_string(value, "runtime", "auto"),
|
||||
|
||||
@@ -1462,6 +1462,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
content.columnconfigure(1, weight=1, uniform="settings-card")
|
||||
|
||||
borderless_value = tk.BooleanVar(value=current_settings.borderless)
|
||||
suppress_unresponsive_value = tk.BooleanVar(
|
||||
value=current_settings.suppress_unresponsive_warnings
|
||||
)
|
||||
runtime_value = tk.StringVar(value=current_settings.runtime)
|
||||
proton_sync_value = tk.StringVar(value=current_settings.proton_sync)
|
||||
proton_path_value = tk.StringVar(value=current_settings.proton_path or "")
|
||||
@@ -1559,6 +1562,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
refresh_rate=current_settings.refresh_rate,
|
||||
fullscreen=current_settings.fullscreen,
|
||||
borderless=borderless_value.get(),
|
||||
suppress_unresponsive_warnings=suppress_unresponsive_value.get(),
|
||||
scaler=current_settings.scaler,
|
||||
filter=current_settings.filter,
|
||||
runtime=runtime_value.get(),
|
||||
@@ -1600,7 +1604,13 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
text="Borderless",
|
||||
variable=borderless_value,
|
||||
style="Settings.TCheckbutton",
|
||||
).pack(side="left")
|
||||
).pack(anchor="w")
|
||||
ttk.Checkbutton(
|
||||
presentation,
|
||||
text="Suppress false 'not responding' warnings",
|
||||
variable=suppress_unresponsive_value,
|
||||
style="Settings.TCheckbutton",
|
||||
).pack(anchor="w", pady=(7, 0))
|
||||
|
||||
rendering_card = ttk.LabelFrame(
|
||||
content,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Exact-profile selection for the post-login V21 world-transition helper."""
|
||||
"""Retired V21 world-helper metadata and legacy status-file parsing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PureWindowsPath
|
||||
@@ -18,6 +17,12 @@ WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
|
||||
WORLD_FIX_HELPER_SHA256 = (
|
||||
"a7a17d29ef4c11f9603790f0110a4d6823b91cce03f70e466b7e0bdbd6fa313e"
|
||||
)
|
||||
WORLD_FIX_ENABLED = False
|
||||
WORLD_FIX_DISABLED_REASON = (
|
||||
"the experimental world fix is retired because PristonTale V21 periodically "
|
||||
"checks the loaded game.dll code section; its five-byte map-loader detour "
|
||||
"causes Module Code Sync Error / Banned Code 1"
|
||||
)
|
||||
WORLD_FIX_STATUS_PROTOCOL = 1
|
||||
WORLD_FIX_STATUS_MAX_BYTES = 64 * 1024
|
||||
|
||||
@@ -29,7 +34,7 @@ class WorldFixStatusTarget:
|
||||
|
||||
|
||||
class WorldFixError(RuntimeError):
|
||||
"""Raised when an exact client needs a missing or untrusted helper."""
|
||||
"""Raised when the retired helper is requested."""
|
||||
|
||||
|
||||
def packaged_world_fix_helper() -> Path:
|
||||
@@ -41,32 +46,10 @@ def resolve_world_fix_helper(
|
||||
*,
|
||||
helper: Path | None = None,
|
||||
) -> Path | None:
|
||||
"""Return the verified helper only for the exact publisher V21 pair."""
|
||||
"""Reject the retired helper before it can alter publisher module memory."""
|
||||
|
||||
game = game_directory.expanduser().resolve(strict=False)
|
||||
hashes = {
|
||||
name: _sha256(game / name)
|
||||
for name in V21_BINARY_HASHES
|
||||
if (game / name).is_file()
|
||||
}
|
||||
if hashes != V21_BINARY_HASHES:
|
||||
return None
|
||||
requested = packaged_world_fix_helper() if helper is None else helper.expanduser()
|
||||
if requested.is_symlink():
|
||||
raise WorldFixError(
|
||||
"the post-login world-fix helper must not be a symbolic link"
|
||||
)
|
||||
candidate = requested.resolve()
|
||||
if not candidate.is_file():
|
||||
raise WorldFixError(
|
||||
"the verified post-login world-fix helper is missing; reinstall the launcher"
|
||||
)
|
||||
digest = _sha256(candidate)
|
||||
if digest != WORLD_FIX_HELPER_SHA256:
|
||||
raise WorldFixError(
|
||||
"the post-login world-fix helper failed its launcher integrity check"
|
||||
)
|
||||
return candidate
|
||||
_ = game_directory, helper
|
||||
raise WorldFixError(WORLD_FIX_DISABLED_REASON)
|
||||
|
||||
|
||||
def create_world_fix_status_target(
|
||||
@@ -206,11 +189,3 @@ def _validated_status_event(value: object) -> dict[str, object] | None:
|
||||
continue
|
||||
clean[key] = item
|
||||
return clean
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
Reference in New Issue
Block a user