1 Commits

Author SHA1 Message Date
531480b195 Fixed issues with X11 focus, improved Direct3D telemetry. 2026-10-24 00:23:50 +02:00
7 changed files with 348 additions and 82 deletions

View File

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

View File

@@ -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")

View File

@@ -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,6 +1082,7 @@ class _X11:
target_size = self.window_size(window)
if borderless:
self._remove_decorations(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:
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,
)
# Borderless clients need the same active-window-only input repair.
return _maintain_game_window_focus(
mapper,
process,
game_window,
**(
{"diagnostics": diagnostic_sink}
if diagnostic_sink is not None
else {}
),
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 {}),
)
return int(process.wait())
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:

View File

@@ -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(

View File

@@ -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)

View File

@@ -2,16 +2,16 @@
"application": "PristonTale-EU",
"archive": {
"layout": "flat",
"name": "PristonTale-EU-1.1.1.tar.gz",
"sha256": "f3825038f83c1348a6eec790e94f5f244fe5ce246d39e2d32f65bb58395542e0",
"size": 1551152,
"url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.1.1/PristonTale-EU-1.1.1.tar.gz?sha256=f3825038f83c1348a6eec790e94f5f244fe5ce246d39e2d32f65bb58395542e0"
"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": "f3825038f83c1348a6eec790e94f5f244fe5ce246d39e2d32f65bb58395542e0",
"archive_size": 1551152,
"archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v1.1.1/PristonTale-EU-1.1.1.tar.gz?sha256=f3825038f83c1348a6eec790e94f5f244fe5ce246d39e2d32f65bb58395542e0",
"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": "d75c345d36b5916ce58fdf5fd9dbdf4e6d006471",
"commit": "770bbb8a88ecc0a847649aee1af2e9500951bcd6",
"compatibility": {
"config_schema": 1,
"manifest_schema": 1,
@@ -36,7 +36,7 @@
},
{
"path": "ptlinux/__init__.py",
"sha256": "1ec358b790abcebfcba6a44510323648e9a11a6b4e1f2b6b666e288c10fe4c96",
"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",
@@ -406,8 +406,8 @@
},
{
"path": "ptlinux/presentation/game_window.py",
"sha256": "217ec82c0d35edf9f3e4346096930460c8f4de6c9b97f98512ef19afcf1eb9ba",
"size": 71882
"sha256": "a605537bb8b3a052369510fdf7709ac5bfde917be651169470f79b59887ea58f",
"size": 80275
},
{
"path": "ptlinux/presentation/gamescope.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",
@@ -706,21 +706,21 @@
},
{
"path": "release.json",
"sha256": "1480e6705168b79b038871832ee1558f1e79e8bdac2fd285c558950cc1fcbae9",
"sha256": "f95fab56f77e0dff21b6f0129625de84f0dff413671126921b37afe8f629e181",
"size": 218
}
],
"minimum_updater_version": "0.9.9",
"notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.1.1",
"published_at": "2026-10-20T21:42:30Z",
"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.1\n\n- Made GE-Proton with modern WoW64 the default runtime for Babel.\n- Automatically migrate existing runtime settings once, while preserving later manual choices and PTR's Proton 7 compatibility.\n- Persist settings migrations during command-line launches and updates.\n- Improved detection of collision warnings in game crash diagnostics.\n\nThe first launch may download GE-Proton and prepare its separate compatibility prefix.\n\n---\n\nVersion: `1.1.1` \nChannel: `stable` \nTag: `v1.1.1` \nSource commit: `d75c345d36b5916ce58fdf5fd9dbdf4e6d006471`\n",
"release_notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v1.1.1",
"summary": "Made GE-Proton with modern WoW64 the default runtime for Babel.",
"title": "PristonTale EU Launcher 1.1.1"
"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.1",
"version": "1.1.1"
"tag": "v1.1.2",
"version": "1.1.2"
}

Binary file not shown.