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__"]
__version__ = "0.5.10"
__version__ = "0.5.11"

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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,

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -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,

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 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()