Reverted experimental fix as game checks dll byte changes.

This commit is contained in:
2026-08-28 21:56:13 +02:00
parent e90cb788ba
commit c49c56bc97
10 changed files with 198 additions and 90 deletions

View File

@@ -4,4 +4,4 @@ from .paths import AppPaths
__all__ = ["AppPaths", "__version__"] __all__ = ["AppPaths", "__version__"]
__version__ = "0.5.10" __version__ = "0.5.11"

View File

@@ -49,10 +49,6 @@ from .self_update import (
rollback_launcher_update, rollback_launcher_update,
update_check_to_dict, update_check_to_dict,
) )
from .world_fix import (
create_world_fix_status_target,
read_world_fix_status,
)
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
@@ -147,8 +143,8 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true", action="store_true",
dest="publisher_dll", dest="publisher_dll",
help=( help=(
"diagnostic fallback: disable the in-memory post-login world fix " "compatibility option retained for older scripts; publisher code is "
"(the publisher DLL is always used)" "always used without the retired world fix"
), ),
) )
play.add_argument("--dry-run", action="store_true") 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}") print(f"game.dll: {report.game_dll}")
if report.game_dll_sha256: if report.game_dll_sha256:
print(f"DLL SHA-256: {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" world_fix_status = "enabled" if report.world_fix_enabled else "retired"
print(f"World fix: {world_fix_status} ({report.world_fix_helper})") print(f"World fix: {world_fix_status}")
if report.world_fix_helper_sha256: if report.world_fix_disabled_reason:
print(f"Helper SHA: {report.world_fix_helper_sha256}") print(f"Reason: {report.world_fix_disabled_reason}")
if report.runtime_kind: if report.runtime_kind:
print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})") print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})")
else: else:
@@ -634,6 +630,9 @@ def _play_locked(
skip_runtime_setup: bool, skip_runtime_setup: bool,
as_json: bool, as_json: bool,
) -> int: ) -> 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) settings = _load_or_default_settings(paths, persist=not dry_run)
source = _source_game(paths, settings, game_dir) source = _source_game(paths, settings, game_dir)
overlay = None overlay = None
@@ -654,12 +653,6 @@ def _play_locked(
patterns=("log_*.txt",), patterns=("log_*.txt",),
) )
game_started_ns = time.time_ns() 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: try:
outcome = run_game( outcome = run_game(
paths, paths,
@@ -667,8 +660,7 @@ def _play_locked(
selection, selection,
runner, runner,
use_gamebox=not no_box, use_gamebox=not no_box,
use_world_fix=not publisher_dll, use_world_fix=False,
world_fix_status_target=world_fix_status_target,
) )
except KeyboardInterrupt: except KeyboardInterrupt:
current_game_log = _current_game_log( current_game_log = _current_game_log(
@@ -678,26 +670,13 @@ def _play_locked(
) )
if current_game_log is not None and not as_json: if current_game_log is not None and not as_json:
_print_interrupted_game_log(current_game_log) _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 raise
current_game_log = _current_game_log( current_game_log = _current_game_log(
native_log_directory, native_log_directory,
native_log_baseline, native_log_baseline,
game_started_ns, game_started_ns,
) )
world_fix_status = read_world_fix_status( world_fix_status = None
world_fix_status_target,
game_started_ns,
)
_emit( _emit(
{ {
"ok": outcome.returncode == 0, "ok": outcome.returncode == 0,
@@ -738,6 +717,11 @@ def _current_game_log(
"import_scene_threw": analysis.import_scene_threw, "import_scene_threw": analysis.import_scene_threw,
"failed_import_scenes": analysis.failed_import_scenes, "failed_import_scenes": analysis.failed_import_scenes,
"stage_without_new_stage": analysis.stage_without_new_stage, "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": [ "stage_without_new_stage_events": [
event.to_dict() event.to_dict()
for event in analysis.events for event in analysis.events
@@ -754,7 +738,13 @@ def _current_game_log(
def _print_interrupted_game_log(report: dict[str, object]) -> None: def _print_interrupted_game_log(report: dict[str, object]) -> None:
print(f"\nCurrent game log: {report['path']}", file=sys.stderr) 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( print(
"Detected a texture/material failure while loading the game world.", "Detected a texture/material failure while loading the game world.",
file=sys.stderr, file=sys.stderr,

View File

@@ -23,5 +23,7 @@ def build_gamebox_command(
] ]
if settings.borderless: if settings.borderless:
command.append("--borderless") command.append("--borderless")
if settings.suppress_unresponsive_warnings:
command.append("--suppress-wm-ping")
command.extend(("--", *[str(value) for value in game_command])) command.extend(("--", *[str(value) for value in game_command]))
return command return command

View File

@@ -44,6 +44,14 @@ def _select_game_window(
return None 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): class GameboxRuntimeError(RuntimeError):
"""Raised when the game command or X11 mapper cannot be started.""" """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.XMapRaised(self.display, window)
self.library.XFlush(self.display) 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: def _remove_decorations(self, window: int) -> None:
atom = self.library.XInternAtom(self.display, b"_MOTIF_WM_HINTS", 0) atom = self.library.XInternAtom(self.display, b"_MOTIF_WM_HINTS", 0)
if atom == 0: if atom == 0:
@@ -205,8 +265,24 @@ class _X11:
ctypes.c_int, ctypes.c_int,
] ]
self.library.XChangeProperty.restype = 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.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
self.library.XMapRaised.restype = ctypes.c_int 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.argtypes = [ctypes.c_void_p]
self.library.XFlush.restype = ctypes.c_int self.library.XFlush.restype = ctypes.c_int
self.library.XFree.argtypes = [ctypes.c_void_p] self.library.XFree.argtypes = [ctypes.c_void_p]
@@ -215,11 +291,46 @@ class _X11:
self.library.XCloseDisplay.restype = ctypes.c_int 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( def run_game_window(
command: Sequence[str], command: Sequence[str],
window_title: str = WINDOW_TITLE, window_title: str = WINDOW_TITLE,
*, *,
borderless: bool = False, borderless: bool = False,
suppress_wm_ping: bool = False,
) -> int: ) -> int:
if not command: if not command:
raise GameboxRuntimeError("game command cannot be empty") raise GameboxRuntimeError("game command cannot be empty")
@@ -270,7 +381,12 @@ def run_game_window(
and game_window_created_at is not None and game_window_created_at is not None
and now - game_window_created_at >= WINDOW_READY_DELAY_SECONDS 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( print(
f"Game window ready: {game_title or window_title}", f"Game window ready: {game_title or window_title}",
file=sys.stderr, file=sys.stderr,
@@ -285,7 +401,12 @@ def run_game_window(
flush=True, flush=True,
) )
if game_window is not None: 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()) return int(process.wait())
finally: finally:
mapper.close() mapper.close()
@@ -295,6 +416,7 @@ def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--window-title", default=WINDOW_TITLE) parser.add_argument("--window-title", default=WINDOW_TITLE)
parser.add_argument("--borderless", action="store_true") parser.add_argument("--borderless", action="store_true")
parser.add_argument("--suppress-wm-ping", action="store_true")
parser.add_argument("command", nargs=argparse.REMAINDER) parser.add_argument("command", nargs=argparse.REMAINDER)
arguments = parser.parse_args(argv) arguments = parser.parse_args(argv)
command = arguments.command command = arguments.command
@@ -305,6 +427,7 @@ def main(argv: Sequence[str] | None = None) -> int:
command, command,
arguments.window_title, arguments.window_title,
borderless=arguments.borderless, borderless=arguments.borderless,
suppress_wm_ping=arguments.suppress_wm_ping,
) )
except (GameboxRuntimeError, OSError) as exc: except (GameboxRuntimeError, OSError) as exc:
print(f"Error: {exc}", file=sys.stderr) print(f"Error: {exc}", file=sys.stderr)

View File

@@ -27,7 +27,12 @@ from .runtime import (
select_runtime, select_runtime,
) )
from .settings import Settings 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" LOGIN_SERVER_HOST = "5.135.35.188"
@@ -81,6 +86,8 @@ class LoginDiagnosticReport:
world_fix_helper: str world_fix_helper: str
world_fix_helper_sha256: str | None world_fix_helper_sha256: str | None
world_fix_helper_ready: bool world_fix_helper_ready: bool
world_fix_enabled: bool
world_fix_disabled_reason: str | None
runtime_kind: str | None runtime_kind: str | None
runtime_path: str | None runtime_path: str | None
runtime_error: 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) game_profile = classify_game_profile(game_exe_sha256, game_dll_sha256)
world_fix_helper = packaged_world_fix_helper() world_fix_helper = packaged_world_fix_helper()
world_fix_helper_sha256 = _sha256_if_file(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 = _runtime_prefix(paths, selection)
prefix_ready = _prefix_ready(prefix) prefix_ready = _prefix_ready(prefix)
directx_ready = native_d3d_ready(prefix) if prefix is not None else False directx_ready = native_d3d_ready(prefix) if prefix is not None else False
@@ -176,8 +186,6 @@ def inspect_login_environment(
prefix_ready, prefix_ready,
directx_ready, directx_ready,
latest_log, latest_log,
world_fix_required=game_profile == "v21.75.83",
world_fix_helper_ready=world_fix_helper_ready,
) )
return LoginDiagnosticReport( return LoginDiagnosticReport(
preflight_ok=( preflight_ok=(
@@ -186,7 +194,6 @@ def inspect_login_environment(
and selection is not None and selection is not None
and prefix_ready and prefix_ready
and directx_ready and directx_ready
and (game_profile != "v21.75.83" or world_fix_helper_ready)
), ),
checked_at=datetime.now().astimezone().isoformat(), checked_at=datetime.now().astimezone().isoformat(),
launcher_version=__version__, launcher_version=__version__,
@@ -205,6 +212,10 @@ def inspect_login_environment(
world_fix_helper=str(world_fix_helper), world_fix_helper=str(world_fix_helper),
world_fix_helper_sha256=world_fix_helper_sha256, world_fix_helper_sha256=world_fix_helper_sha256,
world_fix_helper_ready=world_fix_helper_ready, 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_kind=selection.kind if selection is not None else None,
runtime_path=_runtime_path(selection), runtime_path=_runtime_path(selection),
runtime_error=runtime_error, runtime_error=runtime_error,
@@ -320,9 +331,6 @@ def _diagnostic_warnings(
prefix_ready: bool, prefix_ready: bool,
directx_ready: bool, directx_ready: bool,
latest_log: Path | None, latest_log: Path | None,
*,
world_fix_required: bool,
world_fix_helper_ready: bool,
) -> tuple[str, ...]: ) -> tuple[str, ...]:
warnings: list[str] = [] warnings: list[str] = []
if not probe.reachable: 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; " "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." "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: if runtime_error is not None:
warnings.append(runtime_error) warnings.append(runtime_error)
elif not prefix_ready: elif not prefix_ready:

View File

@@ -106,7 +106,7 @@ def inspect_runtime_compatibility(
source_scene_sha256 = inspection.file_sha256 source_scene_sha256 = inspection.file_sha256
scene_action = _scene_action(inspection) scene_action = _scene_action(inspection)
# Retain the keyword for callers from the 0.5.8 candidate. The runtime # 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 _ = use_world_fix
binary_action = _inspect_runtime_binary_action(paths, source) binary_action = _inspect_runtime_binary_action(paths, source)
except (OSError, ValueError, OverlayError) as exc: except (OSError, ValueError, OverlayError) as exc:
@@ -144,8 +144,8 @@ def prepare_runtime_view(
inspection = inspect_wrapped_assbin(source_scene) inspection = inspect_wrapped_assbin(source_scene)
action = _scene_action(inspection) action = _scene_action(inspection)
game_save = _prepare_persistent_save(paths, source) game_save = _prepare_persistent_save(paths, source)
# ``use_world_fix`` is retained as a compatibility keyword. No code path may # ``use_world_fix`` is retained as a compatibility keyword. The retired
# replace the publisher DLL; the post-login fix is selected by ``run_game``. # helper is never selected and no code path may replace the publisher DLL.
_ = use_world_fix _ = use_world_fix
binary_action, game_exe_source, game_dll_source = _runtime_game_binary_sources( binary_action, game_exe_source, game_dll_source = _runtime_game_binary_sources(
paths, paths,

View File

@@ -637,7 +637,6 @@ def _validate_release_directory(release: Path) -> str:
Path("ptlinux/cli.py"), Path("ptlinux/cli.py"),
Path("ptlinux/self_update.py"), Path("ptlinux/self_update.py"),
Path("ptlinux") / PUBLIC_KEY_FILENAME, Path("ptlinux") / PUBLIC_KEY_FILENAME,
Path("ptlinux/native/pt-world-fix.exe"),
Path("ptdiag/__init__.py"), Path("ptdiag/__init__.py"),
Path("ptdiag/assbin.py"), Path("ptdiag/assbin.py"),
Path("ptdiag/logs.py"), Path("ptdiag/logs.py"),

View File

@@ -19,6 +19,7 @@ class Settings:
refresh_rate: int = 60 refresh_rate: int = 60
fullscreen: bool = False fullscreen: bool = False
borderless: bool = True borderless: bool = True
suppress_unresponsive_warnings: bool = True
scaler: str = "fit" scaler: str = "fit"
filter: str = "linear" filter: str = "linear"
runtime: str = "auto" runtime: str = "auto"
@@ -48,6 +49,11 @@ def load_settings(paths: AppPaths) -> Settings:
refresh_rate=_integer(value, "refresh_rate", 60), refresh_rate=_integer(value, "refresh_rate", 60),
fullscreen=_boolean(value, "fullscreen", False), fullscreen=_boolean(value, "fullscreen", False),
borderless=_boolean(value, "borderless", True), borderless=_boolean(value, "borderless", True),
suppress_unresponsive_warnings=_boolean(
value,
"suppress_unresponsive_warnings",
True,
),
scaler=_string(value, "scaler", "fit"), scaler=_string(value, "scaler", "fit"),
filter=_string(value, "filter", "linear"), filter=_string(value, "filter", "linear"),
runtime=_string(value, "runtime", "auto"), runtime=_string(value, "runtime", "auto"),

View File

@@ -1462,6 +1462,9 @@ def run_dashboard(paths: AppPaths) -> None:
content.columnconfigure(1, weight=1, uniform="settings-card") content.columnconfigure(1, weight=1, uniform="settings-card")
borderless_value = tk.BooleanVar(value=current_settings.borderless) 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) runtime_value = tk.StringVar(value=current_settings.runtime)
proton_sync_value = tk.StringVar(value=current_settings.proton_sync) proton_sync_value = tk.StringVar(value=current_settings.proton_sync)
proton_path_value = tk.StringVar(value=current_settings.proton_path or "") 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, refresh_rate=current_settings.refresh_rate,
fullscreen=current_settings.fullscreen, fullscreen=current_settings.fullscreen,
borderless=borderless_value.get(), borderless=borderless_value.get(),
suppress_unresponsive_warnings=suppress_unresponsive_value.get(),
scaler=current_settings.scaler, scaler=current_settings.scaler,
filter=current_settings.filter, filter=current_settings.filter,
runtime=runtime_value.get(), runtime=runtime_value.get(),
@@ -1600,7 +1604,13 @@ def run_dashboard(paths: AppPaths) -> None:
text="Borderless", text="Borderless",
variable=borderless_value, variable=borderless_value,
style="Settings.TCheckbutton", 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( rendering_card = ttk.LabelFrame(
content, content,

View File

@@ -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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import hashlib
import json import json
import os import os
from pathlib import Path, PureWindowsPath from pathlib import Path, PureWindowsPath
@@ -18,6 +17,12 @@ WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
WORLD_FIX_HELPER_SHA256 = ( WORLD_FIX_HELPER_SHA256 = (
"a7a17d29ef4c11f9603790f0110a4d6823b91cce03f70e466b7e0bdbd6fa313e" "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_PROTOCOL = 1
WORLD_FIX_STATUS_MAX_BYTES = 64 * 1024 WORLD_FIX_STATUS_MAX_BYTES = 64 * 1024
@@ -29,7 +34,7 @@ class WorldFixStatusTarget:
class WorldFixError(RuntimeError): 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: def packaged_world_fix_helper() -> Path:
@@ -41,32 +46,10 @@ def resolve_world_fix_helper(
*, *,
helper: Path | None = None, helper: Path | None = None,
) -> Path | 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) _ = game_directory, helper
hashes = { raise WorldFixError(WORLD_FIX_DISABLED_REASON)
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
def create_world_fix_status_target( def create_world_fix_status_target(
@@ -206,11 +189,3 @@ def _validated_status_event(value: object) -> dict[str, object] | None:
continue continue
clean[key] = item clean[key] = item
return clean 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()