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__"]
|
__all__ = ["AppPaths", "__version__"]
|
||||||
|
|
||||||
__version__ = "0.5.10"
|
__version__ = "0.5.11"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user