3 Commits

Author SHA1 Message Date
be7891f066 Pushed new release: v1.0.8 2026-09-19 12:49:03 +02:00
6a508476b9 Added better game crash diagnostics. 2026-09-18 21:36:12 +02:00
9ef0eff2f5 Fixed a issue with resource exhaustion for directx. 2026-09-15 21:48:27 +02:00
8 changed files with 169 additions and 43 deletions

View File

@@ -1,6 +1,6 @@
# PristonTale EU for Linux # PristonTale EU for Linux
Version 1.0.5 installs, updates, repairs, and launches PristonTale EU through a Version 1.0.8 installs, updates, repairs, and launches PristonTale EU through a
native Linux dashboard. Game files come directly from the publisher and are not native Linux dashboard. Game files come directly from the publisher and are not
included in this archive. included in this archive.
@@ -15,11 +15,11 @@ Then run:
```text ```text
openssl pkey -pubin -in launcher-update-public.pem -outform DER | sha256sum openssl pkey -pubin -in launcher-update-public.pem -outform DER | sha256sum
openssl dgst -sha256 -verify launcher-update-public.pem \ openssl dgst -sha256 -verify launcher-update-public.pem \
-signature pristontale-eu-linux-installer-v1-0-5.tar.gz.sig \ -signature pristontale-eu-linux-installer-v1-0-8.tar.gz.sig \
pristontale-eu-linux-installer-v1-0-5.tar.gz pristontale-eu-linux-installer-v1-0-8.tar.gz
sha256sum -c pristontale-eu-linux-installer-v1-0-5.tar.gz.sha256 sha256sum -c pristontale-eu-linux-installer-v1-0-8.tar.gz.sha256
tar -xzf pristontale-eu-linux-installer-v1-0-5.tar.gz tar -xzf pristontale-eu-linux-installer-v1-0-8.tar.gz
cd pristontale-eu-linux-installer-v1-0-5 cd pristontale-eu-linux-installer-v1-0-8
./install.sh ./install.sh
``` ```

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "PTEU-Linux-Launcher" name = "PTEU-Linux-Launcher"
version = "1.0.5" version = "1.0.8"
description = "PristonTale EU Linux installer, clean launcher, and diagnostic harness" description = "PristonTale EU Linux installer, clean launcher, and diagnostic harness"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [] dependencies = []

View File

@@ -4,4 +4,4 @@ from .paths import AppPaths
__all__ = ["AppPaths", "__version__"] __all__ = ["AppPaths", "__version__"]
__version__ = "1.0.5" __version__ = "1.0.8"

View File

@@ -394,6 +394,7 @@ def play_locked(
if recommended_comparison is not None: if recommended_comparison is not None:
runtime_issues["recommended_one_variable_comparison"] = recommended_comparison runtime_issues["recommended_one_variable_comparison"] = recommended_comparison
clean_exit = outcome.returncode == 0 and current_game_crash is None clean_exit = outcome.returncode == 0 and current_game_crash is None
session_issue = game_log_issue_reason(current_game_log)
launch_evidence = game_readiness_signal launch_evidence = game_readiness_signal
if launch_evidence is None and current_game_log is not None: if launch_evidence is None and current_game_log is not None:
launch_evidence = "game_log" launch_evidence = "game_log"
@@ -402,6 +403,8 @@ def play_locked(
launch_succeeded = launch_evidence is not None launch_succeeded = launch_evidence is not None
report = { report = {
"ok": clean_exit, "ok": clean_exit,
"degraded": session_issue is not None,
"session_issue": session_issue,
"launch_succeeded": launch_succeeded, "launch_succeeded": launch_succeeded,
"dry_run": dry_run, "dry_run": dry_run,
"runtime": asdict(selection), "runtime": asdict(selection),
@@ -430,6 +433,7 @@ def play_locked(
**window_launch_metadata, **window_launch_metadata,
"runtime_seconds": round(game_runtime_seconds), "runtime_seconds": round(game_runtime_seconds),
"clean_exit": clean_exit, "clean_exit": clean_exit,
"degraded": session_issue is not None,
"timed_out": outcome.timed_out, "timed_out": outcome.timed_out,
"launch_succeeded": launch_succeeded, "launch_succeeded": launch_succeeded,
"game_window_ready": game_window_ready, "game_window_ready": game_window_ready,
@@ -440,7 +444,7 @@ def play_locked(
"realm": start_option.name, "realm": start_option.name,
**api._game_exit_metadata(outcome.returncode, selection.kind), **api._game_exit_metadata(outcome.returncode, selection.kind),
} }
if not clean_exit: if not clean_exit or session_issue is not None:
common_game_metadata["failure_reason"] = game_failure_reason( common_game_metadata["failure_reason"] = game_failure_reason(
outcome, outcome,
current_game_log, current_game_log,
@@ -516,6 +520,10 @@ def play_locked(
exit_message = game_crash_message(current_game_log) exit_message = game_crash_message(current_game_log)
elif outcome.timed_out: elif outcome.timed_out:
exit_message = "Game session timed out" exit_message = "Game session timed out"
elif session_issue == "direct3d_resource_exhaustion":
exit_message = (
"Game client exited after logging Direct3D resource exhaustion"
)
elif common_game_metadata.get("signal_inferred") is True: elif common_game_metadata.get("signal_inferred") is True:
signal_wrapper = ( signal_wrapper = (
"Proton" if selection.kind == "proton7" else "UMU" "Proton" if selection.kind == "proton7" else "UMU"
@@ -537,12 +545,14 @@ def play_locked(
severity=( severity=(
"error" "error"
if current_game_crash is not None if current_game_crash is not None
else "info" if clean_exit else "warning" else "warning"
if not clean_exit or session_issue is not None
else "info"
), ),
message=exit_message, message=exit_message,
metadata=( metadata=(
common_game_metadata common_game_metadata
if clean_exit if clean_exit and session_issue is None
else { else {
**common_game_metadata, **common_game_metadata,
**failure_evidence, **failure_evidence,
@@ -646,7 +656,7 @@ def game_failure_reason(
current_game_crash: dict[str, object] | None, current_game_crash: dict[str, object] | None,
exit_metadata: dict[str, object], exit_metadata: dict[str, object],
) -> str: ) -> str:
"""Classify the strongest evidence recorded for a failed game session.""" """Classify the strongest evidence recorded for a failed or degraded session."""
if current_game_log and current_game_log.get("runtime_out_of_memory") is True: if current_game_log and current_game_log.get("runtime_out_of_memory") is True:
return "direct3d_resource_exhaustion" return "direct3d_resource_exhaustion"
@@ -659,6 +669,16 @@ def game_failure_reason(
return "nonzero_exit" return "nonzero_exit"
def game_log_issue_reason(
current_game_log: dict[str, object] | None,
) -> str | None:
"""Classify native-log evidence that degraded an otherwise clean process exit."""
if current_game_log and current_game_log.get("runtime_out_of_memory") is True:
return "direct3d_resource_exhaustion"
return None
def game_crash_message(current_game_log: dict[str, object] | None) -> str: def game_crash_message(current_game_log: dict[str, object] | None) -> str:
if ( if (
current_game_log is not None current_game_log is not None
@@ -846,7 +866,32 @@ def game_log_telemetry_summary(report: dict[str, object]) -> dict[str, object]:
"missing_image_reference_count", "missing_image_reference_count",
"missing_pack_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]: def crash_dump_telemetry_summary(report: dict[str, object]) -> dict[str, object]:
@@ -971,6 +1016,16 @@ def print_game_outcome(
) )
elif outcome.timed_out: elif outcome.timed_out:
print("Game client timed out and was stopped.", file=sys.stderr) print("Game client timed out and was stopped.", file=sys.stderr)
elif (
outcome.returncode == 0
and game_log_issue_reason(current_game_log)
== "direct3d_resource_exhaustion"
):
print(
"Game client exited with status 0 after logging Direct3D resource "
"exhaustion.",
file=sys.stderr,
)
elif outcome.returncode == 0: elif outcome.returncode == 0:
print("Game client exited normally.") print("Game client exited normally.")
elif outcome.returncode is None: elif outcome.returncode is None:
@@ -1039,8 +1094,8 @@ def print_interrupted_game_log(report: dict[str, object]) -> None:
print( print(
"Detected Direct3D resource exhaustion in the 32-bit game process" "Detected Direct3D resource exhaustion in the 32-bit game process"
+ effect_text + effect_text
+ ". Restarting the client can recover temporarily; depot repair will not " + ". Restarting clears the process address space, but this condition can "
"fix this failure.", "recur; depot repair will not fix it.",
file=sys.stderr, file=sys.stderr,
) )
elif report["managed_asset_integrity_failed"]: elif report["managed_asset_integrity_failed"]:

View File

@@ -134,6 +134,11 @@ def _inspect_minidump_bytes(data: bytes) -> dict[str, object] | None:
exception_code = _read_u32(data, exception_rva + 8) exception_code = _read_u32(data, exception_rva + 8)
exception_flags = _read_u32(data, exception_rva + 12) exception_flags = _read_u32(data, exception_rva + 12)
exception_address = _read_u64(data, exception_rva + 24) 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)) modules = _read_modules(data, streams.get(_MODULE_LIST_STREAM_TYPE))
exception_module = _module_for_address(modules, exception_address) exception_module = _module_for_address(modules, exception_address)
architecture = _read_architecture(data, streams.get(_SYSTEM_INFO_STREAM_TYPE)) 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( stack = _read_exception_stack(
data, data,
streams.get(_THREAD_LIST_STREAM_TYPE), streams.get(_THREAD_LIST_STREAM_TYPE),
thread_id, thread_id,
stack_pointer=stack_pointer,
) )
if stack is not None: if stack is not None:
pointer_size = 8 if architecture in {"x86_64", "arm64", "ia64"} else 4 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: if signatures:
result["application_stack"] = signatures result["application_stack"] = signatures
return result return result
@@ -211,6 +227,8 @@ def _read_exception_stack(
data: bytes, data: bytes,
stream: tuple[int, int] | None, stream: tuple[int, int] | None,
thread_id: int, thread_id: int,
*,
stack_pointer: int | None = None,
) -> bytes | None: ) -> bytes | None:
if stream is None or stream[1] < 4: if stream is None or stream[1] < 4:
return None return None
@@ -222,10 +240,55 @@ def _read_exception_stack(
offset = rva + 4 + index * _THREAD_ENTRY_SIZE offset = rva + 4 + index * _THREAD_ENTRY_SIZE
if _read_u32(data, offset) != thread_id: if _read_u32(data, offset) != thread_id:
continue 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) stack_rva = _read_u32(data, offset + 36)
_require_range(data, stack_rva, stack_size) stack_offset = 0
return data[stack_rva : stack_rva + stack_size] 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 return None
@@ -233,12 +296,16 @@ def _application_stack_signatures(
stack: bytes, stack: bytes,
pointer_size: int, pointer_size: int,
modules: tuple[_Module, ...], modules: tuple[_Module, ...],
*,
excluded_addresses: frozenset[int] = frozenset(),
) -> list[dict[str, str]]: ) -> list[dict[str, str]]:
result: list[dict[str, str]] = [] result: list[dict[str, str]] = []
seen: set[tuple[str, int]] = set() seen: set[tuple[str, int]] = set()
format_code = "<Q" if pointer_size == 8 else "<I" format_code = "<Q" if pointer_size == 8 else "<I"
for offset in range(0, len(stack) - pointer_size + 1, pointer_size): for offset in range(0, len(stack) - pointer_size + 1, pointer_size):
address = struct.unpack_from(format_code, stack, offset)[0] address = struct.unpack_from(format_code, stack, offset)[0]
if address in excluded_addresses:
continue
module = _module_for_address(modules, address) module = _module_for_address(modules, address)
if module is None or module.system: if module is None or module.system:
continue continue

View File

@@ -476,6 +476,10 @@ def run_game(
*game_arguments, *game_arguments,
] ]
environment = _umu_environment(paths, selection) environment = _umu_environment(paths, selection)
# Build 22.80.90 can consume an uninitialized heap value as a vector
# length under current Wine. Proton's scoped heap-zero mode prevents the
# resulting std::length_error without changing publisher files.
environment["PROTON_HEAP_ZERO_MEMORY"] = "1"
elif selection.kind == "proton7-container": elif selection.kind == "proton7-container":
if not selection.proton_path: if not selection.proton_path:
raise RuntimeErrorPT("Proton script path is missing") raise RuntimeErrorPT("Proton script path is missing")

View File

@@ -2,16 +2,16 @@
"application": "PristonTale-EU", "application": "PristonTale-EU",
"archive": { "archive": {
"layout": "flat", "layout": "flat",
"name": "PristonTale-EU-1.0.5.tar.gz", "name": "PristonTale-EU-1.0.8.tar.gz",
"sha256": "31414eefc52325a532ded83166d083c4d8d4a6699bfa92d619197b879e6d6578", "sha256": "f9e48505b17475c4b5de6d4ebc6835f3dfcb816aeeac3ae0b4c42cce546177a4",
"size": 1544157, "size": 1544812,
"url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.0.5/PristonTale-EU-1.0.5.tar.gz?sha256=31414eefc52325a532ded83166d083c4d8d4a6699bfa92d619197b879e6d6578" "url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.0.8/PristonTale-EU-1.0.8.tar.gz?sha256=f9e48505b17475c4b5de6d4ebc6835f3dfcb816aeeac3ae0b4c42cce546177a4"
}, },
"archive_sha256": "31414eefc52325a532ded83166d083c4d8d4a6699bfa92d619197b879e6d6578", "archive_sha256": "f9e48505b17475c4b5de6d4ebc6835f3dfcb816aeeac3ae0b4c42cce546177a4",
"archive_size": 1544157, "archive_size": 1544812,
"archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.0.5/PristonTale-EU-1.0.5.tar.gz?sha256=31414eefc52325a532ded83166d083c4d8d4a6699bfa92d619197b879e6d6578", "archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.0.8/PristonTale-EU-1.0.8.tar.gz?sha256=f9e48505b17475c4b5de6d4ebc6835f3dfcb816aeeac3ae0b4c42cce546177a4",
"channel": "stable", "channel": "stable",
"commit": "67d89b40d54f1bf6d1886cd2cdbd35b0fa8b4324", "commit": "6a508476b90955705cdec106ba2151bd53d1f824",
"compatibility": { "compatibility": {
"config_schema": 1, "config_schema": 1,
"manifest_schema": 1, "manifest_schema": 1,
@@ -36,7 +36,7 @@
}, },
{ {
"path": "ptlinux/__init__.py", "path": "ptlinux/__init__.py",
"sha256": "012bb02392f8b6441d2f7f53b71b5736d70495409ee01d52ff1677dd68931cea", "sha256": "f8dcaf56174e23070242e4df9a06d6eb0e7a9b8d221ffaa6465db96227bb2976",
"size": 151 "size": 151
}, },
{ {
@@ -71,8 +71,8 @@
}, },
{ {
"path": "ptlinux/application/commands/play.py", "path": "ptlinux/application/commands/play.py",
"sha256": "7591103e801972398c2d1bd683518b9149c0903b3f6bd3de79c185f66c0b42f6", "sha256": "d4160bad360c7ba4299930b1040565468458ec4f88320220da0e2d29cf64b233",
"size": 34941 "size": 36758
}, },
{ {
"path": "ptlinux/application/commands/settings.py", "path": "ptlinux/application/commands/settings.py",
@@ -126,8 +126,8 @@
}, },
{ {
"path": "ptlinux/diagnostics/crash_dumps.py", "path": "ptlinux/diagnostics/crash_dumps.py",
"sha256": "fba6ce4110128d2e1d551e2ad720319e9e5f2b90740707b553a293c00c1d1ff6", "sha256": "b26e3805a1ef7f1ba464dec9f7ec5c453ecc450ea650183001c7703e6773d24a",
"size": 8611 "size": 10351
}, },
{ {
"path": "ptlinux/diagnostics/native_logs.py", "path": "ptlinux/diagnostics/native_logs.py",
@@ -206,8 +206,8 @@
}, },
{ {
"path": "ptlinux/game/runtime.py", "path": "ptlinux/game/runtime.py",
"sha256": "461c2c5d1774fd3d6f998dfc981ba7b6df59a95414dd66f26e1a74b67f5af8b0", "sha256": "cdeba4551aab6c54f2704d2aabc964a61b54a2cf9006567cf94a0a6aaf281061",
"size": 32162 "size": 32421
}, },
{ {
"path": "ptlinux/game/runtime_view.py", "path": "ptlinux/game/runtime_view.py",
@@ -696,21 +696,21 @@
}, },
{ {
"path": "release.json", "path": "release.json",
"sha256": "3cf9be35e4a0db075d79956b11f8d21c9ae4c8be5169690300bae335df6e7d01", "sha256": "37426179c48b341bb457d376adaa415af3b28a009df574b0c0bb014a4e21d5fe",
"size": 218 "size": 218
} }
], ],
"minimum_updater_version": "0.9.9", "minimum_updater_version": "0.9.9",
"notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.0.5", "notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.0.8",
"published_at": "2026-09-15T19:15:39Z", "published_at": "2026-09-18T19:36:12Z",
"release": { "release": {
"notes_markdown": "# PristonTale EU Launcher 1.0.5\n\nAdds opt-in, bounded game crash-dump attachments for telemetry, improves game-exit diagnostics and Proton 7 crash classification, and packages the current launcher, runtime compatibility, and native helper updates.\n\n## Changes\n\n- Updated telemetry to support game dumps. (`67d89b4`)\n- Released version 1.0.2 (`c278efa`)\n- Reworked some game window and gamescope methods for Cinnamon. (`f3059c3`)\n- Added more folders and restructured classes inside them. (`2cbb931`)\n- Reworked the project structure. (`be7a478`)\n- Delete RELEASE_PIPELINE_WORKPLAN.md (`15741f5`)\n- Add channel-aware release tooling and updater recovery (`0a89323`)\n- Publish signed launcher release history (`cc48bf4`)\n- Upload files to \"updates\" (`84f8de5`)\n- Delete updates/stable.json.sig (`1329485`)\n- Delete updates/stable.json (`8f1655b`)\n- Fix v0.9.11 archive download cache (`be71d31`)\n- Upload files to \"updates\" (`46e11ae`)\n- Delete updates/stable.json.sig (`eeb993e`)\n- Delete updates/stable.json (`a90bd19`)\n\n---\n\nVersion: `1.0.5` \nChannel: `stable` \nTag: `v1.0.5` \nSource commit: `67d89b40d54f1bf6d1886cd2cdbd35b0fa8b4324`\n", "notes_markdown": "# PristonTale EU Launcher 1.0.8\n\nPrevents the Build 22.80.90 startup crash under UMU-Proton 10 and packages the\ncurrent launcher, diagnostics, telemetry, and native compatibility updates.\n\n## Changes\n\n- Enable Proton's heap-zero compatibility mode for UMU game launches to prevent\n the reproduced `std::length_error(\"vector too long\")` startup failure.\n- Improve crash-dump stack analysis and privacy-safe asset-failure telemetry.\n- Improve Direct3D resource-exhaustion handling and current runtime packaging.\n\n---\n\nVersion: `1.0.8` \nChannel: `stable` \nTag: `v1.0.8` \nSource commit: `6a508476b90955705cdec106ba2151bd53d1f824`\n",
"release_notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.0.5", "release_notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.0.8",
"summary": "Adds opt-in, bounded game crash-dump attachments for telemetry, improves game-exit diagnostics and Proton 7 crash classification, and packages the current launcher, runtime compatibility, and native helper updates.", "summary": "Prevents the Build 22.80.90 startup crash under UMU-Proton 10 and packages the",
"title": "PristonTale EU Launcher 1.0.5" "title": "PristonTale EU Launcher 1.0.8"
}, },
"schema": 1, "schema": 1,
"status": "active", "status": "active",
"tag": "v1.0.5", "tag": "v1.0.8",
"version": "1.0.5" "version": "1.0.8"
} }

Binary file not shown.