Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 531480b195 | |||
| 770bbb8a88 |
@@ -1,6 +1,6 @@
|
||||
# PristonTale EU for Linux
|
||||
|
||||
Version 1.0.8 installs, updates, repairs, and launches PristonTale EU through a
|
||||
Version 1.1.1 installs, updates, repairs, and launches PristonTale EU through a
|
||||
native Linux dashboard. Game files come directly from the publisher and are not
|
||||
included in this archive.
|
||||
|
||||
@@ -15,11 +15,11 @@ Then run:
|
||||
```text
|
||||
openssl pkey -pubin -in launcher-update-public.pem -outform DER | sha256sum
|
||||
openssl dgst -sha256 -verify launcher-update-public.pem \
|
||||
-signature pristontale-eu-linux-installer-v1-0-8.tar.gz.sig \
|
||||
pristontale-eu-linux-installer-v1-0-8.tar.gz
|
||||
sha256sum -c pristontale-eu-linux-installer-v1-0-8.tar.gz.sha256
|
||||
tar -xzf pristontale-eu-linux-installer-v1-0-8.tar.gz
|
||||
cd pristontale-eu-linux-installer-v1-0-8
|
||||
-signature pristontale-eu-linux-installer-v1-1-1.tar.gz.sig \
|
||||
pristontale-eu-linux-installer-v1-1-1.tar.gz
|
||||
sha256sum -c pristontale-eu-linux-installer-v1-1-1.tar.gz.sha256
|
||||
tar -xzf pristontale-eu-linux-installer-v1-1-1.tar.gz
|
||||
cd pristontale-eu-linux-installer-v1-1-1
|
||||
./install.sh
|
||||
```
|
||||
|
||||
@@ -34,10 +34,15 @@ Existing installations at the previous default location are preserved during
|
||||
upgrades. The installer adds **PristonTale EU** to the application menu and
|
||||
installs the `pristontale-eu` command in `~/.local/bin`.
|
||||
|
||||
Steam Proton 7.0 is discovered in both the default Steam library and secondary
|
||||
libraries registered with the Steam client. If Proton uses an unregistered
|
||||
manual layout, open **Settings → Optional Overrides** and select the exact
|
||||
`Proton 7.0/proton` script.
|
||||
GE-Proton through the managed UMU helper is the default runtime for the current
|
||||
game, with modern WoW64 and a separate compatibility prefix. Existing settings
|
||||
switch to GE-Proton once on startup; users can choose another runtime afterwards.
|
||||
Renderer preferences and saved Proton paths are retained. PTR continues to use
|
||||
Proton 7.0. Proton 7.0 is discovered in both
|
||||
the default Steam library and secondary libraries registered with Steam. For an
|
||||
unregistered manual layout, select its exact `proton` script under
|
||||
**Settings → Optional Overrides**. Use the renderer setting to choose DXVK or
|
||||
WineD3D; environment assignments do not belong in the Proton script path.
|
||||
|
||||
## Use
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "PTEU-Linux-Launcher"
|
||||
version = "1.0.8"
|
||||
version = "1.1.1"
|
||||
description = "PristonTale EU Linux installer, clean launcher, and diagnostic harness"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
|
||||
@@ -4,4 +4,4 @@ from .paths import AppPaths
|
||||
|
||||
__all__ = ["AppPaths", "__version__"]
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.1.2"
|
||||
|
||||
@@ -21,6 +21,7 @@ from ...diagnostics.crash_dumps import (
|
||||
prepare_minidump_attachment,
|
||||
)
|
||||
from ...diagnostics.native_logs import LogFileState
|
||||
from ...game.depot.realms import PTR_BABEL_REALM_NAME, launcher_realm_name
|
||||
from ...game.runtime import RuntimeErrorPT, runtime_settings_for_realm
|
||||
from ...launcher import LauncherError
|
||||
from ...login_trace import LOGIN_TRACE_DISABLED_REASON
|
||||
@@ -394,6 +395,8 @@ def play_locked(
|
||||
recommended_comparison = recommended_runtime_comparison(
|
||||
selection.kind,
|
||||
runtime_issues,
|
||||
current_game_log=current_game_log,
|
||||
realm=start_option.name,
|
||||
)
|
||||
if recommended_comparison is not None:
|
||||
runtime_issues["recommended_one_variable_comparison"] = recommended_comparison
|
||||
@@ -632,9 +635,24 @@ def game_exit_metadata(
|
||||
def recommended_runtime_comparison(
|
||||
runtime_kind: str,
|
||||
runtime_issues: dict[str, object],
|
||||
*,
|
||||
current_game_log: dict[str, object] | None = None,
|
||||
realm: str | None = None,
|
||||
) -> str | None:
|
||||
"""Choose a useful one-variable comparison from launch-relevant evidence."""
|
||||
|
||||
if realm and launcher_realm_name(realm).casefold() == PTR_BABEL_REALM_NAME.casefold():
|
||||
# PTR pins Proton 7 for its client-specific compatibility requirements.
|
||||
return None
|
||||
if current_game_log and current_game_log.get("runtime_out_of_memory") is True:
|
||||
# GE enables modern WoW64 in an isolated prefix. Compare it before
|
||||
# chasing media-plugin scan warnings; the exact exhausted resource is
|
||||
# not identified by the game's E_OUTOFMEMORY error alone.
|
||||
return (
|
||||
"ge-proton"
|
||||
if runtime_kind in {"proton7", "proton7-container", "umu"}
|
||||
else None
|
||||
)
|
||||
if (
|
||||
runtime_kind == "proton7-container"
|
||||
and runtime_issues.get("host_futex_fatal_count")
|
||||
|
||||
@@ -861,11 +861,9 @@ def _source_game(
|
||||
|
||||
|
||||
def _load_or_default_settings(paths: AppPaths, *, persist: bool) -> Settings:
|
||||
if paths.settings_file.is_file():
|
||||
return load_settings(paths)
|
||||
if persist:
|
||||
return ensure_settings(paths)
|
||||
return Settings()
|
||||
return load_settings(paths)
|
||||
|
||||
|
||||
def _runner(paths: AppPaths, operation: str, dry_run: bool) -> CommandRunner:
|
||||
|
||||
@@ -443,7 +443,7 @@ def parse_native_log_lines(lines: Iterable[str]) -> NativeLogAnalysis:
|
||||
):
|
||||
analysis.out_of_memory_effect_paths.append(value)
|
||||
|
||||
if "Stage without new stage" in raw:
|
||||
if "stage without new stage" in raw.casefold():
|
||||
analysis.stage_without_new_stage = True
|
||||
analysis.events.append(
|
||||
LogEvent("stage_without_new_stage", line_number, raw, timestamp)
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import select
|
||||
@@ -301,24 +302,40 @@ class _X11:
|
||||
self._restore_error_handler()
|
||||
|
||||
def root_window_ids(self) -> set[int]:
|
||||
return set(self._root_windows())
|
||||
# Keep client IDs as well as frames: decorations can be reparented while
|
||||
# another account starts, and a new frame must not make an old game new.
|
||||
frames = self._root_windows()
|
||||
return set(frames) | {self._client_window(frame) for frame in frames}
|
||||
|
||||
def find_first_window(
|
||||
self,
|
||||
title_fragment: str,
|
||||
*,
|
||||
excluded_window_ids: Collection[int],
|
||||
expected_pid: int | None = None,
|
||||
) -> tuple[int, str] | None:
|
||||
windows: list[tuple[int, str]] = []
|
||||
for window in self._root_windows():
|
||||
for frame in self._root_windows():
|
||||
if frame in excluded_window_ids:
|
||||
continue
|
||||
window = self._client_window(frame)
|
||||
if window in excluded_window_ids:
|
||||
continue
|
||||
if expected_pid is not None and self.window_pid(window) != expected_pid:
|
||||
continue
|
||||
# Reparenting WMs need not copy a client's title onto its outer frame.
|
||||
title = self._window_title(window)
|
||||
if title is None and frame != window:
|
||||
title = self._window_title(frame)
|
||||
if title is not None:
|
||||
windows.append((window, title))
|
||||
selected = _select_game_window(windows, title_fragment, excluded_window_ids)
|
||||
if selected is None:
|
||||
return None
|
||||
window, title = selected
|
||||
return self._client_window(window), title
|
||||
return _select_game_window(windows, title_fragment, excluded_window_ids)
|
||||
|
||||
def window_pid(self, window: int) -> int | None:
|
||||
"""Read the client's process identity within this X server."""
|
||||
|
||||
values = self._long_property(window, b"_NET_WM_PID", 1)
|
||||
return values[0] if values and values[0] > 0 else None
|
||||
|
||||
def find_window_by_pid(self, expected_pid: int) -> int | None:
|
||||
"""Find a top-level host window created by one native process."""
|
||||
@@ -1057,6 +1074,7 @@ class _X11:
|
||||
window: int,
|
||||
*,
|
||||
borderless: bool,
|
||||
raise_window: bool = True,
|
||||
) -> tuple[int, int] | None:
|
||||
# Capture the real client size for both decorated and borderless windows.
|
||||
# Wine/DXVK can otherwise retain a desktop-sized render surface inside a
|
||||
@@ -1064,7 +1082,8 @@ class _X11:
|
||||
target_size = self.window_size(window)
|
||||
if borderless:
|
||||
self._remove_decorations(window)
|
||||
self.library.XMapRaised(self.display, window)
|
||||
if raise_window:
|
||||
self.library.XMapRaised(self.display, window)
|
||||
self.library.XFlush(self.display)
|
||||
return target_size
|
||||
|
||||
@@ -1133,6 +1152,8 @@ class _X11:
|
||||
self.library.XSync(self.display, 0)
|
||||
|
||||
def window_is_focused(self, window: int) -> bool:
|
||||
"""Accept focus on the client or one of Wine's child input windows."""
|
||||
|
||||
focused = ctypes.c_ulong()
|
||||
revert_to = ctypes.c_int()
|
||||
if self.library.XGetInputFocus(
|
||||
@@ -1141,7 +1162,33 @@ class _X11:
|
||||
ctypes.byref(revert_to),
|
||||
) != 1:
|
||||
return False
|
||||
return int(focused.value) == window
|
||||
candidate = int(focused.value)
|
||||
visited: set[int] = set()
|
||||
# None (0) and PointerRoot (1) are special focus values, not clients.
|
||||
while candidate not in {0, 1} and candidate not in visited and len(visited) < 64:
|
||||
if candidate == window:
|
||||
return True
|
||||
visited.add(candidate)
|
||||
root = ctypes.c_ulong()
|
||||
parent = ctypes.c_ulong()
|
||||
children = ctypes.POINTER(ctypes.c_ulong)()
|
||||
child_count = ctypes.c_uint()
|
||||
try:
|
||||
status = self.library.XQueryTree(
|
||||
self.display,
|
||||
candidate,
|
||||
ctypes.byref(root),
|
||||
ctypes.byref(parent),
|
||||
ctypes.byref(children),
|
||||
ctypes.byref(child_count),
|
||||
)
|
||||
if status == 0 or candidate == root.value:
|
||||
return False
|
||||
candidate = int(parent.value)
|
||||
finally:
|
||||
if children:
|
||||
self.library.XFree(ctypes.cast(children, ctypes.c_void_p))
|
||||
return False
|
||||
|
||||
def window_is_active(self, window: int) -> bool:
|
||||
"""Return whether the desktop already considers this client active."""
|
||||
@@ -1167,10 +1214,22 @@ class _X11:
|
||||
)
|
||||
if active_window == 0:
|
||||
return False
|
||||
# Wine may keep its last input timestamp on a separate user-time window.
|
||||
# Use this client's timestamp; borrowing another application's timestamp
|
||||
# would incorrectly bypass the desktop's focus-stealing prevention.
|
||||
user_time_window = self._long_property(window, b"_NET_WM_USER_TIME_WINDOW", 1)
|
||||
time_window = user_time_window[0] if user_time_window and user_time_window[0] else window
|
||||
user_time = self._long_property(
|
||||
time_window,
|
||||
b"_NET_WM_USER_TIME",
|
||||
1,
|
||||
)
|
||||
if not user_time and time_window != window:
|
||||
user_time = self._long_property(window, b"_NET_WM_USER_TIME", 1)
|
||||
return self._send_root_client_message(
|
||||
window,
|
||||
active_window,
|
||||
(1, _X_CURRENT_TIME, 0, 0, 0),
|
||||
(1, user_time[0] if user_time else _X_CURRENT_TIME, 0, 0, 0),
|
||||
)
|
||||
|
||||
def _send_root_client_message(
|
||||
@@ -1495,7 +1554,14 @@ def _prepare_game_window(
|
||||
*,
|
||||
borderless: bool,
|
||||
suppress_wm_ping: bool,
|
||||
raise_window: bool = True,
|
||||
) -> None:
|
||||
if not raise_window:
|
||||
try:
|
||||
if mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
if suppress_wm_ping:
|
||||
try:
|
||||
ping_suppressed = mapper.suppress_wm_ping(window)
|
||||
@@ -1520,7 +1586,11 @@ def _prepare_game_window(
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
target_size = mapper.map_window(window, borderless=borderless)
|
||||
target_size = mapper.map_window(
|
||||
window,
|
||||
borderless=borderless,
|
||||
**({"raise_window": False} if not raise_window else {}),
|
||||
)
|
||||
forced_windowed = False
|
||||
try:
|
||||
forced_windowed = mapper.force_windowed_state(window)
|
||||
@@ -1596,18 +1666,67 @@ def _apply_game_window_identity(mapper: _X11, window: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _focus_game_window(mapper: _X11, window: int) -> None:
|
||||
def _focus_game_window(
|
||||
mapper: _X11,
|
||||
window: int,
|
||||
*,
|
||||
activate: bool = True,
|
||||
diagnostics: WindowDiagnosticRecorder | None = None,
|
||||
) -> None:
|
||||
"""Let the dashboard process its ready event before focusing the game."""
|
||||
|
||||
time.sleep(WINDOW_FOCUS_DELAY_SECONDS)
|
||||
activation_requested = False
|
||||
activation_submitted = False
|
||||
outcome = "not_activated"
|
||||
try:
|
||||
mapper.focus_window(window)
|
||||
if activate:
|
||||
if mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
||||
outcome = "not_mapped"
|
||||
return
|
||||
if not mapper.window_is_active(window):
|
||||
activation_requested = True
|
||||
activation_submitted = mapper.activate_window(window)
|
||||
if not activation_submitted:
|
||||
outcome = "request_failed"
|
||||
return
|
||||
# XSendEvent success only means the request was sent. The desktop
|
||||
# may refuse it, or take longer than a fixed sleep to activate us.
|
||||
deadline = time.monotonic() + WINDOW_STATE_CONFIRM_TIMEOUT_SECONDS
|
||||
while True:
|
||||
if mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
||||
outcome = "not_mapped"
|
||||
return
|
||||
if mapper.window_is_active(window):
|
||||
break
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
return
|
||||
time.sleep(min(WINDOW_CONFIRM_POLL_SECONDS, remaining))
|
||||
# Recheck ownership after waiting and preserve an already-focused
|
||||
# Wine child. Never override a declined activation with XMapRaised.
|
||||
focused = _repair_game_window_focus(mapper, window)
|
||||
else:
|
||||
# Host focus may have changed during the dashboard handoff delay.
|
||||
focused = _repair_gamescope_game_window_focus(mapper, window)
|
||||
outcome = "focused" if focused else "focus_not_owned"
|
||||
except Exception as exc:
|
||||
outcome = "error"
|
||||
print(
|
||||
f"Warning: could not focus the game window ({exc}); continuing normally",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
finally:
|
||||
if diagnostics is not None:
|
||||
diagnostics.record(
|
||||
"focus_handoff",
|
||||
window_id=window,
|
||||
activation_requested=activation_requested,
|
||||
activation_submitted=activation_submitted,
|
||||
outcome=outcome,
|
||||
**_sample_window_diagnostic_details(diagnostics, mapper, window),
|
||||
)
|
||||
|
||||
|
||||
def _repair_game_window_focus(
|
||||
@@ -1632,7 +1751,7 @@ def _repair_game_window_focus(
|
||||
active=active,
|
||||
focused=focused,
|
||||
)
|
||||
if state == _X_WM_STATE_ICONIC:
|
||||
if state != _X_WM_STATE_NORMAL:
|
||||
return False
|
||||
if not active:
|
||||
return False
|
||||
@@ -1641,6 +1760,9 @@ def _repair_game_window_focus(
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
# Diagnostic/property reads above can outlast an Alt-Tab or minimize.
|
||||
if mapper.window_state(window) != _X_WM_STATE_NORMAL or not mapper.window_is_active(window):
|
||||
return False
|
||||
mapper.restore_input_focus(window)
|
||||
repaired = mapper.window_is_focused(window)
|
||||
if diagnostics is not None:
|
||||
@@ -1697,13 +1819,15 @@ def _repair_gamescope_game_window_focus(
|
||||
wm_state=_wm_state_name(state),
|
||||
focused=focused,
|
||||
)
|
||||
if state == _X_WM_STATE_ICONIC:
|
||||
if state != _X_WM_STATE_NORMAL:
|
||||
return False
|
||||
if focused:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
if not _gamescope_host_focus_is_allowed() or mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
||||
return False
|
||||
mapper.restore_input_focus(window)
|
||||
repaired = mapper.window_is_focused(window)
|
||||
if diagnostics is not None:
|
||||
@@ -1769,6 +1893,9 @@ def _gamescope_host_focus_status(
|
||||
return {"allowed": False, "bridge": "unreadable"}
|
||||
observed_at = time.monotonic() if now is None else now
|
||||
age = observed_at - published_at
|
||||
# float() accepts NaN/infinity; round() would terminate the supervisor.
|
||||
if not math.isfinite(published_at) or not math.isfinite(age * 1000):
|
||||
return {"allowed": False, "bridge": "invalid"}
|
||||
age_ms = round(age * 1000)
|
||||
if age < 0.0:
|
||||
return {"allowed": False, "bridge": "future", "age_ms": age_ms}
|
||||
@@ -1919,10 +2046,22 @@ def _maintain_game_window_scaling(
|
||||
process: subprocess.Popen[bytes],
|
||||
window: int,
|
||||
*,
|
||||
window_title: str = WINDOW_TITLE,
|
||||
existing_window_ids: Collection[int] = (),
|
||||
expected_pid: int | None = None,
|
||||
diagnostics: WindowDiagnosticRecorder | None = None,
|
||||
) -> int:
|
||||
"""Keep host scaling hints across Wine maximize/restore transitions."""
|
||||
|
||||
def keep_focus_monitoring() -> int:
|
||||
return _maintain_game_window_focus(
|
||||
mapper, process, window,
|
||||
window_title=window_title,
|
||||
existing_window_ids=existing_window_ids,
|
||||
expected_pid=expected_pid,
|
||||
**({"diagnostics": diagnostics} if diagnostics is not None else {}),
|
||||
)
|
||||
|
||||
try:
|
||||
mapper.select_scaling_hint_events(window)
|
||||
except Exception as exc:
|
||||
@@ -1938,7 +2077,7 @@ def _maintain_game_window_scaling(
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return int(process.wait())
|
||||
return keep_focus_monitoring()
|
||||
|
||||
scaling_enabled = (
|
||||
_enable_game_window_scaling(mapper, window)
|
||||
@@ -1950,7 +2089,7 @@ def _maintain_game_window_scaling(
|
||||
)
|
||||
)
|
||||
if not scaling_enabled:
|
||||
return int(process.wait())
|
||||
return keep_focus_monitoring()
|
||||
|
||||
repair_failures = 0
|
||||
next_scaling_repair_at = 0.0
|
||||
@@ -1965,7 +2104,7 @@ def _maintain_game_window_scaling(
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
break
|
||||
return keep_focus_monitoring()
|
||||
now = time.monotonic()
|
||||
if now >= next_focus_repair_at:
|
||||
if diagnostics is None:
|
||||
@@ -2005,7 +2144,7 @@ def _maintain_game_window_scaling(
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
break
|
||||
return keep_focus_monitoring()
|
||||
else:
|
||||
repair_failures = 0
|
||||
if hints_changed and diagnostics is not None:
|
||||
@@ -2018,34 +2157,100 @@ def _maintain_game_window_scaling(
|
||||
return int(process.wait())
|
||||
|
||||
|
||||
def _resolve_direct_game_window(
|
||||
mapper: _X11,
|
||||
window: int,
|
||||
*,
|
||||
window_title: str,
|
||||
excluded_window_ids: Collection[int],
|
||||
expected_pid: int | None,
|
||||
diagnostics: WindowDiagnosticRecorder | None = None,
|
||||
) -> int | None:
|
||||
"""Follow a replacement from the same game without restoring minimized clients."""
|
||||
|
||||
try:
|
||||
state = mapper.window_state(window)
|
||||
if (
|
||||
state is not None
|
||||
and state != _X_WM_STATE_WITHDRAWN
|
||||
and (expected_pid is None or mapper.window_pid(window) == expected_pid)
|
||||
):
|
||||
return window
|
||||
# A shared host display can contain several accounts with identical
|
||||
# titles. Without the original client's PID, a replacement is ambiguous.
|
||||
if expected_pid is None:
|
||||
return None
|
||||
found = mapper.find_first_window(
|
||||
window_title,
|
||||
excluded_window_ids=set(excluded_window_ids) | {window},
|
||||
expected_pid=expected_pid,
|
||||
)
|
||||
if found is None:
|
||||
return None
|
||||
replacement, title = found
|
||||
except Exception:
|
||||
return None
|
||||
_apply_game_window_identity(mapper, replacement)
|
||||
if diagnostics is not None:
|
||||
diagnostics.record(
|
||||
"game_window_replaced",
|
||||
previous_window_id=window,
|
||||
window_id=replacement,
|
||||
**_sample_window_diagnostic_details(diagnostics, mapper, replacement),
|
||||
)
|
||||
print(
|
||||
f"Game window changed; continuing focus monitoring: {title}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return replacement
|
||||
|
||||
|
||||
def _maintain_game_window_focus(
|
||||
mapper: _X11,
|
||||
process: subprocess.Popen[bytes],
|
||||
window: int,
|
||||
*,
|
||||
window_title: str = WINDOW_TITLE,
|
||||
existing_window_ids: Collection[int] = (),
|
||||
expected_pid: int | None = None,
|
||||
diagnostics: WindowDiagnosticRecorder | None = None,
|
||||
) -> int:
|
||||
"""Keep the keyboard-focus repair while retaining Wine's fixed-size hints."""
|
||||
"""Keep keyboard-focus repair without making further window-size changes."""
|
||||
|
||||
print(
|
||||
"Game window resizing is disabled to prevent repeated Direct3D "
|
||||
"device resets.",
|
||||
"Game window focus monitoring active.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
if expected_pid is None:
|
||||
try:
|
||||
expected_pid = mapper.window_pid(window)
|
||||
except Exception:
|
||||
pass
|
||||
retired_window_ids = set(existing_window_ids)
|
||||
next_focus_repair_at = time.monotonic() + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
||||
while process.poll() is None:
|
||||
time.sleep(WINDOW_SCALING_EVENT_TIMEOUT_SECONDS)
|
||||
now = time.monotonic()
|
||||
if now < next_focus_repair_at:
|
||||
continue
|
||||
if diagnostics is None:
|
||||
_repair_game_window_focus(mapper, window)
|
||||
else:
|
||||
resolved_window = _resolve_direct_game_window(
|
||||
mapper,
|
||||
window,
|
||||
window_title=window_title,
|
||||
excluded_window_ids=retired_window_ids,
|
||||
expected_pid=expected_pid,
|
||||
diagnostics=diagnostics,
|
||||
)
|
||||
if resolved_window is not None:
|
||||
if resolved_window != window:
|
||||
retired_window_ids.add(window)
|
||||
window = resolved_window
|
||||
_repair_game_window_focus(
|
||||
mapper,
|
||||
window,
|
||||
diagnostics=diagnostics,
|
||||
**({"diagnostics": diagnostics} if diagnostics is not None else {}),
|
||||
)
|
||||
next_focus_repair_at = time.monotonic() + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
||||
return int(process.wait())
|
||||
@@ -2120,7 +2325,8 @@ def _guard_game_window_startup(
|
||||
) -> bool:
|
||||
"""Recover Wine's login-screen window transition without hiding the client."""
|
||||
|
||||
deadline = time.monotonic() + WINDOW_STARTUP_GUARD_SECONDS
|
||||
started_at = time.monotonic()
|
||||
deadline = started_at + WINDOW_STARTUP_GUARD_SECONDS
|
||||
try:
|
||||
initial_borderless = (
|
||||
mapper.window_is_borderless(window) if borderless else None
|
||||
@@ -2150,8 +2356,10 @@ def _guard_game_window_startup(
|
||||
**_sample_window_diagnostic_details(diagnostics, mapper, window),
|
||||
)
|
||||
recovery_count = 0
|
||||
next_focus_repair_at = started_at + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
||||
while process.poll() is None:
|
||||
if time.monotonic() >= deadline:
|
||||
now = time.monotonic()
|
||||
if now >= deadline:
|
||||
return True
|
||||
try:
|
||||
state = mapper.window_state(window)
|
||||
@@ -2193,6 +2401,17 @@ def _guard_game_window_startup(
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
if state is None or state == _X_WM_STATE_WITHDRAWN:
|
||||
# Let the focus monitor look for the replacement without waiting
|
||||
# out the startup guard on a window which no longer exists.
|
||||
return False
|
||||
if now >= next_focus_repair_at:
|
||||
_repair_game_window_focus(
|
||||
mapper,
|
||||
window,
|
||||
**({"diagnostics": diagnostics} if diagnostics is not None else {}),
|
||||
)
|
||||
next_focus_repair_at = now + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
||||
if (
|
||||
not entered_screen_filling_mode
|
||||
and not login_style_reset
|
||||
@@ -2200,13 +2419,18 @@ def _guard_game_window_startup(
|
||||
time.sleep(WINDOW_STARTUP_POLL_SECONDS)
|
||||
continue
|
||||
|
||||
try:
|
||||
if state != _X_WM_STATE_NORMAL or not mapper.window_is_active(window):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
recovery_count += 1
|
||||
if entered_screen_filling_mode:
|
||||
reason = "entered fullscreen/maximized mode"
|
||||
else:
|
||||
reason = "changed its window layout"
|
||||
print(
|
||||
f"Game startup window {reason}; restoring and refocusing it.",
|
||||
f"Game startup window {reason}; restoring its window layout.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@@ -2219,16 +2443,16 @@ def _guard_game_window_startup(
|
||||
**_sample_window_diagnostic_details(diagnostics, mapper, window),
|
||||
)
|
||||
try:
|
||||
mapper.activate_window(window)
|
||||
time.sleep(WINDOW_ACTIVATION_DELAY_SECONDS)
|
||||
if mapper.window_state(window) != _X_WM_STATE_NORMAL or not mapper.window_is_active(window):
|
||||
return False
|
||||
_prepare_game_window(
|
||||
mapper,
|
||||
window,
|
||||
borderless=borderless,
|
||||
suppress_wm_ping=False,
|
||||
raise_window=False,
|
||||
)
|
||||
mapper.activate_window(window)
|
||||
mapper.focus_window(window)
|
||||
_repair_game_window_focus(mapper, window)
|
||||
if borderless:
|
||||
borderless_was_applied = (
|
||||
mapper.window_is_borderless(window) is True
|
||||
@@ -2322,6 +2546,7 @@ def run_game_window(
|
||||
finally:
|
||||
diagnostics.close(process_started=True, managed=False)
|
||||
game_window: int | None = None
|
||||
game_window_pid: int | None = None
|
||||
game_title: str | None = None
|
||||
game_window_created_at: float | None = None
|
||||
deadline = time.monotonic() + WINDOW_DISCOVERY_TIMEOUT_SECONDS
|
||||
@@ -2336,6 +2561,10 @@ def run_game_window(
|
||||
if found is not None:
|
||||
window, title = found
|
||||
game_window = window
|
||||
try:
|
||||
game_window_pid = mapper.window_pid(window)
|
||||
except Exception:
|
||||
game_window_pid = None
|
||||
game_title = title
|
||||
game_window_created_at = now
|
||||
_apply_game_window_identity(mapper, window)
|
||||
@@ -2381,7 +2610,12 @@ def run_game_window(
|
||||
),
|
||||
)
|
||||
if _gamescope_host_focus_is_allowed():
|
||||
_focus_game_window(mapper, game_window)
|
||||
_focus_game_window(
|
||||
mapper,
|
||||
game_window,
|
||||
activate=False,
|
||||
**({"diagnostics": diagnostic_sink} if diagnostic_sink is not None else {}),
|
||||
)
|
||||
print(
|
||||
"Game presentation is isolated; Gamescope handles "
|
||||
"aspect-preserving outer-window resizing.",
|
||||
@@ -2405,6 +2639,7 @@ def run_game_window(
|
||||
game_window,
|
||||
borderless=borderless,
|
||||
suppress_wm_ping=suppress_wm_ping,
|
||||
raise_window=False,
|
||||
)
|
||||
print(
|
||||
f"Game window ready: {game_title or window_title}",
|
||||
@@ -2422,7 +2657,11 @@ def run_game_window(
|
||||
game_window,
|
||||
),
|
||||
)
|
||||
_focus_game_window(mapper, game_window)
|
||||
_focus_game_window(
|
||||
mapper,
|
||||
game_window,
|
||||
**({"diagnostics": diagnostic_sink} if diagnostic_sink is not None else {}),
|
||||
)
|
||||
startup_stable = _guard_game_window_startup(
|
||||
mapper,
|
||||
process,
|
||||
@@ -2444,31 +2683,32 @@ def run_game_window(
|
||||
mapper,
|
||||
process,
|
||||
game_window,
|
||||
window_title=window_title,
|
||||
existing_window_ids=existing_window_ids,
|
||||
expected_pid=game_window_pid,
|
||||
**(
|
||||
{"diagnostics": diagnostic_sink}
|
||||
if diagnostic_sink is not None
|
||||
else {}
|
||||
),
|
||||
)
|
||||
if not borderless:
|
||||
if allow_window_resizing and not startup_stable:
|
||||
print(
|
||||
"Warning: live window resizing was not enabled because the "
|
||||
"startup window did not stabilize.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return _maintain_game_window_focus(
|
||||
mapper,
|
||||
process,
|
||||
game_window,
|
||||
**(
|
||||
{"diagnostics": diagnostic_sink}
|
||||
if diagnostic_sink is not None
|
||||
else {}
|
||||
),
|
||||
if allow_window_resizing and not startup_stable and not borderless:
|
||||
print(
|
||||
"Warning: live window resizing was not enabled because the "
|
||||
"startup window did not stabilize.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return int(process.wait())
|
||||
# Borderless clients need the same active-window-only input repair.
|
||||
return _maintain_game_window_focus(
|
||||
mapper,
|
||||
process,
|
||||
game_window,
|
||||
window_title=window_title,
|
||||
existing_window_ids=existing_window_ids,
|
||||
expected_pid=game_window_pid,
|
||||
**({"diagnostics": diagnostic_sink} if diagnostic_sink is not None else {}),
|
||||
)
|
||||
time.sleep(0.1)
|
||||
if process.poll() is None:
|
||||
print(
|
||||
@@ -2482,6 +2722,7 @@ def run_game_window(
|
||||
game_window,
|
||||
borderless=borderless,
|
||||
suppress_wm_ping=suppress_wm_ping,
|
||||
raise_window=False,
|
||||
)
|
||||
return int(process.wait())
|
||||
finally:
|
||||
|
||||
@@ -18,7 +18,9 @@ from .update_resolver import SUPPORTED_CHANNELS
|
||||
|
||||
|
||||
SETTINGS_SCHEMA = 1
|
||||
DEFAULT_GAME_RUNTIME = "ge-proton"
|
||||
_TITLE_FRAME_DEFAULT_MIGRATION_KEY = "title_frame_default_migrated"
|
||||
_GE_PROTON_DEFAULT_MIGRATION_KEY = "ge_proton_default_migrated"
|
||||
UI_SCALE_CHOICES = frozenset({"auto", "100", "125", "150", "175", "200"})
|
||||
_RETIRED_SETTINGS = frozenset(
|
||||
{
|
||||
@@ -43,7 +45,7 @@ class Settings:
|
||||
game_crash_dump_upload_enabled: bool = False
|
||||
borderless: bool = False
|
||||
suppress_unresponsive_warnings: bool = True
|
||||
runtime: str = "auto"
|
||||
runtime: str = DEFAULT_GAME_RUNTIME
|
||||
proton_sync: str = "auto"
|
||||
proton_path: str | None = None
|
||||
renderer: str = "auto"
|
||||
@@ -188,7 +190,7 @@ def _settings_from_object(value: dict[str, object]) -> Settings:
|
||||
"suppress_unresponsive_warnings",
|
||||
True,
|
||||
),
|
||||
runtime=_string(value, "runtime", "auto"),
|
||||
runtime=_string(value, "runtime", DEFAULT_GAME_RUNTIME),
|
||||
proton_sync=_string(value, "proton_sync", "auto"),
|
||||
proton_path=proton_path,
|
||||
renderer=renderer,
|
||||
@@ -201,6 +203,12 @@ def _settings_from_object(value: dict[str, object]) -> Settings:
|
||||
update_channel=_string(value, "update_channel", "stable"),
|
||||
)
|
||||
_validate(settings)
|
||||
# Move existing installations to the modern WoW64 profile once. Validate the
|
||||
# saved runtime first so migration cannot hide malformed settings. New saves
|
||||
# carry the marker, allowing users to choose another runtime afterwards.
|
||||
# Keep the optional Proton path for PTR and future explicit legacy choices.
|
||||
if not _boolean(value, _GE_PROTON_DEFAULT_MIGRATION_KEY, False):
|
||||
return replace(settings, runtime=DEFAULT_GAME_RUNTIME)
|
||||
return settings
|
||||
|
||||
|
||||
@@ -237,6 +245,7 @@ def _settings_need_migration(value: dict[str, object]) -> bool:
|
||||
value.get("schema") != SETTINGS_SCHEMA
|
||||
or not _SETTINGS_FIELD_NAMES.issubset(value)
|
||||
or value.get(_TITLE_FRAME_DEFAULT_MIGRATION_KEY) is not True
|
||||
or value.get(_GE_PROTON_DEFAULT_MIGRATION_KEY) is not True
|
||||
or bool(_RETIRED_SETTINGS.intersection(value))
|
||||
or _legacy_renderer_assignment(value.get("proton_path")) is not None
|
||||
)
|
||||
@@ -279,6 +288,7 @@ def _write_settings(
|
||||
payload.update(asdict(settings))
|
||||
payload["schema"] = SETTINGS_SCHEMA
|
||||
payload[_TITLE_FRAME_DEFAULT_MIGRATION_KEY] = True
|
||||
payload[_GE_PROTON_DEFAULT_MIGRATION_KEY] = True
|
||||
temporary = paths.settings_file.with_name(
|
||||
f".{paths.settings_file.name}.{os.getpid()}.{threading.get_ident()}."
|
||||
f"{secrets.token_hex(8)}.tmp"
|
||||
|
||||
@@ -380,12 +380,19 @@ def normalized_error_message(message: str) -> str:
|
||||
|
||||
|
||||
def _resource_usage() -> dict[str, object]:
|
||||
return _environment_resource_usage(
|
||||
"""Report launcher-lifetime CPU time and peak RSS, excluding the game."""
|
||||
|
||||
snapshot = _environment_resource_usage(
|
||||
get_usage=resource.getrusage,
|
||||
usage_target=resource.RUSAGE_SELF,
|
||||
platform_name=sys.platform,
|
||||
is_finite=math.isfinite,
|
||||
)
|
||||
if snapshot:
|
||||
# Keep the existing numeric fields for telemetry consumers, but make
|
||||
# their scope explicit even when attached to a game_exited event.
|
||||
snapshot["resource_usage_scope"] = "launcher_process"
|
||||
return snapshot
|
||||
|
||||
|
||||
def error_fingerprint(
|
||||
|
||||
@@ -224,7 +224,7 @@ def resource_usage(
|
||||
platform_name: str = sys.platform,
|
||||
is_finite: Callable[[float], bool] = math.isfinite,
|
||||
) -> dict[str, object]:
|
||||
"""Read one bounded process-usage snapshot on the Linux launcher host."""
|
||||
"""Read cumulative CPU time and peak resident memory for the selected process."""
|
||||
|
||||
try:
|
||||
usage = get_usage(usage_target)
|
||||
|
||||
@@ -26,8 +26,8 @@ from ..viewmodels.downloader import (
|
||||
|
||||
|
||||
RUNTIME_LABELS: dict[str, str] = {
|
||||
"Automatic (recommended)": "auto",
|
||||
"GE-Proton — Modern WoW64 via UMU": "ge-proton",
|
||||
"GE-Proton — Modern WoW64 (recommended)": "ge-proton",
|
||||
"Automatic": "auto",
|
||||
"Proton 7 — Steam Runtime container": "proton7-container",
|
||||
"Proton 7 — Direct": "proton7",
|
||||
"UMU Launcher": "umu",
|
||||
@@ -431,7 +431,7 @@ def open_settings_dialog(
|
||||
rendering_card.columnconfigure(1, weight=1)
|
||||
rendering_hint = ttk.Label(
|
||||
rendering_card,
|
||||
text="Automatic uses GE-Proton with modern WoW64 for Babel when UMU is available.",
|
||||
text="GE-Proton with modern WoW64 is recommended for Babel. PTR Babel uses Proton 7.",
|
||||
style="SettingsHint.TLabel",
|
||||
wraplength=360,
|
||||
)
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
"application": "PristonTale-EU",
|
||||
"archive": {
|
||||
"layout": "flat",
|
||||
"name": "PristonTale-EU-1.1.0.tar.gz",
|
||||
"sha256": "b0bb10bdbe0d0e01ce04c8cdb2307c45f0cedb7e2c081794b6efe68d6aa4ac5c",
|
||||
"size": 1550967,
|
||||
"url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.1.0/PristonTale-EU-1.1.0.tar.gz?sha256=b0bb10bdbe0d0e01ce04c8cdb2307c45f0cedb7e2c081794b6efe68d6aa4ac5c"
|
||||
"name": "PristonTale-EU-1.1.2.tar.gz",
|
||||
"sha256": "5e2ced0bee8bb82a8731b5e6acfa10abba3b86190a56ff5c58d2c98248f27281",
|
||||
"size": 1553283,
|
||||
"url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.1.2/PristonTale-EU-1.1.2.tar.gz?sha256=5e2ced0bee8bb82a8731b5e6acfa10abba3b86190a56ff5c58d2c98248f27281"
|
||||
},
|
||||
"archive_sha256": "b0bb10bdbe0d0e01ce04c8cdb2307c45f0cedb7e2c081794b6efe68d6aa4ac5c",
|
||||
"archive_size": 1550967,
|
||||
"archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.1.0/PristonTale-EU-1.1.0.tar.gz?sha256=b0bb10bdbe0d0e01ce04c8cdb2307c45f0cedb7e2c081794b6efe68d6aa4ac5c",
|
||||
"archive_sha256": "5e2ced0bee8bb82a8731b5e6acfa10abba3b86190a56ff5c58d2c98248f27281",
|
||||
"archive_size": 1553283,
|
||||
"archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.1.2/PristonTale-EU-1.1.2.tar.gz?sha256=5e2ced0bee8bb82a8731b5e6acfa10abba3b86190a56ff5c58d2c98248f27281",
|
||||
"channel": "stable",
|
||||
"commit": "be7891f06617674864b035645137441321db03b4",
|
||||
"commit": "770bbb8a88ecc0a847649aee1af2e9500951bcd6",
|
||||
"compatibility": {
|
||||
"config_schema": 1,
|
||||
"manifest_schema": 1,
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/__init__.py",
|
||||
"sha256": "d533461854917e3343b9f600b5949421eadedb9a0a550a8adf0444b81023d333",
|
||||
"sha256": "1bb085ba401a36b95964349f6415c075a18f2ecce2b08aeecfb59bab472848aa",
|
||||
"size": 151
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/application/commands/play.py",
|
||||
"sha256": "3328a0dff4335c3e20a89a5984d889bee1137f509f3605a979f4d8aee595efe4",
|
||||
"size": 37609
|
||||
"sha256": "13b252a82bc2b28d22e7d2fefc24e739e20e913c667e14a1cb11198b9d880aae",
|
||||
"size": 38393
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/application/commands/settings.py",
|
||||
@@ -91,8 +91,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/cli.py",
|
||||
"sha256": "ae8f3740fa997d7e00d26bc5fec68f582e70a051cfb00cd88c8a2eaeb547b4b0",
|
||||
"size": 23871
|
||||
"sha256": "e142e0f33a7f1f8f88b8ad431d1b28b24dab3f74d367ed1912e8106ba6aab229",
|
||||
"size": 23816
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/command.py",
|
||||
@@ -131,8 +131,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/diagnostics/native_logs.py",
|
||||
"sha256": "3c07a45e89f69b2607288d7fd98f20e0679ee35599de03a3d4e3838ef93e91ba",
|
||||
"size": 18769
|
||||
"sha256": "1a7044630052311e7f74ddd716b82a60383d4404493970db35f6741019510ecb",
|
||||
"size": 18780
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/doctor.py",
|
||||
@@ -406,8 +406,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/presentation/game_window.py",
|
||||
"sha256": "217ec82c0d35edf9f3e4346096930460c8f4de6c9b97f98512ef19afcf1eb9ba",
|
||||
"size": 71882
|
||||
"sha256": "a605537bb8b3a052369510fdf7709ac5bfde917be651169470f79b59887ea58f",
|
||||
"size": 80275
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/presentation/gamescope.py",
|
||||
@@ -446,8 +446,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/settings.py",
|
||||
"sha256": "8d62b262bb43eff664b9f6a58bf68a88fec0a8e05b6b9e97cbb47509079c2115",
|
||||
"size": 12059
|
||||
"sha256": "7fd8ecdcbc76feb9bef1f9c840e0e5f73f159c1d370d04e2f3455917a7bc3cce",
|
||||
"size": 12727
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/system_identity.py",
|
||||
@@ -456,8 +456,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/telemetry/__init__.py",
|
||||
"sha256": "fb79ca2ececb07eef4a78f3306b817caa300ddf3da9c71973d660349b9ce644f",
|
||||
"size": 12635
|
||||
"sha256": "bef4c247d5bbf2e2eb9c14e89e914d4a433040c400a27303150038077a9aee22",
|
||||
"size": 12941
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/telemetry/dependencies.py",
|
||||
@@ -466,8 +466,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/telemetry/environment.py",
|
||||
"sha256": "bac7a3a023f4530d306edcd366481cfbc867823c57d59734ebb043dcdb74f6eb",
|
||||
"size": 7333
|
||||
"sha256": "f8ccee2d33283e935e474a66aa845141d395299cc14fea0f631304a9d9207e9a",
|
||||
"size": 7341
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/telemetry/errors.py",
|
||||
@@ -656,8 +656,8 @@
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/ui/dialogs/settings_dialog.py",
|
||||
"sha256": "20a8e461799fa2104e5a09a6e4ec2e8c027caa0688726c57c9ed45e872c1971f",
|
||||
"size": 25299
|
||||
"sha256": "36ffa11821ec1ee3c95774071736082828c58c1f37874f00e66441d2ba0f76fa",
|
||||
"size": 25294
|
||||
},
|
||||
{
|
||||
"path": "ptlinux/ui/viewmodels/__init__.py",
|
||||
@@ -706,21 +706,21 @@
|
||||
},
|
||||
{
|
||||
"path": "release.json",
|
||||
"sha256": "5dfcea2e0eb6ee4d79dba982d79e1a4d5efe1ce0579537d2a0cfab990153636a",
|
||||
"sha256": "f95fab56f77e0dff21b6f0129625de84f0dff413671126921b37afe8f629e181",
|
||||
"size": 218
|
||||
}
|
||||
],
|
||||
"minimum_updater_version": "0.9.9",
|
||||
"notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.1.0",
|
||||
"published_at": "2026-09-19T10:49:03Z",
|
||||
"notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.1.2",
|
||||
"published_at": "2026-10-23T19:34:51Z",
|
||||
"release": {
|
||||
"notes_markdown": "# PristonTale EU Launcher 1.1.0\n\n- Improved GE-Proton/UMU compatibility, launch arguments, and renderer selection.\n- Isolated runtime prefixes and strengthened compatibility checks for game updates.\n- Fixed missing-Tk startup recovery and SteamOS dependency warnings.\n- Improved CDN retries, mirror failover, and interrupted download recovery.\n- Fixed Gamescope shutdown handling and optimized window resizing detection.\n- Expanded launch diagnostics and regression coverage.\n\n---\n\nVersion: `1.1.0` \nChannel: `stable` \nTag: `v1.1.0` \nSource commit: `be7891f06617674864b035645137441321db03b4`\n",
|
||||
"release_notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.1.0",
|
||||
"summary": "Improved GE-Proton/UMU compatibility, launch arguments, and renderer selection.",
|
||||
"title": "PristonTale EU Launcher 1.1.0"
|
||||
"notes_markdown": "# PristonTale EU Launcher 1.1.2\n\n- Improved GE-Proton keyboard focus on X11, including delayed startup and login-window changes.\n- Kept focus recovery active for fixed-size and borderless windows while respecting Alt-Tab and minimization.\n- Prevented replacement-window tracking from attaching to another game account.\n- Added GE-Proton comparison advice for Direct3D resource exhaustion while preserving PTR runtime requirements.\n- Clarified that reported launcher memory and CPU usage exclude the game process.\n\nThe reported Zorin/NVIDIA freeze still needs a gameplay retest with GE-Proton and these focus fixes.\n\n---\n\nVersion: `1.1.2` \nChannel: `stable` \nTag: `v1.1.2` \nSource commit: `770bbb8a88ecc0a847649aee1af2e9500951bcd6`\n",
|
||||
"release_notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.1.2",
|
||||
"summary": "Improved GE-Proton keyboard focus on X11, including delayed startup and login-window changes.",
|
||||
"title": "PristonTale EU Launcher 1.1.2"
|
||||
},
|
||||
"schema": 1,
|
||||
"status": "active",
|
||||
"tag": "v1.1.0",
|
||||
"version": "1.1.0"
|
||||
"tag": "v1.1.2",
|
||||
"version": "1.1.2"
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user