Added asset checks and fixes for missing data in game client.
This commit is contained in:
156
ptlinux/cli.py
156
ptlinux/cli.py
@@ -15,6 +15,7 @@ from typing import Callable, Sequence
|
||||
|
||||
from ptdiag.logs import (
|
||||
LogFileState,
|
||||
classify_asset_references,
|
||||
newest_current_native_log,
|
||||
parse_current_run_log,
|
||||
snapshot_native_logs,
|
||||
@@ -33,6 +34,8 @@ from .depot import (
|
||||
DownloadOptions,
|
||||
SyncProgress,
|
||||
read_cached_depot_launch_catalog,
|
||||
read_current_depot_file_catalog,
|
||||
preflight_managed_game,
|
||||
select_depot_start_option,
|
||||
sync_managed_game,
|
||||
)
|
||||
@@ -879,6 +882,10 @@ def _play_locked(
|
||||
)
|
||||
source = _source_game(paths, settings, game_dir)
|
||||
overlay = None
|
||||
depot_file_catalog = None
|
||||
depot_preflight = None
|
||||
if game_dir is None and not dry_run:
|
||||
depot_file_catalog = read_current_depot_file_catalog(paths)
|
||||
_print_operation_status(
|
||||
as_json,
|
||||
1,
|
||||
@@ -900,6 +907,23 @@ def _play_locked(
|
||||
"another game client is active, but the prepared runtime view is missing"
|
||||
)
|
||||
elif not dry_run:
|
||||
if game_dir is None:
|
||||
if depot_file_catalog is not None:
|
||||
depot_preflight = preflight_managed_game(
|
||||
paths,
|
||||
catalog=depot_file_catalog,
|
||||
)
|
||||
if depot_preflight is not None and not depot_preflight.ready:
|
||||
details = "; ".join(
|
||||
f"{issue.path}: {issue.reason}"
|
||||
for issue in depot_preflight.issues[:3]
|
||||
)
|
||||
raise LauncherError(
|
||||
"the managed game integrity check found "
|
||||
f"{depot_preflight.issue_count} missing or changed file(s)"
|
||||
+ (f" ({details})" if details else "")
|
||||
+ "; run `pristontale-eu update --repair` before playing"
|
||||
)
|
||||
overlay = prepare_runtime_view(paths, source)
|
||||
_print_operation_status(as_json, 2, 4, "Ensuring the compatibility runtime is available…")
|
||||
umu_bootstrap = (
|
||||
@@ -933,9 +957,19 @@ def _play_locked(
|
||||
native_log_directory,
|
||||
patterns=("log_*.txt",),
|
||||
)
|
||||
crash_directory = paths.game_save / "crash"
|
||||
crash_baseline = snapshot_native_logs(
|
||||
crash_directory,
|
||||
patterns=("*.dat", "*.dmp"),
|
||||
)
|
||||
game_started_ns = time.time_ns()
|
||||
if before_game is not None:
|
||||
before_game()
|
||||
managed_asset_paths = (
|
||||
[entry.path for entry in depot_file_catalog.files]
|
||||
if depot_file_catalog is not None
|
||||
else None
|
||||
)
|
||||
try:
|
||||
outcome = run_game(
|
||||
paths,
|
||||
@@ -952,6 +986,8 @@ def _play_locked(
|
||||
native_log_directory,
|
||||
native_log_baseline,
|
||||
game_started_ns,
|
||||
managed_asset_paths=managed_asset_paths,
|
||||
source_game=source if managed_asset_paths is not None else None,
|
||||
)
|
||||
if current_game_log is not None and not as_json:
|
||||
_print_interrupted_game_log(current_game_log)
|
||||
@@ -960,6 +996,13 @@ def _play_locked(
|
||||
native_log_directory,
|
||||
native_log_baseline,
|
||||
game_started_ns,
|
||||
managed_asset_paths=managed_asset_paths,
|
||||
source_game=source if managed_asset_paths is not None else None,
|
||||
)
|
||||
current_game_crash = _current_game_crash(
|
||||
crash_directory,
|
||||
crash_baseline,
|
||||
game_started_ns,
|
||||
)
|
||||
runtime_issues = _runtime_issue_summary(outcome.stderr)
|
||||
if (
|
||||
@@ -972,22 +1015,37 @@ def _play_locked(
|
||||
elif runtime_issues["dbghelp_symdeferred_assertion_count"]:
|
||||
runtime_issues["recommended_one_variable_comparison"] = "umu"
|
||||
report = {
|
||||
"ok": outcome.returncode == 0,
|
||||
"ok": outcome.returncode == 0 and current_game_crash is None,
|
||||
"dry_run": dry_run,
|
||||
"runtime": asdict(selection),
|
||||
"umu_bootstrap": asdict(umu_bootstrap) if umu_bootstrap else None,
|
||||
"realm": asdict(start_option),
|
||||
"runtime_view": asdict(overlay) if overlay else None,
|
||||
"depot_preflight": (
|
||||
{
|
||||
**asdict(depot_preflight),
|
||||
"ready": depot_preflight.ready,
|
||||
"fully_verified": depot_preflight.fully_verified,
|
||||
}
|
||||
if depot_preflight
|
||||
else None
|
||||
),
|
||||
"setup_commands": [asdict(result) for result in setup_results],
|
||||
"game": asdict(outcome),
|
||||
"current_game_log": current_game_log,
|
||||
"current_game_crash": current_game_crash,
|
||||
"runtime_issues": runtime_issues,
|
||||
}
|
||||
if as_json or dry_run:
|
||||
_emit(report, as_json)
|
||||
else:
|
||||
_print_game_outcome(outcome, current_game_log, runtime_issues)
|
||||
return 0 if outcome.returncode == 0 else 1
|
||||
_print_game_outcome(
|
||||
outcome,
|
||||
current_game_log,
|
||||
runtime_issues,
|
||||
current_game_crash,
|
||||
)
|
||||
return 0 if outcome.returncode == 0 and current_game_crash is None else 1
|
||||
|
||||
|
||||
def _ensure_portable_umu_for_play(
|
||||
@@ -1028,6 +1086,9 @@ def _current_game_log(
|
||||
directory: Path,
|
||||
baseline: dict[str, LogFileState],
|
||||
started_ns: int,
|
||||
*,
|
||||
managed_asset_paths: Sequence[str] | None = None,
|
||||
source_game: Path | None = None,
|
||||
) -> dict[str, object] | None:
|
||||
state = newest_current_native_log(
|
||||
directory,
|
||||
@@ -1038,6 +1099,11 @@ def _current_game_log(
|
||||
if state is None:
|
||||
return None
|
||||
analysis = parse_current_run_log(state, baseline)
|
||||
asset_references = classify_asset_references(
|
||||
analysis,
|
||||
managed_asset_paths,
|
||||
source_game=source_game,
|
||||
)
|
||||
return {
|
||||
"path": str(state.path),
|
||||
"modified_at": datetime.fromtimestamp(
|
||||
@@ -1047,6 +1113,14 @@ def _current_game_log(
|
||||
"load_texture_to_material_failed": analysis.load_texture_to_material_failed,
|
||||
"import_scene_threw": analysis.import_scene_threw,
|
||||
"asset_pack_health_failed": analysis.asset_pack_health_failed,
|
||||
"managed_asset_integrity_failed": asset_references.managed_integrity_failed,
|
||||
"asset_references": asset_references.to_dict(),
|
||||
"runtime_out_of_memory": analysis.runtime_out_of_memory,
|
||||
"out_of_memory_count": len(analysis.out_of_memory_events),
|
||||
"out_of_memory_effect_paths": analysis.out_of_memory_effect_paths,
|
||||
"effect_creation_failures": [
|
||||
event.to_dict() for event in analysis.effect_creation_failures
|
||||
],
|
||||
"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,
|
||||
@@ -1065,6 +1139,10 @@ def _current_game_log(
|
||||
"failed_asset_file_count": analysis.failed_asset_file_count,
|
||||
"failed_texture_file_count": analysis.failed_texture_file_count,
|
||||
"failed_effect_file_count": analysis.failed_effect_file_count,
|
||||
"missing_texture_reference_count": analysis.missing_texture_reference_count,
|
||||
"missing_effect_reference_count": analysis.missing_effect_reference_count,
|
||||
"missing_image_reference_count": analysis.missing_image_reference_count,
|
||||
"missing_pack_image_reference_count": analysis.missing_pack_image_reference_count,
|
||||
"stage_without_new_stage_events": [
|
||||
event.to_dict()
|
||||
for event in analysis.events
|
||||
@@ -1079,6 +1157,28 @@ def _current_game_log(
|
||||
}
|
||||
|
||||
|
||||
def _current_game_crash(
|
||||
directory: Path,
|
||||
baseline: dict[str, LogFileState],
|
||||
started_ns: int,
|
||||
) -> dict[str, object] | None:
|
||||
state = newest_current_native_log(
|
||||
directory,
|
||||
baseline,
|
||||
started_ns,
|
||||
patterns=("*.dat", "*.dmp"),
|
||||
)
|
||||
if state is None:
|
||||
return None
|
||||
return {
|
||||
"path": str(state.path),
|
||||
"modified_at": datetime.fromtimestamp(
|
||||
state.mtime_ns / 1_000_000_000
|
||||
).astimezone().isoformat(),
|
||||
"size": state.size,
|
||||
}
|
||||
|
||||
|
||||
def _runtime_issue_summary(stderr: str) -> dict[str, object]:
|
||||
folded = stderr.casefold()
|
||||
futex_message = "the futex facility returned an unexpected error code"
|
||||
@@ -1113,14 +1213,24 @@ def _print_game_outcome(
|
||||
outcome: CommandOutcome,
|
||||
current_game_log: dict[str, object] | None,
|
||||
runtime_issues: dict[str, object],
|
||||
current_game_crash: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
# Keep the human-facing stream concise. CommandRunner already relayed every
|
||||
# process line live; replaying its capture here made each Proton failure appear
|
||||
# twice in the dashboard.
|
||||
if outcome.timed_out:
|
||||
if current_game_crash is not None:
|
||||
crash_path = current_game_crash.get("path")
|
||||
crash_path_text = crash_path if isinstance(crash_path, str) else "unknown"
|
||||
print(
|
||||
f"Game client crashed and wrote a dump: {crash_path_text}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif outcome.timed_out:
|
||||
print("Game client timed out and was stopped.", file=sys.stderr)
|
||||
elif outcome.returncode == 0:
|
||||
print("Game client exited normally.")
|
||||
elif outcome.returncode is None:
|
||||
print("Game client exited without a status.", file=sys.stderr)
|
||||
else:
|
||||
print(f"Game client exited with status {outcome.returncode}.", file=sys.stderr)
|
||||
if current_game_log is not None:
|
||||
@@ -1162,22 +1272,48 @@ def _print_game_outcome(
|
||||
|
||||
|
||||
def _print_interrupted_game_log(report: dict[str, object]) -> None:
|
||||
print(f"\nCurrent game log: {report['path']}", file=sys.stderr)
|
||||
log_path = report.get("path")
|
||||
log_path_text = log_path if isinstance(log_path, str) else "unknown"
|
||||
print(f"\nCurrent game log: {log_path_text}", file=sys.stderr)
|
||||
asset_references = report.get("asset_references")
|
||||
catalog_available = (
|
||||
asset_references.get("catalog_available")
|
||||
if isinstance(asset_references, dict)
|
||||
else False
|
||||
)
|
||||
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["asset_pack_health_failed"]:
|
||||
elif report["runtime_out_of_memory"]:
|
||||
effects = report.get("out_of_memory_effect_paths")
|
||||
effect_text = ""
|
||||
if isinstance(effects, list) and effects:
|
||||
effect_text = " while creating " + ", ".join(str(path) for path in effects[:3])
|
||||
print(
|
||||
"Detected a large number of missing game-asset files. This indicates a "
|
||||
"corrupted or incomplete installation that needs repair.",
|
||||
"Detected Direct3D resource exhaustion in the 32-bit game process"
|
||||
+ effect_text
|
||||
+ ". Restarting the client can recover temporarily; depot repair will not "
|
||||
"fix this failure.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif report["managed_asset_integrity_failed"]:
|
||||
print(
|
||||
"The game reported an asset that belongs to the publisher manifest but is "
|
||||
"missing from the managed installation.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"Run `pristontale-eu update --repair` and restart. If the issue "
|
||||
"reappears, re-download the game files with a clean install path.",
|
||||
"Run `pristontale-eu update --repair` and restart.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif report["asset_pack_health_failed"] and not catalog_available:
|
||||
print(
|
||||
"The game reported many unresolved asset paths, but no matching cached "
|
||||
"publisher manifest was available to classify them. Run "
|
||||
"`pristontale-eu update` to refresh the manifest cache.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif report["load_texture_to_material_failed"]:
|
||||
|
||||
Reference in New Issue
Block a user