192 lines
5.3 KiB
Python
192 lines
5.3 KiB
Python
"""Retired V21 world-helper metadata and legacy status-file parsing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import json
|
|
import os
|
|
from pathlib import Path, PureWindowsPath
|
|
|
|
|
|
V21_BINARY_HASHES = {
|
|
"Game.exe": "2bc3edd26c5f6d7ae9bbdc23967c5aefd374bdf934fcdfa2ed42c6442e550837",
|
|
"game.dll": "4b4e5c28e2bfdaa8d36c8341fad27becfd03faf63aa8ea874f5d43c5b1961868",
|
|
}
|
|
WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
|
|
# Filled from the reproducible stripped PE after the native helper is built.
|
|
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
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorldFixStatusTarget:
|
|
path: Path
|
|
windows_path: str
|
|
|
|
|
|
class WorldFixError(RuntimeError):
|
|
"""Raised when the retired helper is requested."""
|
|
|
|
|
|
def packaged_world_fix_helper() -> Path:
|
|
return Path(__file__).resolve().parent / "native" / WORLD_FIX_HELPER_FILENAME
|
|
|
|
|
|
def resolve_world_fix_helper(
|
|
game_directory: Path,
|
|
*,
|
|
helper: Path | None = None,
|
|
) -> Path | None:
|
|
"""Reject the retired helper before it can alter publisher module memory."""
|
|
|
|
_ = game_directory, helper
|
|
raise WorldFixError(WORLD_FIX_DISABLED_REASON)
|
|
|
|
|
|
def create_world_fix_status_target(
|
|
game_save: Path,
|
|
started_ns: int,
|
|
) -> WorldFixStatusTarget:
|
|
"""Allocate a unique launcher-owned status path without pre-creating it."""
|
|
|
|
if started_ns <= 0:
|
|
raise WorldFixError("world-fix status start time must be positive")
|
|
directory = game_save / "launcher"
|
|
if directory.is_symlink():
|
|
raise WorldFixError(
|
|
"the world-fix status directory must not be a symbolic link"
|
|
)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
if not directory.is_dir():
|
|
raise WorldFixError("the world-fix status directory is unavailable")
|
|
filename = f"ptworld-{started_ns}-{os.getpid()}.jsonl"
|
|
path = directory / filename
|
|
if path.exists() or path.is_symlink():
|
|
raise WorldFixError(f"world-fix status target already exists: {path}")
|
|
windows_path = str(PureWindowsPath("save", "launcher", filename))
|
|
return WorldFixStatusTarget(path=path, windows_path=windows_path)
|
|
|
|
|
|
def read_world_fix_status(
|
|
target: WorldFixStatusTarget | None,
|
|
started_ns: int,
|
|
) -> dict[str, object] | None:
|
|
"""Read only the bounded, fresh status stream belonging to this game run."""
|
|
|
|
if target is None:
|
|
return None
|
|
report: dict[str, object] = {
|
|
"path": str(target.path),
|
|
"status": "missing",
|
|
"fresh": False,
|
|
"events": [],
|
|
}
|
|
try:
|
|
if target.path.is_symlink() or not target.path.is_file():
|
|
return report
|
|
state = target.path.stat()
|
|
except OSError as exc:
|
|
report["status"] = "unreadable"
|
|
report["error"] = str(exc)
|
|
return report
|
|
report["modified_at_ns"] = state.st_mtime_ns
|
|
report["size"] = state.st_size
|
|
if state.st_mtime_ns < started_ns:
|
|
report["status"] = "stale"
|
|
return report
|
|
report["fresh"] = True
|
|
if state.st_size > WORLD_FIX_STATUS_MAX_BYTES:
|
|
report["status"] = "oversized"
|
|
return report
|
|
try:
|
|
lines = target.path.read_text(encoding="utf-8").splitlines()
|
|
except OSError as exc:
|
|
report["status"] = "unreadable"
|
|
report["error"] = str(exc)
|
|
return report
|
|
|
|
events: list[dict[str, object]] = []
|
|
invalid_lines = 0
|
|
for line in lines[:64]:
|
|
try:
|
|
value = json.loads(line)
|
|
except (json.JSONDecodeError, TypeError):
|
|
invalid_lines += 1
|
|
continue
|
|
clean = _validated_status_event(value)
|
|
if clean is None:
|
|
invalid_lines += 1
|
|
continue
|
|
events.append(clean)
|
|
report["events"] = events
|
|
if invalid_lines:
|
|
report["invalid_lines"] = invalid_lines
|
|
event_names = {
|
|
event["event"]
|
|
for event in events
|
|
if isinstance(event.get("event"), str)
|
|
}
|
|
for name in (
|
|
"helper_started",
|
|
"game_started",
|
|
"game_dll_verified",
|
|
"hook_armed",
|
|
"login_accepted",
|
|
"world_fix_activated",
|
|
):
|
|
report[name] = name in event_names
|
|
if "world_fix_activated" in event_names:
|
|
report["status"] = "activated"
|
|
elif "world_fix_activation_failed" in event_names:
|
|
report["status"] = "activation_failed"
|
|
elif "world_fix_not_activated" in event_names:
|
|
report["status"] = "not_activated"
|
|
elif "hook_armed" in event_names:
|
|
report["status"] = "armed"
|
|
elif event_names & {
|
|
"game_path_failed",
|
|
"game_command_failed",
|
|
"game_start_failed",
|
|
"game_dll_unavailable",
|
|
"game_dll_rejected",
|
|
"hook_arm_failed",
|
|
}:
|
|
report["status"] = "helper_failed"
|
|
elif events:
|
|
report["status"] = "started"
|
|
else:
|
|
report["status"] = "invalid"
|
|
return report
|
|
|
|
|
|
def _validated_status_event(value: object) -> dict[str, object] | None:
|
|
if not isinstance(value, dict):
|
|
return None
|
|
if value.get("protocol") != WORLD_FIX_STATUS_PROTOCOL:
|
|
return None
|
|
event = value.get("event")
|
|
tick_ms = value.get("tick_ms")
|
|
if not isinstance(event, str) or not event or len(event) > 64:
|
|
return None
|
|
if isinstance(tick_ms, bool) or not isinstance(tick_ms, int) or tick_ms < 0:
|
|
return None
|
|
clean: dict[str, object] = {
|
|
"event": event,
|
|
"tick_ms": tick_ms,
|
|
}
|
|
for key in ("process_id", "base", "size", "windows_error", "exit_code"):
|
|
item = value.get(key)
|
|
if isinstance(item, bool) or not isinstance(item, int) or item < 0:
|
|
continue
|
|
clean[key] = item
|
|
return clean
|