From c49c56bc975cfc9426cddbf7dcf43e8730a3026a Mon Sep 17 00:00:00 2001 From: m4ko Date: Fri, 28 Aug 2026 21:56:13 +0200 Subject: [PATCH] Reverted experimental fix as game checks dll byte changes. --- ptlinux/__init__.py | 2 +- ptlinux/cli.py | 56 +++++++-------- ptlinux/gamebox.py | 2 + ptlinux/gamebox_runtime.py | 127 ++++++++++++++++++++++++++++++++++- ptlinux/login_diagnostics.py | 29 ++++---- ptlinux/overlay.py | 6 +- ptlinux/self_update.py | 1 - ptlinux/settings.py | 6 ++ ptlinux/ui.py | 12 +++- ptlinux/world_fix.py | 47 +++---------- 10 files changed, 198 insertions(+), 90 deletions(-) diff --git a/ptlinux/__init__.py b/ptlinux/__init__.py index 3ea0fa0..0f49a83 100644 --- a/ptlinux/__init__.py +++ b/ptlinux/__init__.py @@ -4,4 +4,4 @@ from .paths import AppPaths __all__ = ["AppPaths", "__version__"] -__version__ = "0.5.10" +__version__ = "0.5.11" diff --git a/ptlinux/cli.py b/ptlinux/cli.py index 541c295..2e93e4a 100644 --- a/ptlinux/cli.py +++ b/ptlinux/cli.py @@ -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, diff --git a/ptlinux/gamebox.py b/ptlinux/gamebox.py index d8da0bb..af8157e 100644 --- a/ptlinux/gamebox.py +++ b/ptlinux/gamebox.py @@ -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 diff --git a/ptlinux/gamebox_runtime.py b/ptlinux/gamebox_runtime.py index eaff450..bd38ac6 100644 --- a/ptlinux/gamebox_runtime.py +++ b/ptlinux/gamebox_runtime.py @@ -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) diff --git a/ptlinux/login_diagnostics.py b/ptlinux/login_diagnostics.py index 264c876..a2e423b 100644 --- a/ptlinux/login_diagnostics.py +++ b/ptlinux/login_diagnostics.py @@ -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: diff --git a/ptlinux/overlay.py b/ptlinux/overlay.py index 18ea0ef..807759a 100644 --- a/ptlinux/overlay.py +++ b/ptlinux/overlay.py @@ -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, diff --git a/ptlinux/self_update.py b/ptlinux/self_update.py index 51fc29b..8c3d345 100644 --- a/ptlinux/self_update.py +++ b/ptlinux/self_update.py @@ -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"), diff --git a/ptlinux/settings.py b/ptlinux/settings.py index 20f1c45..22a47b9 100644 --- a/ptlinux/settings.py +++ b/ptlinux/settings.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"), diff --git a/ptlinux/ui.py b/ptlinux/ui.py index 4c0ae3c..ba2474b 100644 --- a/ptlinux/ui.py +++ b/ptlinux/ui.py @@ -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, diff --git a/ptlinux/world_fix.py b/ptlinux/world_fix.py index 0869e09..19f6203 100644 --- a/ptlinux/world_fix.py +++ b/ptlinux/world_fix.py @@ -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()