From 6a508476b90955705cdec106ba2151bd53d1f824 Mon Sep 17 00:00:00 2001 From: m4ko Date: Fri, 18 Sep 2026 21:36:12 +0200 Subject: [PATCH] Added better game crash diagnostics. --- src/ptlinux/application/commands/play.py | 27 ++++++++- src/ptlinux/diagnostics/crash_dumps.py | 75 ++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/ptlinux/application/commands/play.py b/src/ptlinux/application/commands/play.py index b7990a7..4772238 100644 --- a/src/ptlinux/application/commands/play.py +++ b/src/ptlinux/application/commands/play.py @@ -866,7 +866,32 @@ def game_log_telemetry_summary(report: dict[str, object]) -> dict[str, object]: "missing_image_reference_count", "missing_pack_image_reference_count", ) - return {key: report[key] for key in keys if key in report} + summary = {key: report[key] for key in keys if key in report} + asset_references = report.get("asset_references") + if isinstance(asset_references, dict): + asset_summary = { + key: asset_references[key] + for key in ( + "total_events", + "unique_valid_references", + "invalid_reference_events", + "catalog_available", + "managed_integrity_failed", + ) + if key in asset_references + } + for source_key, output_key in ( + ("managed_present_paths", "managed_present_count"), + ("managed_missing_paths", "managed_missing_count"), + ("publisher_unmanaged_paths", "publisher_unmanaged_count"), + ("unclassified_paths", "unclassified_count"), + ): + paths = asset_references.get(source_key) + if isinstance(paths, (list, tuple)): + asset_summary[output_key] = len(paths) + if asset_summary: + summary["asset_reference_summary"] = asset_summary + return summary def crash_dump_telemetry_summary(report: dict[str, object]) -> dict[str, object]: diff --git a/src/ptlinux/diagnostics/crash_dumps.py b/src/ptlinux/diagnostics/crash_dumps.py index 88d0f91..cc60bea 100644 --- a/src/ptlinux/diagnostics/crash_dumps.py +++ b/src/ptlinux/diagnostics/crash_dumps.py @@ -134,6 +134,11 @@ def _inspect_minidump_bytes(data: bytes) -> dict[str, object] | None: exception_code = _read_u32(data, exception_rva + 8) exception_flags = _read_u32(data, exception_rva + 12) exception_address = _read_u64(data, exception_rva + 24) + exception_parameters = _read_exception_parameters( + data, + exception_rva, + _exception_size, + ) modules = _read_modules(data, streams.get(_MODULE_LIST_STREAM_TYPE)) exception_module = _module_for_address(modules, exception_address) architecture = _read_architecture(data, streams.get(_SYSTEM_INFO_STREAM_TYPE)) @@ -154,14 +159,25 @@ def _inspect_minidump_bytes(data: bytes) -> dict[str, object] | None: ), } ) + stack_pointer = _read_exception_stack_pointer( + data, + exception_stream, + architecture, + ) stack = _read_exception_stack( data, streams.get(_THREAD_LIST_STREAM_TYPE), thread_id, + stack_pointer=stack_pointer, ) if stack is not None: pointer_size = 8 if architecture in {"x86_64", "arm64", "ia64"} else 4 - signatures = _application_stack_signatures(stack, pointer_size, modules) + signatures = _application_stack_signatures( + stack, + pointer_size, + modules, + excluded_addresses=frozenset(exception_parameters), + ) if signatures: result["application_stack"] = signatures return result @@ -211,6 +227,8 @@ def _read_exception_stack( data: bytes, stream: tuple[int, int] | None, thread_id: int, + *, + stack_pointer: int | None = None, ) -> bytes | None: if stream is None or stream[1] < 4: return None @@ -222,10 +240,55 @@ def _read_exception_stack( offset = rva + 4 + index * _THREAD_ENTRY_SIZE if _read_u32(data, offset) != thread_id: continue - stack_size = min(_read_u32(data, offset + 32), _MAX_STACK_BYTES) + stack_start = _read_u64(data, offset + 24) + reported_stack_size = _read_u32(data, offset + 32) stack_rva = _read_u32(data, offset + 36) - _require_range(data, stack_rva, stack_size) - return data[stack_rva : stack_rva + stack_size] + stack_offset = 0 + if ( + stack_pointer is not None + and stack_start <= stack_pointer < stack_start + reported_stack_size + ): + stack_offset = stack_pointer - stack_start + stack_size = min( + reported_stack_size - stack_offset, + _MAX_STACK_BYTES, + ) + _require_range(data, stack_rva + stack_offset, stack_size) + return data[stack_rva + stack_offset : stack_rva + stack_offset + stack_size] + return None + + +def _read_exception_parameters( + data: bytes, + exception_rva: int, + exception_size: int, +) -> tuple[int, ...]: + parameter_count = _read_u32(data, exception_rva + 32) + if parameter_count > 15 or 40 + parameter_count * 8 > exception_size: + raise ValueError("invalid minidump exception parameters") + return tuple( + _read_u64(data, exception_rva + 40 + index * 8) + for index in range(parameter_count) + ) + + +def _read_exception_stack_pointer( + data: bytes, + stream: tuple[int, int], + architecture: str, +) -> int | None: + rva, size = stream + if size < 168: + return None + context_size = _read_u32(data, rva + 160) + context_rva = _read_u32(data, rva + 164) + if context_size == 0 or context_rva == 0: + return None + _require_range(data, context_rva, context_size) + if architecture == "x86" and context_size >= 200: + return _read_u32(data, context_rva + 196) + if architecture == "x86_64" and context_size >= 160: + return _read_u64(data, context_rva + 152) return None @@ -233,12 +296,16 @@ def _application_stack_signatures( stack: bytes, pointer_size: int, modules: tuple[_Module, ...], + *, + excluded_addresses: frozenset[int] = frozenset(), ) -> list[dict[str, str]]: result: list[dict[str, str]] = [] seen: set[tuple[str, int]] = set() format_code = "