Added server status and server selection dropdown.

This commit is contained in:
2026-08-30 22:15:32 +02:00
parent 562c924809
commit bc87f10661
3 changed files with 513 additions and 50 deletions

View File

@@ -7,19 +7,22 @@ from datetime import datetime
import hashlib
import json
import os
from pathlib import Path
from pathlib import Path, PurePosixPath
import platform
import shlex
import shutil
import sys
from typing import Mapping, Sequence
from .command import CommandOutcome, CommandRunner, OutputLineFilter
from .depot import DEFAULT_DEPOT_START_OPTION, DepotStartOption
from .gamebox import build_gamebox_command
from .paths import AppPaths
from .settings import Settings
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
WINDOWS_GAME_DIRECTORY = r"C:\Program Files (x86)\Pristontale EU"
WINDOWS_GAME_EXE = rf"{WINDOWS_GAME_DIRECTORY}\Game.exe"
NATIVE_D3D43_DLLS = ("d3dx9_43", "d3dcompiler_43")
DXVK_D3D9_OVERRIDE = "native,builtin"
DLL_OVERRIDES_KEY = r"HKCU\Software\Wine\DllOverrides"
@@ -293,6 +296,7 @@ def run_game(
selection: RuntimeSelection,
runner: CommandRunner,
*,
start_option: DepotStartOption = DEFAULT_DEPOT_START_OPTION,
use_gamebox: bool = True,
timeout_seconds: float = 12 * 60 * 60,
check: bool = True,
@@ -301,11 +305,15 @@ def run_game(
) -> CommandOutcome:
if not paths.runtime_view.is_dir() and not runner.dry_run:
raise RuntimeErrorPT("runtime view is missing; prepare the game first")
native_executable, windows_executable, game_arguments = _game_start_command(
paths,
start_option,
)
if selection.kind == "umu":
game_command = [
selection.executable,
str(paths.runtime_view / "Game.exe"),
"PTEU_REBORN",
native_executable,
*game_arguments,
]
environment = _umu_environment(paths)
elif selection.kind == "proton7-container":
@@ -313,8 +321,8 @@ def run_game(
raise RuntimeErrorPT("Proton script path is missing")
game_command = [
selection.executable,
str(paths.runtime_view / "Game.exe"),
"PTEU_REBORN",
native_executable,
*game_arguments,
]
environment = _umu_proton7_environment(
paths,
@@ -330,8 +338,8 @@ def run_game(
"ptlinux.proton_compat",
selection.proton_path,
"runinprefix",
WINDOWS_GAME_EXE,
"PTEU_REBORN",
windows_executable,
*game_arguments,
]
environment = _proton_environment(
paths,
@@ -356,6 +364,57 @@ def run_game(
)
def _game_start_command(
paths: AppPaths,
option: DepotStartOption,
) -> tuple[str, str, tuple[str, ...]]:
relative = PurePosixPath(option.path)
if (
relative.is_absolute()
or not relative.parts
or any(part in {"", ".", ".."} for part in relative.parts)
or relative.suffix.casefold() != ".exe"
):
raise RuntimeErrorPT(f"invalid realm executable path: {option.path!r}")
try:
arguments = tuple(shlex.split(option.arguments))
except ValueError as exc:
raise RuntimeErrorPT(
f"invalid launch arguments for realm {option.name}: {exc}"
) from exc
if not arguments:
raise RuntimeErrorPT(f"realm {option.name} has no launch arguments")
native = str(_resolve_runtime_path_case(paths.runtime_view, relative))
windows_relative = "\\".join(relative.parts)
return native, rf"{WINDOWS_GAME_DIRECTORY}\{windows_relative}", arguments
def _resolve_runtime_path_case(root: Path, relative: PurePosixPath) -> Path:
"""Match Windows manifest casing against the case-sensitive runtime view."""
direct = root.joinpath(*relative.parts)
if direct.exists():
return direct
cursor = root
for part in relative.parts:
if not cursor.is_dir():
return direct
try:
matches = tuple(
entry for entry in cursor.iterdir() if entry.name.casefold() == part.casefold()
)
except OSError:
return direct
if not matches:
return direct
if len(matches) > 1:
raise RuntimeErrorPT(
f"realm executable path has ambiguous casing: {relative.as_posix()}"
)
cursor = matches[0]
return cursor
def run_proton_prefix_command(
paths: AppPaths,
selection: RuntimeSelection,

View File

@@ -19,6 +19,7 @@ SETTINGS_SCHEMA = 1
@dataclass(frozen=True)
class Settings:
realm: str = "Babel"
width: int = 1600
height: int = 900
output_width: int = 1600
@@ -133,6 +134,7 @@ def _read_settings_object(path: Path) -> dict[str, object] | None:
def _settings_from_object(value: dict[str, object]) -> Settings:
settings = Settings(
realm=_string(value, "realm", "Babel").strip(),
width=_integer(value, "width", 1600),
height=_integer(value, "height", 900),
output_width=_integer(value, "output_width", 1600),
@@ -215,6 +217,12 @@ def _fsync_directory(path: Path) -> None:
def _validate(settings: Settings) -> None:
if (
len(settings.realm) > 64
or not settings.realm.strip()
or any(ord(character) < 32 for character in settings.realm)
):
raise ValueError("realm must be a non-empty display name")
for name, value in (
("width", settings.width),
("height", settings.height),

View File

@@ -17,7 +17,16 @@ import webbrowser
from typing import Callable, Mapping
from . import __version__
from .depot import DepotClient, DepotError, DepotSnapshot
from .depot import (
DEFAULT_DEPOT_START_OPTION,
DepotClient,
DepotError,
DepotSnapshot,
DepotStartOption,
cache_depot_launch_catalog,
launch_catalog_from_snapshot,
select_depot_start_option,
)
from .desktop_identity import (
APPLICATION_NAME,
APPLICATION_WM_CLASS,
@@ -48,6 +57,11 @@ from .self_update import (
check_for_launcher_update,
read_cached_launcher_update_check,
)
from .server_status import (
RealmServerStatus,
ServerStatusError,
fetch_server_status,
)
MAX_NEWS_CARDS = 3
@@ -70,6 +84,7 @@ DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
DASHBOARD_TRANSITION_STEPS = 9
DASHBOARD_TRANSITION_MINIMUM_OPACITY = 0.18
PRELOADER_STATUS_CHARACTERS = 64
DASHBOARD_STARTUP_COMPONENTS = frozenset(
{
"broadcast",
@@ -78,6 +93,7 @@ DASHBOARD_STARTUP_COMPONENTS = frozenset(
"logo",
"news",
"news_artwork",
"server_status",
"version",
}
)
@@ -92,6 +108,22 @@ class LauncherUpdatePresentation:
badge_text: str | None
@dataclass(frozen=True)
class GameSyncPresentation:
"""User-facing wording for one managed-game synchronization."""
operation_label: str
file_progress_verb: str
@dataclass(frozen=True)
class ServerStatusPresentation:
"""Text and Tk style for one official realm-health result."""
text: str
style: str
class DashboardError(RuntimeError):
"""Raised when the native dashboard cannot be displayed."""
@@ -148,6 +180,61 @@ def game_client_activity_text(running_count: int) -> str:
return f"{running_count} {label} running"
def game_sync_presentation(
*,
installed: bool,
repair: bool = False,
) -> GameSyncPresentation:
"""Distinguish a fresh download from an update or repair."""
if not installed:
return GameSyncPresentation(
operation_label=(
"Installing and verifying official game files…"
if repair
else "Installing official game files…"
),
file_progress_verb="Downloading",
)
if repair:
return GameSyncPresentation(
operation_label="Checking every game file against the official depot…",
file_progress_verb="Repairing",
)
return GameSyncPresentation(
operation_label="Synchronizing official game files…",
file_progress_verb="Updating",
)
def game_file_progress_status(current_file: str, verb: str) -> str:
"""Render the current depot asset with the operation's exact intent."""
return f"{verb} {current_file}"
def server_status_presentation(status: RealmServerStatus) -> ServerStatusPresentation:
if status.online is True:
players = (
f" · {status.players_online} online"
if status.players_online is not None
else " · online"
)
return ServerStatusPresentation(
text=f"● {status.name}{players}",
style="ServerOnline.TLabel",
)
if status.online is False:
return ServerStatusPresentation(
text=f"● {status.name} · offline",
style="ServerOffline.TLabel",
)
return ServerStatusPresentation(
text=f"○ {status.name} · status unknown",
style="ServerUnknown.TLabel",
)
def launcher_update_presentation(
check: LauncherUpdateCheck,
) -> LauncherUpdatePresentation:
@@ -200,6 +287,29 @@ def dashboard_transition_opacity(step: int, *, fading_in: bool) -> float:
return 1.0 - opacity_range * progress
def preloader_status_text(
value: str,
step: int | None = None,
total: int | None = None,
) -> str:
"""Render one launch stage without letting its widget change shape."""
if step is None or total is None:
return value
return f"Step {step} of {total} · {value}"
def freeze_preloader_geometry(frame: tk.Misc) -> tuple[int, int]:
"""Lock a measured preloader frame so later status text cannot reflow it."""
frame.update_idletasks()
width = max(1, frame.winfo_reqwidth())
height = max(1, frame.winfo_reqheight())
frame.configure(width=width, height=height)
frame.pack_propagate(False)
return width, height
def launcher_restart_spec(
paths: AppPaths,
environment: Mapping[str, str] | None = None,
@@ -228,6 +338,12 @@ def launcher_restart_spec(
return [sys.executable, "-B", "-P", "-m", "ptlinux", "gui"], env
def launcher_child_command(*arguments: str) -> list[str]:
"""Build a child command without writing bytecode into the active release."""
return [sys.executable, "-B", "-m", "ptlinux", *arguments]
class ProgressCoalescer:
"""Keep only the latest cross-thread progress event for the Tk loop.
@@ -347,7 +463,7 @@ def run_dashboard(paths: AppPaths) -> None:
root.configure(background="#0c0908")
root.withdraw()
try:
ensure_settings(paths)
startup_settings = ensure_settings(paths)
except (OSError, ValueError) as startup_error:
root.deiconify()
messagebox.showerror(
@@ -425,6 +541,37 @@ def run_dashboard(paths: AppPaths) -> None:
foreground="#aa9a8b",
font=("Sans", 8),
)
style.configure(
"ServerHeading.TLabel",
background="#191311",
foreground="#d8aa61",
font=("Sans", 8, "bold"),
padding=(0, 3),
)
style.configure(
"ServerOnline.TLabel",
background="#102719",
foreground="#58e27a",
font=("Sans", 8, "bold"),
padding=(8, 3),
anchor="w",
)
style.configure(
"ServerOffline.TLabel",
background="#301514",
foreground="#ff6258",
font=("Sans", 8, "bold"),
padding=(8, 3),
anchor="w",
)
style.configure(
"ServerUnknown.TLabel",
background="#211916",
foreground="#b8a99a",
font=("Sans", 8),
padding=(8, 3),
anchor="w",
)
style.configure(
"Speed.TLabel",
background="#191311",
@@ -494,6 +641,56 @@ def run_dashboard(paths: AppPaths) -> None:
fieldbackground=[("readonly", "#100d0c")],
foreground=[("readonly", "#f8ead2")],
)
style.configure(
"Realm.TCombobox",
fieldbackground="#211916",
background="#211916",
foreground="#fff0d2",
arrowcolor="#d8aa61",
bordercolor="#4a3528",
lightcolor="#4a3528",
darkcolor="#4a3528",
font=("Sans", 10, "bold"),
padding=(10, 7),
relief="flat",
)
style.map(
"Realm.TCombobox",
fieldbackground=[
("disabled", "#171412"),
("readonly", "#211916"),
],
background=[
("disabled", "#171412"),
("active", "#2a201c"),
("readonly", "#211916"),
],
foreground=[
("disabled", "#887a70"),
("readonly", "#fff0d2"),
],
arrowcolor=[
("disabled", "#887a70"),
("active", "#f0d7a2"),
("readonly", "#d8aa61"),
],
bordercolor=[
("disabled", "#4a3528"),
("focus", "#6a4d38"),
("readonly", "#4a3528"),
],
)
root.option_add("*realm_selector*Listbox.background", "#211916")
root.option_add("*realm_selector*Listbox.foreground", "#fff0d2")
root.option_add("*realm_selector*Listbox.selectBackground", "#2a201c")
root.option_add("*realm_selector*Listbox.selectForeground", "#f0d7a2")
root.option_add("*realm_selector*Listbox.font", "Sans 10 bold")
root.option_add("*realm_selector*Listbox.justify", "center")
root.option_add("*realm_selector*Listbox.selectBorderWidth", 4)
root.option_add("*realm_selector*Listbox.borderWidth", 1)
root.option_add("*realm_selector*Listbox.highlightThickness", 0)
root.option_add("*realm_selector*Listbox.relief", "flat")
root.option_add("*realm_selector*Listbox.activeStyle", "none")
style.configure(
"Settings.TCheckbutton",
background="#191311",
@@ -665,11 +862,14 @@ def run_dashboard(paths: AppPaths) -> None:
text="NATIVE LINUX LAUNCHER",
style="SplashTagline.TLabel",
).pack(anchor="w", pady=(2, 17))
ttk.Label(
startup_status_label = ttk.Label(
startup_shell,
textvariable=startup_status,
style="SplashStatus.TLabel",
).pack(anchor="w", pady=(0, 8))
width=PRELOADER_STATUS_CHARACTERS,
anchor="w",
)
startup_status_label.pack(fill="x", pady=(0, 8))
startup_progress = ttk.Progressbar(
startup_shell,
style="Depot.Horizontal.TProgressbar",
@@ -677,6 +877,7 @@ def run_dashboard(paths: AppPaths) -> None:
length=420,
)
startup_progress.pack(fill="x")
freeze_preloader_geometry(startup_border)
startup_progress.start(12)
startup_ready_components: set[str] = set()
startup_transition_started = False
@@ -921,8 +1122,25 @@ def run_dashboard(paths: AppPaths) -> None:
activity_panel = ttk.Frame(frame, style="Surface.TFrame", padding=(10, 7))
activity_panel.pack(fill="x", pady=(0, 8))
realm_value = tk.StringVar(value=DEFAULT_DEPOT_START_OPTION.name)
activity_action = ttk.Frame(activity_panel, style="Surface.TFrame")
activity_action.pack(side="right", fill="y", padx=(12, 0))
play_button = ttk.Button(activity_action, text="PLAY", style="Play.TButton")
play_button.pack(fill="both", expand=True)
realm_selector = ttk.Combobox(
activity_action,
name="realm_selector",
textvariable=realm_value,
values=(DEFAULT_DEPOT_START_OPTION.name,),
state="readonly",
style="Realm.TCombobox",
font=("Sans", 10, "bold"),
justify="center",
width=20,
)
realm_selector.pack(fill="x", pady=(5, 0))
activity_copy = ttk.Frame(activity_panel, style="Surface.TFrame")
activity_copy.pack(side="left", fill="x", expand=True, padx=(0, 12))
activity_copy.pack(side="left", fill="both", expand=True)
status = tk.StringVar(value="Ready")
progress_text = tk.StringVar(value="Click Play to start your journey")
speed_text = tk.StringVar(value="")
@@ -962,8 +1180,20 @@ def run_dashboard(paths: AppPaths) -> None:
style="Muted.TLabel",
anchor="w",
).pack(side="left", fill="x", expand=True)
play_button = ttk.Button(activity_panel, text="PLAY", style="Play.TButton")
play_button.pack(side="right", fill="y")
server_status_row = ttk.Frame(activity_copy, style="Surface.TFrame")
server_status_row.pack(fill="x", pady=(5, 1))
ttk.Label(
server_status_row,
text="SERVERS",
style="ServerHeading.TLabel",
).pack(side="left", fill="y", padx=(0, 8))
server_status_content = ttk.Frame(server_status_row, style="Surface.TFrame")
server_status_content.pack(side="left", fill="y", anchor="w")
ttk.Label(
server_status_content,
text="Checking official server status…",
style="ServerUnknown.TLabel",
).pack(side="left", anchor="w")
news_panel = ttk.Frame(frame, style="Surface.TFrame", padding=(10, 8))
news_panel.pack(fill="x", pady=(0, 8), before=controls)
@@ -1054,8 +1284,13 @@ def run_dashboard(paths: AppPaths) -> None:
operation_stage_total = 0
operation_message = ""
operation_refreshes_compatibility = False
operation_file_progress_verb = "Updating"
last_elapsed_second = -1
official_snapshot: DepotSnapshot | None = None
available_start_options: tuple[DepotStartOption, ...] = (DEFAULT_DEPOT_START_OPTION,)
preferred_realm = startup_settings.realm
latest_server_statuses: tuple[RealmServerStatus, ...] | None = None
server_status_placeholder = "Checking official server status…"
launcher_update_check: LauncherUpdateCheck | None = None
runtime_compatibility: RuntimeCompatibility | None = None
compatibility_check_generation = 0
@@ -1106,6 +1341,7 @@ def run_dashboard(paths: AppPaths) -> None:
new_state = "disabled" if value else "normal"
for button in buttons:
button.configure(state=new_state)
realm_selector.configure(state="disabled" if value else "readonly")
if not value and game_clients.running_count:
for button in game_mutation_buttons:
button.configure(state="disabled")
@@ -1143,7 +1379,9 @@ def run_dashboard(paths: AppPaths) -> None:
transfer_rate.reset()
speed_text.set("")
if isinstance(current_file, str) and current_file:
status.set(f"Updating {current_file}")
status.set(
game_file_progress_status(current_file, operation_file_progress_verb)
)
elif phase == "complete":
status.set("Game files are up to date and verified.")
@@ -1214,8 +1452,15 @@ def run_dashboard(paths: AppPaths) -> None:
text="Please wait while the game prepares its window.",
style="PreloaderStatus.TLabel",
).pack(anchor="w")
ttk.Label(content, textvariable=status_variable, style="PreloaderMuted.TLabel").pack(
status_label = ttk.Label(
content,
textvariable=status_variable,
style="PreloaderMuted.TLabel",
width=PRELOADER_STATUS_CHARACTERS,
anchor="w",
)
status_label.pack(
fill="x",
pady=(4, 10),
)
progress_widget = ttk.Progressbar(
@@ -1227,6 +1472,7 @@ def run_dashboard(paths: AppPaths) -> None:
)
preloader_progress = progress_widget
progress_widget.pack(fill="x")
freeze_preloader_geometry(content)
window.update_idletasks()
width = window.winfo_reqwidth()
height = window.winfo_reqheight()
@@ -1237,10 +1483,7 @@ def run_dashboard(paths: AppPaths) -> None:
def set_preloader_status(value: str, step: int | None = None, total: int | None = None) -> None:
if preloader_status is not None:
if step is not None and total is not None:
preloader_status.set(f"Step {step} of {total} · {value}")
else:
preloader_status.set(value)
preloader_status.set(preloader_status_text(value, step, total))
if preloader_progress is not None and step is not None and total is not None:
preloader_progress.configure(maximum=total, value=step)
@@ -1259,6 +1502,7 @@ def run_dashboard(paths: AppPaths) -> None:
show_preloader: bool = False,
restart_on_success: bool = False,
game_files_may_change: bool = False,
file_progress_verb: str = "Updating",
game_client_id: int | None = None,
) -> None:
nonlocal operation_active, game_window_active
@@ -1267,6 +1511,7 @@ def run_dashboard(paths: AppPaths) -> None:
nonlocal operation_stage_total
nonlocal operation_message
nonlocal operation_refreshes_compatibility
nonlocal operation_file_progress_verb
nonlocal last_elapsed_second
operation_active = True
game_window_active = game_clients.running_count > 0
@@ -1275,6 +1520,7 @@ def run_dashboard(paths: AppPaths) -> None:
operation_stage_total = 0
operation_message = ""
operation_refreshes_compatibility = game_files_may_change
operation_file_progress_verb = file_progress_verb
last_elapsed_second = -1
progress_updates.clear()
transfer_rate.reset()
@@ -1658,6 +1904,102 @@ def run_dashboard(paths: AppPaths) -> None:
threading.Thread(target=worker, name="ptlinux-logo", daemon=True).start()
def render_server_status() -> None:
for child in server_status_content.winfo_children():
child.destroy()
if latest_server_statuses is None:
ttk.Label(
server_status_content,
text=server_status_placeholder,
style="ServerUnknown.TLabel",
).pack(side="left", anchor="w")
return
statuses_by_name = {
item.name.casefold(): item for item in latest_server_statuses
}
visible = [
statuses_by_name[option.name.casefold()]
for option in available_start_options
if option.name.casefold() in statuses_by_name
]
if not visible:
ttk.Label(
server_status_content,
text="Official status is unavailable for this realm.",
style="ServerUnknown.TLabel",
).pack(side="left", anchor="w")
return
for index, server in enumerate(visible):
presentation = server_status_presentation(server)
ttk.Label(
server_status_content,
text=presentation.text,
style=presentation.style,
).pack(
side="left",
anchor="w",
padx=(0 if index == 0 else 6, 0),
)
def show_server_status(
statuses: tuple[RealmServerStatus, ...] | None,
*,
placeholder: str = "Server status unavailable.",
) -> None:
nonlocal latest_server_statuses, server_status_placeholder
latest_server_statuses = statuses
server_status_placeholder = placeholder
render_server_status()
def install_realm_options(snapshot: DepotSnapshot) -> None:
nonlocal available_start_options, preferred_realm
catalog = launch_catalog_from_snapshot(snapshot)
available_start_options = catalog.start_options
selected = select_depot_start_option(catalog, preferred_realm)
realm_selector.configure(
values=tuple(option.name for option in available_start_options),
state="disabled" if operation_active else "readonly",
)
realm_value.set(selected.name)
if selected.name != preferred_realm:
preferred_realm = selected.name
try:
update_settings(paths, realm=selected.name)
except (OSError, ValueError) as save_error:
append_log(f"Could not save the selected realm: {save_error}")
render_server_status()
def on_realm_selected(_event: object | None = None) -> None:
nonlocal preferred_realm
selected_name = realm_value.get()
selected = next(
(
option
for option in available_start_options
if option.name.casefold() == selected_name.casefold()
),
None,
)
if selected is None:
fallback = select_depot_start_option(
launch_catalog_from_snapshot(official_snapshot)
if official_snapshot is not None
else None,
preferred_realm,
)
realm_value.set(fallback.name)
return
realm_value.set(selected.name)
if preferred_realm == selected.name:
return
preferred_realm = selected.name
try:
update_settings(paths, realm=selected.name)
except (OSError, ValueError) as save_error:
append_log(f"Could not save the selected realm: {save_error}")
realm_selector.bind("<<ComboboxSelected>>", on_realm_selected)
def refresh_primary_action(
snapshot: DepotSnapshot | None = None,
*,
@@ -1802,16 +2144,38 @@ def run_dashboard(paths: AppPaths) -> None:
def news_worker() -> None:
events.put(("news", fetch_news(paths.news_cache)))
def server_status_worker() -> None:
try:
statuses = fetch_server_status()
except ServerStatusError as status_error:
events.put(("server_status_error", str(status_error)))
else:
events.put(("server_status", statuses))
def version_worker() -> None:
try:
snapshot = DepotClient().fetch_snapshot()
except DepotError as remote_error:
events.put(("version_error", str(remote_error)))
else:
try:
cache_depot_launch_catalog(paths.depot_launch_catalog_cache, snapshot)
except OSError as cache_error:
events.put(
(
"output",
f"Could not cache official realm choices: {cache_error}\n",
)
)
events.put(("version", snapshot))
threading.Thread(target=broadcast_worker, name="ptlinux-broadcast", daemon=True).start()
threading.Thread(target=news_worker, name="ptlinux-news", daemon=True).start()
threading.Thread(
target=server_status_worker,
name="ptlinux-server-status",
daemon=True,
).start()
threading.Thread(target=version_worker, name="ptlinux-version", daemon=True).start()
refresh_launcher_update_check()
@@ -1968,6 +2332,23 @@ def run_dashboard(paths: AppPaths) -> None:
"broadcast",
"Broadcast ready",
)
elif (
kind == "server_status"
and isinstance(value, tuple)
and all(isinstance(item, RealmServerStatus) for item in value)
):
show_server_status(value)
mark_startup_component_ready(
"server_status",
"Server status ready",
)
elif kind == "server_status_error" and isinstance(value, str):
show_server_status(None)
append_log(f"Server status check unavailable: {value}")
mark_startup_component_ready(
"server_status",
"Server status check complete",
)
elif kind == "news" and isinstance(value, tuple):
show_news(value)
mark_startup_component_ready(
@@ -2013,6 +2394,7 @@ def run_dashboard(paths: AppPaths) -> None:
version.set(
f"Launcher {__version__} · {value.version} · depot {value.build}"
)
install_realm_options(value)
refresh_primary_action(value)
mark_startup_component_ready(
"version",
@@ -2074,7 +2456,7 @@ def run_dashboard(paths: AppPaths) -> None:
root.after(poll_delay, poll_events)
def command_for(*arguments: str) -> list[str]:
return [sys.executable, "-m", "ptlinux", *arguments]
return launcher_child_command(*arguments)
def run_launcher_update(required_reason: str | None = None) -> None:
check = launcher_update_check
@@ -2128,15 +2510,51 @@ def run_dashboard(paths: AppPaths) -> None:
def start_game_client() -> None:
nonlocal active_game_launch_id
if official_snapshot is not None:
try:
cache_depot_launch_catalog(
paths.depot_launch_catalog_cache,
official_snapshot,
)
except OSError as cache_error:
messagebox.showerror(
"Realm selection",
"The launcher could not save the official realm selection:\n\n"
f"{cache_error}",
parent=root,
)
return
client_id = game_clients.begin()
active_game_launch_id = client_id
run_operation(
f"Starting PristonTale EU client {client_id}…",
command_for("play"),
f"Starting PristonTale EU {realm_value.get()} client {client_id}…",
command_for("play", "--realm", realm_value.get()),
show_preloader=True,
game_client_id=client_id,
)
def synchronize_game_files(
installation: GameInstallationStatus | None = None,
*,
repair: bool = False,
) -> None:
current = installation or refresh_primary_action(update_activity=False)
presentation = game_sync_presentation(
installed=current.installed,
repair=repair,
)
command = (
command_for("update", "--no-import", "--repair")
if repair
else command_for("update")
)
run_operation(
presentation.operation_label,
command,
game_files_may_change=True,
file_progress_verb=presentation.file_progress_verb,
)
def run_primary_action() -> None:
installation = refresh_primary_action(update_activity=False)
if (
@@ -2144,18 +2562,8 @@ def run_dashboard(paths: AppPaths) -> None:
or (game_clients.running_count > 0 and installation.installed)
):
start_game_client()
elif installation.action is PrimaryAction.INSTALL:
run_operation(
"Installing official game files…",
command_for("update"),
game_files_may_change=True,
)
elif installation.action is PrimaryAction.UPDATE:
run_operation(
"Synchronizing official game files…",
command_for("update"),
game_files_may_change=True,
)
elif installation.action in {PrimaryAction.INSTALL, PrimaryAction.UPDATE}:
synchronize_game_files(installation)
elif installation.action is PrimaryAction.LAUNCHER_UPDATE:
run_launcher_update(
runtime_compatibility.reason
@@ -2164,20 +2572,8 @@ def run_dashboard(paths: AppPaths) -> None:
)
play_button.configure(command=run_primary_action)
update_button.configure(
command=lambda: run_operation(
"Synchronizing official game files…",
command_for("update"),
game_files_may_change=True,
)
)
repair_button.configure(
command=lambda: run_operation(
"Checking every game file against the official depot…",
command_for("update", "--no-import", "--repair"),
game_files_may_change=True,
)
)
update_button.configure(command=synchronize_game_files)
repair_button.configure(command=lambda: synchronize_game_files(repair=True))
doctor_button.configure(
command=lambda: run_operation("Checking the system…", command_for("doctor"))
)