Added more diagnostics to the native world fixer and py logs system.
This commit is contained in:
@@ -4,4 +4,4 @@ from .paths import AppPaths
|
||||
|
||||
__all__ = ["AppPaths", "__version__"]
|
||||
|
||||
__version__ = "0.5.9"
|
||||
__version__ = "0.5.10"
|
||||
|
||||
@@ -49,6 +49,10 @@ 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:
|
||||
@@ -650,6 +654,12 @@ 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,
|
||||
@@ -658,6 +668,7 @@ def _play_locked(
|
||||
runner,
|
||||
use_gamebox=not no_box,
|
||||
use_world_fix=not publisher_dll,
|
||||
world_fix_status_target=world_fix_status_target,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
current_game_log = _current_game_log(
|
||||
@@ -667,12 +678,26 @@ 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,
|
||||
)
|
||||
_emit(
|
||||
{
|
||||
"ok": outcome.returncode == 0,
|
||||
@@ -682,6 +707,7 @@ def _play_locked(
|
||||
"setup_commands": [asdict(result) for result in setup_results],
|
||||
"game": asdict(outcome),
|
||||
"current_game_log": current_game_log,
|
||||
"world_fix": world_fix_status,
|
||||
},
|
||||
as_json,
|
||||
)
|
||||
@@ -712,7 +738,17 @@ 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,
|
||||
"stage_without_new_stage_events": [
|
||||
event.to_dict()
|
||||
for event in analysis.events
|
||||
if event.kind == "stage_without_new_stage"
|
||||
],
|
||||
"scene_imports": {
|
||||
name: [scene.to_dict() for scene in imports]
|
||||
for name, imports in sorted(analysis.scenes.items())
|
||||
},
|
||||
"failed_file_count": len(analysis.failed_files),
|
||||
"failed_files": [event.to_dict() for event in analysis.failed_files[:64]],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -16,7 +16,11 @@ from .command import CommandOutcome, CommandRunner, OutputLineFilter
|
||||
from .gamebox import build_gamebox_command
|
||||
from .paths import AppPaths
|
||||
from .settings import Settings
|
||||
from .world_fix import WorldFixError, resolve_world_fix_helper
|
||||
from .world_fix import (
|
||||
WorldFixError,
|
||||
WorldFixStatusTarget,
|
||||
resolve_world_fix_helper,
|
||||
)
|
||||
|
||||
|
||||
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
|
||||
@@ -252,6 +256,7 @@ def run_game(
|
||||
*,
|
||||
use_gamebox: bool = True,
|
||||
use_world_fix: bool = False,
|
||||
world_fix_status_target: WorldFixStatusTarget | None = None,
|
||||
timeout_seconds: float = 12 * 60 * 60,
|
||||
check: bool = True,
|
||||
extra_environment: Mapping[str, str] | None = None,
|
||||
@@ -273,6 +278,10 @@ def run_game(
|
||||
]
|
||||
if world_fix_helper is None:
|
||||
game_command.append("PTEU_REBORN")
|
||||
elif world_fix_status_target is not None:
|
||||
game_command.extend(
|
||||
["--status-file", world_fix_status_target.windows_path]
|
||||
)
|
||||
environment = _umu_environment(paths)
|
||||
elif selection.kind == "proton7-container":
|
||||
if not selection.proton_path:
|
||||
@@ -283,6 +292,10 @@ def run_game(
|
||||
]
|
||||
if world_fix_helper is None:
|
||||
game_command.append("PTEU_REBORN")
|
||||
elif world_fix_status_target is not None:
|
||||
game_command.extend(
|
||||
["--status-file", world_fix_status_target.windows_path]
|
||||
)
|
||||
environment = _umu_proton7_environment(
|
||||
paths,
|
||||
selection,
|
||||
@@ -301,6 +314,10 @@ def run_game(
|
||||
]
|
||||
if world_fix_helper is None:
|
||||
game_command.append("PTEU_REBORN")
|
||||
elif world_fix_status_target is not None:
|
||||
game_command.extend(
|
||||
["--status-file", world_fix_status_target.windows_path]
|
||||
)
|
||||
environment = _proton_environment(
|
||||
paths,
|
||||
selection,
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
|
||||
V21_BINARY_HASHES = {
|
||||
@@ -13,8 +16,16 @@ V21_BINARY_HASHES = {
|
||||
WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
|
||||
# Filled from the reproducible stripped PE after the native helper is built.
|
||||
WORLD_FIX_HELPER_SHA256 = (
|
||||
"d5bdf655f62dae429eff627cf8960f670ec59c12a9759dcd14342786f53e4019"
|
||||
"a7a17d29ef4c11f9603790f0110a4d6823b91cce03f70e466b7e0bdbd6fa313e"
|
||||
)
|
||||
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):
|
||||
@@ -58,6 +69,145 @@ def resolve_world_fix_helper(
|
||||
return candidate
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
|
||||
Reference in New Issue
Block a user