Added ui changes to modals, pre-loader and some fixes for other ui elements and performance improvements all over.
This commit is contained in:
@@ -20,6 +20,8 @@ OFFICIAL_LOGO_URL = "https://pristontale.eu/public/img/pristontale/logo/logo_sma
|
|||||||
BROADCAST_ENDPOINT = "http://5.135.35.188:90/broadcasts"
|
BROADCAST_ENDPOINT = "http://5.135.35.188:90/broadcasts"
|
||||||
MAX_NEWS_BYTES = 2 * 1024 * 1024
|
MAX_NEWS_BYTES = 2 * 1024 * 1024
|
||||||
MAX_NEWS_IMAGE_BYTES = 12 * 1024 * 1024
|
MAX_NEWS_IMAGE_BYTES = 12 * 1024 * 1024
|
||||||
|
MAX_BROADCAST_AUTHOR_CHARACTERS = 100
|
||||||
|
MAX_BROADCAST_CONTENT_CHARACTERS = 2000
|
||||||
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||||
|
|
||||||
|
|
||||||
@@ -241,26 +243,41 @@ def _parse_latest_broadcast(payload: bytes) -> Broadcast:
|
|||||||
value = json.loads(payload.decode("utf-8-sig"))
|
value = json.loads(payload.decode("utf-8-sig"))
|
||||||
if not isinstance(value, list) or not value or not isinstance(value[0], dict):
|
if not isinstance(value, list) or not value or not isinstance(value[0], dict):
|
||||||
raise NewsError("broadcast response does not contain a latest broadcast")
|
raise NewsError("broadcast response does not contain a latest broadcast")
|
||||||
latest = value[0]
|
return _broadcast_from_mapping(value[0])
|
||||||
author = latest.get("author")
|
|
||||||
content = latest.get("content")
|
|
||||||
|
def _broadcast_from_mapping(latest: dict[object, object]) -> Broadcast:
|
||||||
|
author = _bounded_text(latest.get("author"), MAX_BROADCAST_AUTHOR_CHARACTERS)
|
||||||
|
content = _bounded_text(latest.get("content"), MAX_BROADCAST_CONTENT_CHARACTERS)
|
||||||
url = _broadcast_url(latest.get("url"))
|
url = _broadcast_url(latest.get("url"))
|
||||||
if (
|
if (
|
||||||
not isinstance(author, str)
|
author is None
|
||||||
or not author.strip()
|
or content is None
|
||||||
or not isinstance(content, str)
|
|
||||||
or not content.strip()
|
|
||||||
or url is None
|
or url is None
|
||||||
):
|
):
|
||||||
raise NewsError("latest broadcast is incomplete")
|
raise NewsError("latest broadcast is incomplete")
|
||||||
|
published_at = (
|
||||||
|
_format_timestamp(latest.get("timestamp"))
|
||||||
|
if "timestamp" in latest
|
||||||
|
else _bounded_text(latest.get("published_at"), 64)
|
||||||
|
)
|
||||||
return Broadcast(
|
return Broadcast(
|
||||||
author=author.strip(),
|
author=author,
|
||||||
content=content.strip(),
|
content=content,
|
||||||
published_at=_format_timestamp(latest.get("timestamp")),
|
published_at=published_at,
|
||||||
url=url,
|
url=url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_text(value: object, maximum_characters: int) -> str | None:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
text = value.strip()
|
||||||
|
if not text or len(text) > maximum_characters:
|
||||||
|
return None
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def _format_timestamp(value: object) -> str | None:
|
def _format_timestamp(value: object) -> str | None:
|
||||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||||
return None
|
return None
|
||||||
@@ -288,10 +305,16 @@ def _image_url(value: object) -> str | None:
|
|||||||
def _broadcast_url(value: object) -> str | None:
|
def _broadcast_url(value: object) -> str | None:
|
||||||
if not isinstance(value, str) or not value.strip():
|
if not isinstance(value, str) or not value.strip():
|
||||||
return None
|
return None
|
||||||
parsed = urlparse(value)
|
url = value.strip()
|
||||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
parsed = urlparse(url)
|
||||||
|
if (
|
||||||
|
parsed.scheme.casefold() != "https"
|
||||||
|
or not parsed.netloc
|
||||||
|
or parsed.username is not None
|
||||||
|
or parsed.password is not None
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
return value.strip()
|
return url
|
||||||
|
|
||||||
|
|
||||||
def _read_cache(path: Path) -> tuple[NewsArticle, ...]:
|
def _read_cache(path: Path) -> tuple[NewsArticle, ...]:
|
||||||
@@ -320,8 +343,8 @@ def _write_cache(path: Path, articles: tuple[NewsArticle, ...]) -> None:
|
|||||||
def _read_broadcast_cache(path: Path) -> Broadcast | None:
|
def _read_broadcast_cache(path: Path) -> Broadcast | None:
|
||||||
try:
|
try:
|
||||||
value = json.loads(path.read_text(encoding="utf-8"))
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
return Broadcast(**value) if isinstance(value, dict) else None
|
return _broadcast_from_mapping(value) if isinstance(value, dict) else None
|
||||||
except (OSError, UnicodeError, TypeError, json.JSONDecodeError):
|
except (OSError, UnicodeError, NewsError, json.JSONDecodeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ def create_window_chrome(
|
|||||||
*,
|
*,
|
||||||
title: str,
|
title: str,
|
||||||
subtitle: str,
|
subtitle: str,
|
||||||
|
window_icon: Any | None = None,
|
||||||
show_title_bar: bool = True,
|
show_title_bar: bool = True,
|
||||||
show_close: bool = True,
|
show_close: bool = True,
|
||||||
show_minimize: bool = False,
|
show_minimize: bool = False,
|
||||||
@@ -347,7 +348,31 @@ def create_window_chrome(
|
|||||||
title_bar.pack_propagate(False)
|
title_bar.pack_propagate(False)
|
||||||
tk_accent = tk.Frame(title_bar, background="#d98927", width=3)
|
tk_accent = tk.Frame(title_bar, background="#d98927", width=3)
|
||||||
tk_accent.pack(side="left", fill="y")
|
tk_accent.pack(side="left", fill="y")
|
||||||
mark = tk.Canvas(
|
title_icon: Any | None = None
|
||||||
|
if window_icon is not None:
|
||||||
|
try:
|
||||||
|
factor = icon_subsample_factor(
|
||||||
|
window_icon.width(),
|
||||||
|
window_icon.height(),
|
||||||
|
20,
|
||||||
|
)
|
||||||
|
title_icon = window_icon.subsample(factor, factor)
|
||||||
|
except (tk.TclError, ValueError):
|
||||||
|
title_icon = None
|
||||||
|
if title_icon is not None:
|
||||||
|
mark = tk.Label(
|
||||||
|
title_bar,
|
||||||
|
image=title_icon,
|
||||||
|
width=22,
|
||||||
|
height=22,
|
||||||
|
background="#17100d",
|
||||||
|
borderwidth=0,
|
||||||
|
highlightthickness=0,
|
||||||
|
cursor="fleur",
|
||||||
|
)
|
||||||
|
mark.image = title_icon
|
||||||
|
else:
|
||||||
|
mark = tk.Frame(
|
||||||
title_bar,
|
title_bar,
|
||||||
width=22,
|
width=22,
|
||||||
height=22,
|
height=22,
|
||||||
@@ -356,8 +381,6 @@ def create_window_chrome(
|
|||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
cursor="fleur",
|
cursor="fleur",
|
||||||
)
|
)
|
||||||
mark.create_oval(3, 3, 19, 19, outline="#d98927", width=1)
|
|
||||||
mark.create_text(11, 11, text="PT", fill="#f5d18b", font=("Georgia", 6, "bold"))
|
|
||||||
mark.pack(side="left", padx=(9, 7))
|
mark.pack(side="left", padx=(9, 7))
|
||||||
identity = tk.Frame(title_bar, background="#17100d", cursor="fleur")
|
identity = tk.Frame(title_bar, background="#17100d", cursor="fleur")
|
||||||
identity.pack(side="left", fill="y")
|
identity.pack(side="left", fill="y")
|
||||||
@@ -440,6 +463,7 @@ def create_dashboard_chrome(
|
|||||||
*,
|
*,
|
||||||
title: str,
|
title: str,
|
||||||
subtitle: str,
|
subtitle: str,
|
||||||
|
window_icon: Any | None = None,
|
||||||
) -> DashboardChrome:
|
) -> DashboardChrome:
|
||||||
"""Install the draggable frame and full controls used by the dashboard."""
|
"""Install the draggable frame and full controls used by the dashboard."""
|
||||||
|
|
||||||
@@ -447,6 +471,7 @@ def create_dashboard_chrome(
|
|||||||
root,
|
root,
|
||||||
title=title,
|
title=title,
|
||||||
subtitle=subtitle,
|
subtitle=subtitle,
|
||||||
|
window_icon=window_icon,
|
||||||
show_close=True,
|
show_close=True,
|
||||||
show_minimize=True,
|
show_minimize=True,
|
||||||
allow_close=True,
|
allow_close=True,
|
||||||
|
|||||||
514
ptlinux/ui.py
514
ptlinux/ui.py
@@ -9,12 +9,13 @@ import json
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from typing import TYPE_CHECKING, Callable, Mapping
|
from typing import TYPE_CHECKING, Callable, Iterator, Mapping
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tkinter import Frame, PhotoImage, Toplevel
|
from tkinter import Frame, PhotoImage, Toplevel
|
||||||
@@ -109,6 +110,10 @@ DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
|
|||||||
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
|
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
|
||||||
DASHBOARD_TRANSITION_STEPS = 9
|
DASHBOARD_TRANSITION_STEPS = 9
|
||||||
DASHBOARD_TRANSITION_MINIMUM_OPACITY = 0.18
|
DASHBOARD_TRANSITION_MINIMUM_OPACITY = 0.18
|
||||||
|
STARTUP_PROGRESS_ANIMATION_INTERVAL_MILLISECONDS = 16
|
||||||
|
STARTUP_PROGRESS_ANIMATION_EASING = 0.28
|
||||||
|
STARTUP_PROGRESS_ANIMATION_MINIMUM_STEP = 0.08
|
||||||
|
PATCH_PRELOAD_INTERVAL_MILLISECONDS = 1
|
||||||
PRELOADER_STATUS_CHARACTERS = 64
|
PRELOADER_STATUS_CHARACTERS = 64
|
||||||
DASHBOARD_DEBUG_KEY = "<Control-Shift-F12>"
|
DASHBOARD_DEBUG_KEY = "<Control-Shift-F12>"
|
||||||
SERVER_STATUS_STALE_SECONDS = 5 * 60
|
SERVER_STATUS_STALE_SECONDS = 5 * 60
|
||||||
@@ -176,6 +181,60 @@ class DashboardError(RuntimeError):
|
|||||||
"""Raised when the native dashboard cannot be displayed."""
|
"""Raised when the native dashboard cannot be displayed."""
|
||||||
|
|
||||||
|
|
||||||
|
class GameLaunchSupervisorRegistry:
|
||||||
|
"""Bridge Tk cancellation requests to game launch worker processes safely."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._processes: dict[int, subprocess.Popen[str]] = {}
|
||||||
|
self._cancel_requested: set[int] = set()
|
||||||
|
|
||||||
|
def attach(self, client_id: int, process: subprocess.Popen[str]) -> bool:
|
||||||
|
"""Register a supervisor and report whether cancellation raced its start."""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._processes[client_id] = process
|
||||||
|
return client_id in self._cancel_requested
|
||||||
|
|
||||||
|
def request_cancel(self, client_id: int) -> subprocess.Popen[str] | None:
|
||||||
|
"""Record cancellation and return an already-started supervisor, if any."""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._cancel_requested.add(client_id)
|
||||||
|
return self._processes.get(client_id)
|
||||||
|
|
||||||
|
def detach(self, client_id: int, process: subprocess.Popen[str]) -> None:
|
||||||
|
with self._lock:
|
||||||
|
if self._processes.get(client_id) is process:
|
||||||
|
self._processes.pop(client_id, None)
|
||||||
|
|
||||||
|
def is_cancel_requested(self, client_id: int) -> bool:
|
||||||
|
with self._lock:
|
||||||
|
return client_id in self._cancel_requested
|
||||||
|
|
||||||
|
def finish(self, client_id: int) -> bool:
|
||||||
|
"""Forget one launch and return whether the user cancelled it."""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._processes.pop(client_id, None)
|
||||||
|
if client_id not in self._cancel_requested:
|
||||||
|
return False
|
||||||
|
self._cancel_requested.remove(client_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def interrupt_game_supervisor(process: subprocess.Popen[str]) -> bool:
|
||||||
|
"""Ask the play CLI to stop its complete Proton/game process group."""
|
||||||
|
|
||||||
|
if process.poll() is not None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
process.send_signal(signal.SIGINT)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class GameClientTracker:
|
class GameClientTracker:
|
||||||
"""Track dashboard-launched clients without retaining process objects in Tk."""
|
"""Track dashboard-launched clients without retaining process objects in Tk."""
|
||||||
|
|
||||||
@@ -527,6 +586,39 @@ def preloader_status_text(
|
|||||||
return f"Step {step} of {total} · {value}"
|
return f"Step {step} of {total} · {value}"
|
||||||
|
|
||||||
|
|
||||||
|
def preloader_logo_subsample_factor(
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
*,
|
||||||
|
max_dimension: int = 88,
|
||||||
|
) -> int:
|
||||||
|
"""Return an integer Tk subsample factor for a compact preloader logo."""
|
||||||
|
|
||||||
|
if width <= 0 or height <= 0 or max_dimension <= 0:
|
||||||
|
raise ValueError("preloader logo dimensions must be positive")
|
||||||
|
return max(
|
||||||
|
1,
|
||||||
|
math.ceil(width / max_dimension),
|
||||||
|
math.ceil(height / max_dimension),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def startup_progress_animation_value(current: float, target: float) -> float:
|
||||||
|
"""Ease one determinate startup-progress frame without overshooting."""
|
||||||
|
|
||||||
|
distance = abs(target - current)
|
||||||
|
if distance == 0:
|
||||||
|
return target
|
||||||
|
step = max(
|
||||||
|
STARTUP_PROGRESS_ANIMATION_MINIMUM_STEP,
|
||||||
|
distance * STARTUP_PROGRESS_ANIMATION_EASING,
|
||||||
|
)
|
||||||
|
if step >= distance:
|
||||||
|
return target
|
||||||
|
direction = 1.0 if target > current else -1.0
|
||||||
|
return current + (direction * step)
|
||||||
|
|
||||||
|
|
||||||
def freeze_preloader_geometry(frame: Frame) -> tuple[int, int]:
|
def freeze_preloader_geometry(frame: Frame) -> tuple[int, int]:
|
||||||
"""Lock a measured preloader frame so later status text cannot reflow it."""
|
"""Lock a measured preloader frame so later status text cannot reflow it."""
|
||||||
|
|
||||||
@@ -701,6 +793,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
root,
|
root,
|
||||||
title="PRISTONTALE EU",
|
title="PRISTONTALE EU",
|
||||||
subtitle="NATIVE LINUX LAUNCHER",
|
subtitle="NATIVE LINUX LAUNCHER",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
dashboard_content = dashboard_chrome.content
|
dashboard_content = dashboard_chrome.content
|
||||||
try:
|
try:
|
||||||
@@ -712,6 +805,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
title="Launcher settings",
|
title="Launcher settings",
|
||||||
message=f"The launcher could not load its settings:\n\n{startup_error}",
|
message=f"The launcher could not load its settings:\n\n{startup_error}",
|
||||||
kind="error",
|
kind="error",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
root.destroy()
|
root.destroy()
|
||||||
return
|
return
|
||||||
@@ -734,25 +828,6 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
foreground="#a8998a",
|
foreground="#a8998a",
|
||||||
font=("Sans", 8),
|
font=("Sans", 8),
|
||||||
)
|
)
|
||||||
style.configure("Splash.TFrame", background="#191311")
|
|
||||||
style.configure(
|
|
||||||
"SplashBrand.TLabel",
|
|
||||||
background="#191311",
|
|
||||||
foreground="#f5d18b",
|
|
||||||
font=("Georgia", 22, "bold"),
|
|
||||||
)
|
|
||||||
style.configure(
|
|
||||||
"SplashTagline.TLabel",
|
|
||||||
background="#191311",
|
|
||||||
foreground="#d8aa61",
|
|
||||||
font=("Sans", 8, "bold"),
|
|
||||||
)
|
|
||||||
style.configure(
|
|
||||||
"SplashStatus.TLabel",
|
|
||||||
background="#191311",
|
|
||||||
foreground="#c9baaa",
|
|
||||||
font=("Sans", 9),
|
|
||||||
)
|
|
||||||
style.configure(
|
style.configure(
|
||||||
"Section.TLabel",
|
"Section.TLabel",
|
||||||
background="#191311",
|
background="#191311",
|
||||||
@@ -1014,6 +1089,29 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
lightcolor="#4a3528",
|
lightcolor="#4a3528",
|
||||||
darkcolor="#32251f",
|
darkcolor="#32251f",
|
||||||
)
|
)
|
||||||
|
style.map(
|
||||||
|
"News.Vertical.TScrollbar",
|
||||||
|
background=[
|
||||||
|
("pressed", "#765034"),
|
||||||
|
("active", "#62452f"),
|
||||||
|
("!disabled", "#4a3528"),
|
||||||
|
],
|
||||||
|
arrowcolor=[
|
||||||
|
("pressed", "#fff0c2"),
|
||||||
|
("active", "#f0c77f"),
|
||||||
|
("!disabled", "#d8aa61"),
|
||||||
|
],
|
||||||
|
lightcolor=[
|
||||||
|
("pressed", "#765034"),
|
||||||
|
("active", "#62452f"),
|
||||||
|
("!disabled", "#4a3528"),
|
||||||
|
],
|
||||||
|
darkcolor=[
|
||||||
|
("pressed", "#4b3021"),
|
||||||
|
("active", "#3d2a21"),
|
||||||
|
("!disabled", "#32251f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"Play.TButton",
|
"Play.TButton",
|
||||||
background="#d98927",
|
background="#d98927",
|
||||||
@@ -1114,6 +1212,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
|
|
||||||
startup_started_at = time.monotonic()
|
startup_started_at = time.monotonic()
|
||||||
startup_status = tk.StringVar(value="Loading dashboard contents…")
|
startup_status = tk.StringVar(value="Loading dashboard contents…")
|
||||||
|
startup_image_references: list[tk.PhotoImage] = []
|
||||||
startup_overlay = tk.Frame(
|
startup_overlay = tk.Frame(
|
||||||
dashboard_content,
|
dashboard_content,
|
||||||
background="#0c0908",
|
background="#0c0908",
|
||||||
@@ -1123,62 +1222,117 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
startup_overlay.place(x=0, y=0, relwidth=1, relheight=1)
|
startup_overlay.place(x=0, y=0, relwidth=1, relheight=1)
|
||||||
startup_border = tk.Frame(
|
startup_border = tk.Frame(
|
||||||
startup_overlay,
|
startup_overlay,
|
||||||
background="#d98927",
|
background="#6a482d",
|
||||||
borderwidth=0,
|
borderwidth=0,
|
||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
)
|
)
|
||||||
startup_border.place(relx=0.5, rely=0.5, anchor="center")
|
startup_border.place(relx=0.5, rely=0.5, anchor="center")
|
||||||
startup_shell = ttk.Frame(
|
startup_shell = ttk.Frame(
|
||||||
startup_border,
|
startup_border,
|
||||||
style="Splash.TFrame",
|
style="Root.TFrame",
|
||||||
padding=(26, 22),
|
padding=(34, 26),
|
||||||
)
|
)
|
||||||
startup_shell.pack(fill="both", expand=True, padx=1, pady=1)
|
startup_shell.pack(fill="both", expand=True, padx=1, pady=1)
|
||||||
|
startup_logo_source = window_icon
|
||||||
|
if startup_logo_source is not None:
|
||||||
|
startup_logo_factor = preloader_logo_subsample_factor(
|
||||||
|
startup_logo_source.width(),
|
||||||
|
startup_logo_source.height(),
|
||||||
|
)
|
||||||
|
loaded_startup_logo = startup_logo_source.subsample(
|
||||||
|
startup_logo_factor,
|
||||||
|
startup_logo_factor,
|
||||||
|
)
|
||||||
|
startup_image_references.append(loaded_startup_logo)
|
||||||
|
tk.Label(
|
||||||
|
startup_shell,
|
||||||
|
image=loaded_startup_logo,
|
||||||
|
background="#0c0908",
|
||||||
|
borderwidth=0,
|
||||||
|
highlightthickness=0,
|
||||||
|
).pack(anchor="center", pady=(0, 16))
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
startup_shell,
|
startup_shell,
|
||||||
text="PRISTONTALE EU",
|
text="Preparing the launcher…",
|
||||||
style="SplashBrand.TLabel",
|
style="PreloaderStatus.TLabel",
|
||||||
).pack(anchor="w")
|
anchor="center",
|
||||||
ttk.Label(
|
).pack(fill="x")
|
||||||
startup_shell,
|
|
||||||
text="NATIVE LINUX LAUNCHER",
|
|
||||||
style="SplashTagline.TLabel",
|
|
||||||
).pack(anchor="w", pady=(2, 17))
|
|
||||||
startup_status_label = ttk.Label(
|
startup_status_label = ttk.Label(
|
||||||
startup_shell,
|
startup_shell,
|
||||||
textvariable=startup_status,
|
textvariable=startup_status,
|
||||||
style="SplashStatus.TLabel",
|
style="PreloaderMuted.TLabel",
|
||||||
width=PRELOADER_STATUS_CHARACTERS,
|
width=PRELOADER_STATUS_CHARACTERS,
|
||||||
anchor="w",
|
anchor="center",
|
||||||
)
|
)
|
||||||
startup_status_label.pack(fill="x", pady=(0, 8))
|
startup_status_label.pack(fill="x", pady=(5, 11))
|
||||||
|
startup_progress_value = tk.DoubleVar(value=0.0)
|
||||||
startup_progress = ttk.Progressbar(
|
startup_progress = ttk.Progressbar(
|
||||||
startup_shell,
|
startup_shell,
|
||||||
style="Depot.Horizontal.TProgressbar",
|
style="Depot.Horizontal.TProgressbar",
|
||||||
mode="indeterminate",
|
orient="horizontal",
|
||||||
|
mode="determinate",
|
||||||
|
maximum=len(DASHBOARD_STARTUP_COMPONENTS),
|
||||||
|
variable=startup_progress_value,
|
||||||
length=420,
|
length=420,
|
||||||
|
takefocus=False,
|
||||||
)
|
)
|
||||||
startup_progress.pack(fill="x")
|
startup_progress.pack(fill="x")
|
||||||
freeze_preloader_geometry(startup_border)
|
freeze_preloader_geometry(startup_border)
|
||||||
startup_progress.start(12)
|
|
||||||
startup_ready_components: set[str] = set()
|
startup_ready_components: set[str] = set()
|
||||||
startup_transition_started = False
|
startup_transition_started = False
|
||||||
startup_ready_after: str | None = None
|
startup_ready_after: str | None = None
|
||||||
startup_timeout_after: str | None = None
|
startup_timeout_after: str | None = None
|
||||||
|
startup_progress_after: str | None = None
|
||||||
|
startup_progress_target = 0.0
|
||||||
root.deiconify()
|
root.deiconify()
|
||||||
root.lift()
|
root.lift()
|
||||||
startup_overlay.lift()
|
startup_overlay.lift()
|
||||||
root.update()
|
root.update()
|
||||||
|
|
||||||
|
def cancel_startup_progress_animation() -> None:
|
||||||
|
nonlocal startup_progress_after
|
||||||
|
if startup_progress_after is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
root.after_cancel(startup_progress_after)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
startup_progress_after = None
|
||||||
|
|
||||||
|
def animate_startup_progress() -> None:
|
||||||
|
nonlocal startup_progress_after
|
||||||
|
startup_progress_after = None
|
||||||
|
if startup_transition_started or not startup_overlay.winfo_exists():
|
||||||
|
return
|
||||||
|
current = float(startup_progress_value.get())
|
||||||
|
value = startup_progress_animation_value(current, startup_progress_target)
|
||||||
|
startup_progress_value.set(value)
|
||||||
|
if value != startup_progress_target:
|
||||||
|
startup_progress_after = root.after(
|
||||||
|
STARTUP_PROGRESS_ANIMATION_INTERVAL_MILLISECONDS,
|
||||||
|
animate_startup_progress,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_startup_progress_target(value: float, *, immediate: bool = False) -> None:
|
||||||
|
nonlocal startup_progress_after, startup_progress_target
|
||||||
|
startup_progress_target = min(
|
||||||
|
max(0.0, value),
|
||||||
|
float(len(DASHBOARD_STARTUP_COMPONENTS)),
|
||||||
|
)
|
||||||
|
if immediate:
|
||||||
|
cancel_startup_progress_animation()
|
||||||
|
startup_progress_value.set(startup_progress_target)
|
||||||
|
return
|
||||||
|
if startup_progress_after is None:
|
||||||
|
startup_progress_after = root.after(
|
||||||
|
STARTUP_PROGRESS_ANIMATION_INTERVAL_MILLISECONDS,
|
||||||
|
animate_startup_progress,
|
||||||
|
)
|
||||||
|
|
||||||
def update_startup_progress(message: str) -> None:
|
def update_startup_progress(message: str) -> None:
|
||||||
if startup_transition_started:
|
if startup_transition_started:
|
||||||
return
|
return
|
||||||
startup_progress.stop()
|
set_startup_progress_target(float(len(startup_ready_components)))
|
||||||
startup_progress.configure(
|
|
||||||
mode="determinate",
|
|
||||||
maximum=len(DASHBOARD_STARTUP_COMPONENTS),
|
|
||||||
value=len(startup_ready_components),
|
|
||||||
)
|
|
||||||
startup_status.set(
|
startup_status.set(
|
||||||
f"{message} · {len(startup_ready_components)}/"
|
f"{message} · {len(startup_ready_components)}/"
|
||||||
f"{len(DASHBOARD_STARTUP_COMPONENTS)}"
|
f"{len(DASHBOARD_STARTUP_COMPONENTS)}"
|
||||||
@@ -1239,6 +1393,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
def swap_loader_for_dashboard(*, fade_supported: bool) -> None:
|
def swap_loader_for_dashboard(*, fade_supported: bool) -> None:
|
||||||
if startup_overlay.winfo_exists():
|
if startup_overlay.winfo_exists():
|
||||||
startup_overlay.destroy()
|
startup_overlay.destroy()
|
||||||
|
startup_image_references.clear()
|
||||||
root.update_idletasks()
|
root.update_idletasks()
|
||||||
if fade_supported:
|
if fade_supported:
|
||||||
fade_dashboard_in()
|
fade_dashboard_in()
|
||||||
@@ -1273,11 +1428,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
version.set(
|
version.set(
|
||||||
f"Launcher {__version__} · game build check continuing in background"
|
f"Launcher {__version__} · game build check continuing in background"
|
||||||
)
|
)
|
||||||
startup_progress.stop()
|
set_startup_progress_target(
|
||||||
startup_progress.configure(
|
float(len(DASHBOARD_STARTUP_COMPONENTS)),
|
||||||
mode="determinate",
|
immediate=True,
|
||||||
maximum=len(DASHBOARD_STARTUP_COMPONENTS),
|
|
||||||
value=len(DASHBOARD_STARTUP_COMPONENTS),
|
|
||||||
)
|
)
|
||||||
startup_status.set(
|
startup_status.set(
|
||||||
"Opening dashboard with available content…"
|
"Opening dashboard with available content…"
|
||||||
@@ -1326,14 +1479,23 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
borderwidth=0,
|
borderwidth=0,
|
||||||
)
|
)
|
||||||
logo.create_oval(4, 4, 38, 38, outline="#d98927", width=2)
|
dashboard_header_icon: PhotoImage | None = None
|
||||||
logo.create_text(
|
if window_icon is not None:
|
||||||
21,
|
try:
|
||||||
21,
|
header_icon_factor = preloader_logo_subsample_factor(
|
||||||
text="PT",
|
window_icon.width(),
|
||||||
fill="#f5d18b",
|
window_icon.height(),
|
||||||
font=("Georgia", 9, "bold"),
|
max_dimension=40,
|
||||||
)
|
)
|
||||||
|
dashboard_header_icon = window_icon.subsample(
|
||||||
|
header_icon_factor,
|
||||||
|
header_icon_factor,
|
||||||
|
)
|
||||||
|
except tk.TclError:
|
||||||
|
dashboard_header_icon = None
|
||||||
|
if dashboard_header_icon is not None:
|
||||||
|
logo.create_image(21, 21, image=dashboard_header_icon)
|
||||||
|
logo.image = dashboard_header_icon
|
||||||
logo.pack(side="left", padx=(0, 9))
|
logo.pack(side="left", padx=(0, 9))
|
||||||
brand = ttk.Frame(header, style="Root.TFrame")
|
brand = ttk.Frame(header, style="Root.TFrame")
|
||||||
brand.pack(side="left", fill="x", expand=True)
|
brand.pack(side="left", fill="x", expand=True)
|
||||||
@@ -1598,6 +1760,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
active_news_tab = "news"
|
active_news_tab = "news"
|
||||||
rendered_news_articles: tuple[NewsArticle, ...] | None = None
|
rendered_news_articles: tuple[NewsArticle, ...] | None = None
|
||||||
rendered_patch_notes: tuple[PatchNote, ...] | None = None
|
rendered_patch_notes: tuple[PatchNote, ...] | None = None
|
||||||
|
patch_render_generation = 0
|
||||||
|
patch_render_target: tuple[PatchNote, ...] | None = None
|
||||||
|
patch_render_callbacks: list[Callable[[], None]] = []
|
||||||
logo_image_references: list[tk.PhotoImage] = []
|
logo_image_references: list[tk.PhotoImage] = []
|
||||||
preloader: tk.Toplevel | None = None
|
preloader: tk.Toplevel | None = None
|
||||||
preloader_status: tk.StringVar | None = None
|
preloader_status: tk.StringVar | None = None
|
||||||
@@ -1607,6 +1772,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
dashboard_debug_window: tk.Toplevel | None = None
|
dashboard_debug_window: tk.Toplevel | None = None
|
||||||
dashboard_debug_output: tk.Text | None = None
|
dashboard_debug_output: tk.Text | None = None
|
||||||
game_clients = GameClientTracker()
|
game_clients = GameClientTracker()
|
||||||
|
game_launch_supervisors = GameLaunchSupervisorRegistry()
|
||||||
client_runtime_debug: dict[int, RuntimeDebugInfo] = {}
|
client_runtime_debug: dict[int, RuntimeDebugInfo] = {}
|
||||||
client_launch_settings: dict[int, Settings] = {}
|
client_launch_settings: dict[int, Settings] = {}
|
||||||
active_game_launch_id: int | None = None
|
active_game_launch_id: int | None = None
|
||||||
@@ -1774,6 +1940,25 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
progress_text.set(activity_text())
|
progress_text.set(activity_text())
|
||||||
set_preloader_status(operation_message, operation_stage, operation_stage_total)
|
set_preloader_status(operation_message, operation_stage, operation_stage_total)
|
||||||
|
|
||||||
|
def cancel_active_game_launch() -> None:
|
||||||
|
client_id = active_game_launch_id
|
||||||
|
if client_id is None:
|
||||||
|
close_game_preloader()
|
||||||
|
return
|
||||||
|
process = game_launch_supervisors.request_cancel(client_id)
|
||||||
|
interrupted = process is not None and interrupt_game_supervisor(process)
|
||||||
|
if interrupted:
|
||||||
|
append_log(f"Cancelling game client {client_id}; stopping its process group.")
|
||||||
|
elif process is None:
|
||||||
|
append_log(f"Cancelling game client {client_id} before its supervisor starts.")
|
||||||
|
else:
|
||||||
|
append_log(f"Game client {client_id} was already stopping.")
|
||||||
|
close_game_preloader()
|
||||||
|
root.deiconify()
|
||||||
|
root.lift()
|
||||||
|
status.set(f"Cancelling game client {client_id}…")
|
||||||
|
progress_text.set("Stopping the initiated game and compatibility processes…")
|
||||||
|
|
||||||
def show_game_preloader() -> None:
|
def show_game_preloader() -> None:
|
||||||
nonlocal preloader, preloader_status, preloader_progress
|
nonlocal preloader, preloader_status, preloader_progress
|
||||||
if preloader is not None and preloader.winfo_exists():
|
if preloader is not None and preloader.winfo_exists():
|
||||||
@@ -1784,7 +1969,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
window.title("Launching PristonTale EU")
|
window.title("Launching PristonTale EU")
|
||||||
window.configure(background="#0c0908")
|
window.configure(background="#0c0908")
|
||||||
window.resizable(False, False)
|
window.resizable(False, False)
|
||||||
window.protocol("WM_DELETE_WINDOW", lambda: None)
|
window.protocol("WM_DELETE_WINDOW", cancel_active_game_launch)
|
||||||
window.transient(root)
|
window.transient(root)
|
||||||
apply_window_icon(window)
|
apply_window_icon(window)
|
||||||
loader_chrome = create_window_chrome(
|
loader_chrome = create_window_chrome(
|
||||||
@@ -1794,8 +1979,26 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
show_title_bar=False,
|
show_title_bar=False,
|
||||||
show_close=False,
|
show_close=False,
|
||||||
show_minimize=False,
|
show_minimize=False,
|
||||||
allow_close=False,
|
allow_close=True,
|
||||||
)
|
)
|
||||||
|
loader_chrome.set_close_command(cancel_active_game_launch)
|
||||||
|
cancel_button = tk.Button(
|
||||||
|
loader_chrome.content,
|
||||||
|
text="×",
|
||||||
|
command=cancel_active_game_launch,
|
||||||
|
background="#0c0908",
|
||||||
|
foreground="#bda998",
|
||||||
|
activebackground="#a63f2f",
|
||||||
|
activeforeground="#ffffff",
|
||||||
|
font=("Sans", 12),
|
||||||
|
width=3,
|
||||||
|
borderwidth=0,
|
||||||
|
highlightthickness=0,
|
||||||
|
relief="flat",
|
||||||
|
cursor="hand2",
|
||||||
|
takefocus=True,
|
||||||
|
)
|
||||||
|
cancel_button.place(relx=1.0, x=-9, y=9, anchor="ne")
|
||||||
status_variable = tk.StringVar(value="Starting launcher process…")
|
status_variable = tk.StringVar(value="Starting launcher process…")
|
||||||
preloader_status = status_variable
|
preloader_status = status_variable
|
||||||
content = ttk.Frame(
|
content = ttk.Frame(
|
||||||
@@ -1804,14 +2007,17 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
padding=(34, 26),
|
padding=(34, 26),
|
||||||
)
|
)
|
||||||
content.pack(fill="both", expand=True)
|
content.pack(fill="both", expand=True)
|
||||||
|
cancel_button.lift()
|
||||||
logo_source = window_icon
|
logo_source = window_icon
|
||||||
if logo_source is not None:
|
if logo_source is not None:
|
||||||
factor = max(
|
modal_logo_factor = preloader_logo_subsample_factor(
|
||||||
1,
|
logo_source.width(),
|
||||||
math.ceil(logo_source.width() / 88),
|
logo_source.height(),
|
||||||
math.ceil(logo_source.height() / 88),
|
)
|
||||||
|
modal_logo = logo_source.subsample(
|
||||||
|
modal_logo_factor,
|
||||||
|
modal_logo_factor,
|
||||||
)
|
)
|
||||||
modal_logo = logo_source.subsample(factor, factor)
|
|
||||||
preloader_image_references.clear()
|
preloader_image_references.clear()
|
||||||
preloader_image_references.append(modal_logo)
|
preloader_image_references.append(modal_logo)
|
||||||
tk.Label(
|
tk.Label(
|
||||||
@@ -1936,6 +2142,13 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
else:
|
else:
|
||||||
events.put(("output", f"[Client {game_client_id}] {line}"))
|
events.put(("output", f"[Client {game_client_id}] {line}"))
|
||||||
|
|
||||||
|
if (
|
||||||
|
game_client_id is not None
|
||||||
|
and game_launch_supervisors.is_cancel_requested(game_client_id)
|
||||||
|
):
|
||||||
|
publish_output("Launch cancelled before the game supervisor started.\n")
|
||||||
|
events.put(("game_done", (game_client_id, 130)))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
@@ -1954,9 +2167,16 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
events.put(("game_done", (game_client_id, 1)))
|
events.put(("game_done", (game_client_id, 1)))
|
||||||
return
|
return
|
||||||
if game_client_id is not None:
|
if game_client_id is not None:
|
||||||
|
cancel_on_start = game_launch_supervisors.attach(
|
||||||
|
game_client_id,
|
||||||
|
process,
|
||||||
|
)
|
||||||
events.put(
|
events.put(
|
||||||
("game_supervisor_started", (game_client_id, process.pid))
|
("game_supervisor_started", (game_client_id, process.pid))
|
||||||
)
|
)
|
||||||
|
if cancel_on_start:
|
||||||
|
publish_output("Stopping the game supervisor after launch cancellation.\n")
|
||||||
|
interrupt_game_supervisor(process)
|
||||||
if process.stdout is not None:
|
if process.stdout is not None:
|
||||||
for line in process.stdout:
|
for line in process.stdout:
|
||||||
if line.startswith("STATUS "):
|
if line.startswith("STATUS "):
|
||||||
@@ -2009,6 +2229,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
):
|
):
|
||||||
events.put(("game_window_ready", game_client_id))
|
events.put(("game_window_ready", game_client_id))
|
||||||
returncode = process.wait()
|
returncode = process.wait()
|
||||||
|
if game_client_id is not None:
|
||||||
|
game_launch_supervisors.detach(game_client_id, process)
|
||||||
if game_client_id is None:
|
if game_client_id is None:
|
||||||
events.put(
|
events.put(
|
||||||
("launcher_update_done" if restart_on_success else "done", returncode)
|
("launcher_update_done" if restart_on_success else "done", returncode)
|
||||||
@@ -2211,25 +2433,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
def show_news(articles: tuple[NewsArticle, ...]) -> None:
|
def show_news(articles: tuple[NewsArticle, ...]) -> None:
|
||||||
render_news(articles)
|
render_news(articles)
|
||||||
|
|
||||||
def render_patch_notes(patches: tuple[PatchNote, ...]) -> None:
|
def append_patch_note(patch: PatchNote) -> Iterator[None]:
|
||||||
nonlocal rendered_patch_notes
|
|
||||||
visible_patches = patches[:MAX_PATCH_NOTES]
|
|
||||||
if rendered_patch_notes == visible_patches:
|
|
||||||
return
|
|
||||||
for child in patches_content.winfo_children():
|
|
||||||
child.destroy()
|
|
||||||
if not visible_patches:
|
|
||||||
ttk.Label(
|
|
||||||
patches_content,
|
|
||||||
text=(
|
|
||||||
"Official patch notes are temporarily unavailable. "
|
|
||||||
"Your cached game can still play."
|
|
||||||
),
|
|
||||||
style="Muted.TLabel",
|
|
||||||
).pack(anchor="w", padx=8, pady=8)
|
|
||||||
rendered_patch_notes = visible_patches
|
|
||||||
return
|
|
||||||
for patch in visible_patches:
|
|
||||||
card = tk.Frame(
|
card = tk.Frame(
|
||||||
patches_content,
|
patches_content,
|
||||||
background="#211916",
|
background="#211916",
|
||||||
@@ -2296,6 +2500,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
foreground="#ded1c4",
|
foreground="#ded1c4",
|
||||||
font=("Sans", 9),
|
font=("Sans", 9),
|
||||||
).pack(anchor="w", pady=(8, 0))
|
).pack(anchor="w", pady=(8, 0))
|
||||||
|
yield
|
||||||
for section in patch.sections:
|
for section in patch.sections:
|
||||||
tk.Label(
|
tk.Label(
|
||||||
card,
|
card,
|
||||||
@@ -2304,6 +2509,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
foreground="#d8aa61",
|
foreground="#d8aa61",
|
||||||
font=("Sans", 9, "bold"),
|
font=("Sans", 9, "bold"),
|
||||||
).pack(anchor="w", pady=(12, 5))
|
).pack(anchor="w", pady=(12, 5))
|
||||||
|
yield
|
||||||
for change in section.items:
|
for change in section.items:
|
||||||
change_row = tk.Frame(card, background="#211916")
|
change_row = tk.Frame(card, background="#211916")
|
||||||
change_row.pack(anchor="w", fill="x", pady=3)
|
change_row.pack(anchor="w", fill="x", pady=3)
|
||||||
@@ -2331,11 +2537,99 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
justify="left",
|
justify="left",
|
||||||
anchor="w",
|
anchor="w",
|
||||||
).pack(side="left", anchor="w", fill="x", expand=True)
|
).pack(side="left", anchor="w", fill="x", expand=True)
|
||||||
|
yield
|
||||||
make_news_card_clickable(card, patch.url, border_only=True)
|
make_news_card_clickable(card, patch.url, border_only=True)
|
||||||
rendered_patch_notes = visible_patches
|
|
||||||
|
|
||||||
def show_patch_notes(patches: tuple[PatchNote, ...]) -> None:
|
def render_patch_notes(
|
||||||
render_patch_notes(patches)
|
patches: tuple[PatchNote, ...],
|
||||||
|
*,
|
||||||
|
on_complete: Callable[[], None] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Preload patch cards incrementally without blocking Tk's event loop."""
|
||||||
|
|
||||||
|
nonlocal patch_render_generation, patch_render_target, rendered_patch_notes
|
||||||
|
visible_patches = patches[:MAX_PATCH_NOTES]
|
||||||
|
if rendered_patch_notes == visible_patches:
|
||||||
|
if on_complete is not None:
|
||||||
|
on_complete()
|
||||||
|
return
|
||||||
|
if patch_render_target == visible_patches:
|
||||||
|
if on_complete is not None:
|
||||||
|
patch_render_callbacks.append(on_complete)
|
||||||
|
return
|
||||||
|
|
||||||
|
patch_render_generation += 1
|
||||||
|
generation = patch_render_generation
|
||||||
|
patch_render_target = visible_patches
|
||||||
|
if on_complete is not None:
|
||||||
|
patch_render_callbacks.append(on_complete)
|
||||||
|
rendered_patch_notes = None
|
||||||
|
for child in patches_content.winfo_children():
|
||||||
|
child.destroy()
|
||||||
|
patches_content.grid()
|
||||||
|
if active_news_tab == "patches":
|
||||||
|
patches_content.tkraise()
|
||||||
|
else:
|
||||||
|
news_content.tkraise()
|
||||||
|
|
||||||
|
def finish_render() -> None:
|
||||||
|
nonlocal patch_render_target, rendered_patch_notes
|
||||||
|
if generation != patch_render_generation:
|
||||||
|
return
|
||||||
|
rendered_patch_notes = visible_patches
|
||||||
|
patch_render_target = None
|
||||||
|
if active_news_tab == "patches":
|
||||||
|
patches_content.grid()
|
||||||
|
patches_content.tkraise()
|
||||||
|
else:
|
||||||
|
patches_content.grid_remove()
|
||||||
|
news_content.tkraise()
|
||||||
|
news_canvas.configure(scrollregion=news_canvas.bbox("all"))
|
||||||
|
callbacks = tuple(patch_render_callbacks)
|
||||||
|
patch_render_callbacks.clear()
|
||||||
|
for callback in callbacks:
|
||||||
|
callback()
|
||||||
|
|
||||||
|
if not visible_patches:
|
||||||
|
ttk.Label(
|
||||||
|
patches_content,
|
||||||
|
text=(
|
||||||
|
"Official patch notes are temporarily unavailable. "
|
||||||
|
"Your cached game can still play."
|
||||||
|
),
|
||||||
|
style="Muted.TLabel",
|
||||||
|
).pack(anchor="w", padx=8, pady=8)
|
||||||
|
root.after(PATCH_PRELOAD_INTERVAL_MILLISECONDS, finish_render)
|
||||||
|
return
|
||||||
|
|
||||||
|
def patch_render_steps() -> Iterator[None]:
|
||||||
|
for patch in visible_patches:
|
||||||
|
yield from append_patch_note(patch)
|
||||||
|
|
||||||
|
render_steps = patch_render_steps()
|
||||||
|
|
||||||
|
def append_next() -> None:
|
||||||
|
if generation != patch_render_generation:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
next(render_steps)
|
||||||
|
except StopIteration:
|
||||||
|
finish_render()
|
||||||
|
return
|
||||||
|
patches_content.update_idletasks()
|
||||||
|
root.after(
|
||||||
|
PATCH_PRELOAD_INTERVAL_MILLISECONDS,
|
||||||
|
append_next,
|
||||||
|
)
|
||||||
|
|
||||||
|
root.after(PATCH_PRELOAD_INTERVAL_MILLISECONDS, append_next)
|
||||||
|
|
||||||
|
def show_patch_notes(
|
||||||
|
patches: tuple[PatchNote, ...],
|
||||||
|
*,
|
||||||
|
on_complete: Callable[[], None] | None = None,
|
||||||
|
) -> None:
|
||||||
|
render_patch_notes(patches, on_complete=on_complete)
|
||||||
|
|
||||||
def switch_news_tab(tab: str) -> None:
|
def switch_news_tab(tab: str) -> None:
|
||||||
nonlocal active_news_tab
|
nonlocal active_news_tab
|
||||||
@@ -2910,11 +3204,13 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
def refresh_primary_action(
|
def refresh_primary_action(
|
||||||
snapshot: DepotSnapshot | None = None,
|
snapshot: DepotSnapshot | None = None,
|
||||||
*,
|
*,
|
||||||
|
installation: GameInstallationStatus | None = None,
|
||||||
update_activity: bool = True,
|
update_activity: bool = True,
|
||||||
) -> GameInstallationStatus:
|
) -> GameInstallationStatus:
|
||||||
nonlocal official_snapshot, latest_installation_status
|
nonlocal official_snapshot, latest_installation_status
|
||||||
if snapshot is not None:
|
if snapshot is not None:
|
||||||
official_snapshot = snapshot
|
official_snapshot = snapshot
|
||||||
|
if installation is None:
|
||||||
installation = inspect_game_installation(
|
installation = inspect_game_installation(
|
||||||
paths,
|
paths,
|
||||||
official_snapshot.build if official_snapshot is not None else None,
|
official_snapshot.build if official_snapshot is not None else None,
|
||||||
@@ -2979,8 +3275,15 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
compatibility_check_generation += 1
|
compatibility_check_generation += 1
|
||||||
check_generation = compatibility_check_generation
|
check_generation = compatibility_check_generation
|
||||||
runtime_compatibility = None
|
runtime_compatibility = None
|
||||||
local_status = inspect_game_installation(paths)
|
local_status = inspect_game_installation(
|
||||||
refresh_primary_action(update_activity=update_activity)
|
paths,
|
||||||
|
official_snapshot.build if official_snapshot is not None else None,
|
||||||
|
compatibility_ready=None,
|
||||||
|
)
|
||||||
|
refresh_primary_action(
|
||||||
|
installation=local_status,
|
||||||
|
update_activity=update_activity,
|
||||||
|
)
|
||||||
if not local_status.installed or game_import_requires_verification(paths):
|
if not local_status.installed or game_import_requires_verification(paths):
|
||||||
mark_startup_component_ready(
|
mark_startup_component_ready(
|
||||||
"compatibility",
|
"compatibility",
|
||||||
@@ -3091,6 +3394,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
*,
|
*,
|
||||||
ready: bool,
|
ready: bool,
|
||||||
returncode: int | None = None,
|
returncode: int | None = None,
|
||||||
|
cancelled: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
nonlocal active_game_launch_id
|
nonlocal active_game_launch_id
|
||||||
nonlocal operation_active, game_window_active
|
nonlocal operation_active, game_window_active
|
||||||
@@ -3113,6 +3417,10 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
if ready:
|
if ready:
|
||||||
status.set("Ready for another account.")
|
status.set("Ready for another account.")
|
||||||
progress_text.set("Click Play to open another game window.")
|
progress_text.set("Click Play to open another game window.")
|
||||||
|
elif cancelled:
|
||||||
|
root.lift()
|
||||||
|
status.set("Game launch cancelled.")
|
||||||
|
progress_text.set("The initiated game and compatibility processes were stopped.")
|
||||||
else:
|
else:
|
||||||
root.lift()
|
root.lift()
|
||||||
status.set(
|
status.set(
|
||||||
@@ -3181,6 +3489,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
f"restart: {restart_error}"
|
f"restart: {restart_error}"
|
||||||
),
|
),
|
||||||
kind="error",
|
kind="error",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
root.destroy()
|
root.destroy()
|
||||||
@@ -3206,13 +3515,18 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
and isinstance(value[0], int)
|
and isinstance(value[0], int)
|
||||||
and isinstance(value[1], Mapping)
|
and isinstance(value[1], Mapping)
|
||||||
and value[0] == active_game_launch_id
|
and value[0] == active_game_launch_id
|
||||||
|
and not game_launch_supervisors.is_cancel_requested(value[0])
|
||||||
):
|
):
|
||||||
step = _as_int(value[1].get("step"))
|
step = _as_int(value[1].get("step"))
|
||||||
total = _as_int(value[1].get("total"))
|
total = _as_int(value[1].get("total"))
|
||||||
message = value[1].get("message")
|
message = value[1].get("message")
|
||||||
if step > 0 and total > 0 and isinstance(message, str) and message:
|
if step > 0 and total > 0 and isinstance(message, str) and message:
|
||||||
set_operation_stage(step, total, message)
|
set_operation_stage(step, total, message)
|
||||||
elif kind == "game_started" and isinstance(value, int):
|
elif (
|
||||||
|
kind == "game_started"
|
||||||
|
and isinstance(value, int)
|
||||||
|
and not game_launch_supervisors.is_cancel_requested(value)
|
||||||
|
):
|
||||||
launch_settings = client_launch_settings.get(value)
|
launch_settings = client_launch_settings.get(value)
|
||||||
if launch_settings is not None:
|
if launch_settings is not None:
|
||||||
client_runtime_debug[value] = inspect_runtime_debug(
|
client_runtime_debug[value] = inspect_runtime_debug(
|
||||||
@@ -3221,7 +3535,11 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
)
|
)
|
||||||
progress_bar.stop()
|
progress_bar.stop()
|
||||||
status.set(f"Game client {value} started; preparing its window…")
|
status.set(f"Game client {value} started; preparing its window…")
|
||||||
elif kind == "game_window_ready" and isinstance(value, int):
|
elif (
|
||||||
|
kind == "game_window_ready"
|
||||||
|
and isinstance(value, int)
|
||||||
|
and not game_launch_supervisors.is_cancel_requested(value)
|
||||||
|
):
|
||||||
if game_clients.mark_running(value):
|
if game_clients.mark_running(value):
|
||||||
game_window_active = True
|
game_window_active = True
|
||||||
release_game_launch_controls(value, ready=True)
|
release_game_launch_controls(value, ready=True)
|
||||||
@@ -3233,6 +3551,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
and isinstance(value[1], int)
|
and isinstance(value[1], int)
|
||||||
):
|
):
|
||||||
client_id, returncode = value
|
client_id, returncode = value
|
||||||
|
cancelled = game_launch_supervisors.finish(client_id)
|
||||||
phase = game_clients.finish(client_id)
|
phase = game_clients.finish(client_id)
|
||||||
client_runtime_debug.pop(client_id, None)
|
client_runtime_debug.pop(client_id, None)
|
||||||
client_launch_settings.pop(client_id, None)
|
client_launch_settings.pop(client_id, None)
|
||||||
@@ -3243,6 +3562,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
client_id,
|
client_id,
|
||||||
ready=False,
|
ready=False,
|
||||||
returncode=returncode,
|
returncode=returncode,
|
||||||
|
cancelled=cancelled,
|
||||||
)
|
)
|
||||||
elif phase == "running" and not operation_active:
|
elif phase == "running" and not operation_active:
|
||||||
set_busy(False)
|
set_busy(False)
|
||||||
@@ -3290,10 +3610,12 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
and isinstance(value, tuple)
|
and isinstance(value, tuple)
|
||||||
and all(isinstance(item, PatchNote) for item in value)
|
and all(isinstance(item, PatchNote) for item in value)
|
||||||
):
|
):
|
||||||
show_patch_notes(value)
|
show_patch_notes(
|
||||||
mark_startup_component_ready(
|
value,
|
||||||
|
on_complete=lambda: mark_startup_component_ready(
|
||||||
"patches",
|
"patches",
|
||||||
"Patch notes ready",
|
"Patch notes ready",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
elif kind == "news_image":
|
elif kind == "news_image":
|
||||||
complete_news_image(value)
|
complete_news_image(value)
|
||||||
@@ -3377,6 +3699,11 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
)
|
)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
except Exception as event_error:
|
||||||
|
append_log(
|
||||||
|
"Dashboard event processing recovered from an error: "
|
||||||
|
f"{event_error}"
|
||||||
|
)
|
||||||
if operation_active:
|
if operation_active:
|
||||||
latest_progress = progress_updates.take()
|
latest_progress = progress_updates.take()
|
||||||
if latest_progress is not None:
|
if latest_progress is not None:
|
||||||
@@ -3421,6 +3748,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
title="Launcher is up to date",
|
title="Launcher is up to date",
|
||||||
message=check.reason,
|
message=check.reason,
|
||||||
kind="success",
|
kind="success",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
show_themed_dialog(
|
show_themed_dialog(
|
||||||
@@ -3431,6 +3759,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
f"{required_reason}\n\n{check.reason}"
|
f"{required_reason}\n\n{check.reason}"
|
||||||
),
|
),
|
||||||
kind="warning",
|
kind="warning",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not check.supported:
|
if not check.supported:
|
||||||
@@ -3439,6 +3768,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
title="Manual launcher update required",
|
title="Manual launcher update required",
|
||||||
message=check.reason,
|
message=check.reason,
|
||||||
kind="warning",
|
kind="warning",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not show_themed_dialog(
|
if not show_themed_dialog(
|
||||||
@@ -3450,6 +3780,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
kind="question",
|
kind="question",
|
||||||
confirm=True,
|
confirm=True,
|
||||||
accept_text="INSTALL & RESTART",
|
accept_text="INSTALL & RESTART",
|
||||||
|
window_icon=window_icon,
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
run_operation(
|
run_operation(
|
||||||
@@ -3477,6 +3808,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
f"{cache_error}"
|
f"{cache_error}"
|
||||||
),
|
),
|
||||||
kind="error",
|
kind="error",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
launch_settings: Settings | None = None
|
launch_settings: Settings | None = None
|
||||||
@@ -3561,6 +3893,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
title="Settings",
|
title="Settings",
|
||||||
message=f"The launcher could not load its settings:\n\n{error}",
|
message=f"The launcher could not load its settings:\n\n{error}",
|
||||||
kind="error",
|
kind="error",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
dialog = tk.Toplevel(root)
|
dialog = tk.Toplevel(root)
|
||||||
@@ -3576,6 +3909,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
dialog,
|
dialog,
|
||||||
title="PRISTONTALE EU",
|
title="PRISTONTALE EU",
|
||||||
subtitle="LAUNCHER SETTINGS",
|
subtitle="LAUNCHER SETTINGS",
|
||||||
|
window_icon=window_icon,
|
||||||
show_close=True,
|
show_close=True,
|
||||||
show_minimize=False,
|
show_minimize=False,
|
||||||
allow_close=True,
|
allow_close=True,
|
||||||
@@ -3732,6 +4066,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
title="Settings",
|
title="Settings",
|
||||||
message=str(error),
|
message=str(error),
|
||||||
kind="error",
|
kind="error",
|
||||||
|
window_icon=window_icon,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
append_log(
|
append_log(
|
||||||
@@ -3944,7 +4279,6 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
)
|
)
|
||||||
if cached_launcher_update is not None:
|
if cached_launcher_update is not None:
|
||||||
show_launcher_update_check(cached_launcher_update, log_reason=False)
|
show_launcher_update_check(cached_launcher_update, log_reason=False)
|
||||||
refresh_primary_action()
|
|
||||||
refresh_runtime_compatibility()
|
refresh_runtime_compatibility()
|
||||||
poll_events()
|
poll_events()
|
||||||
load_official_logo()
|
load_official_logo()
|
||||||
|
|||||||
Reference in New Issue
Block a user