Added a debug system and fixed thread issues with update / repair.
This commit is contained in:
687
ptlinux/ui.py
687
ptlinux/ui.py
@@ -39,6 +39,12 @@ from .desktop_identity import (
|
|||||||
APPLICATION_WM_CLASS,
|
APPLICATION_WM_CLASS,
|
||||||
find_application_icon,
|
find_application_icon,
|
||||||
)
|
)
|
||||||
|
from .dashboard_debug import (
|
||||||
|
RuntimeDebugInfo,
|
||||||
|
VerificationDebugInfo,
|
||||||
|
inspect_runtime_debug,
|
||||||
|
read_verification_debug,
|
||||||
|
)
|
||||||
from .install_status import (
|
from .install_status import (
|
||||||
GameInstallationStatus,
|
GameInstallationStatus,
|
||||||
PrimaryAction,
|
PrimaryAction,
|
||||||
@@ -57,7 +63,7 @@ from .news import (
|
|||||||
)
|
)
|
||||||
from .overlay import RuntimeCompatibility, inspect_runtime_compatibility
|
from .overlay import RuntimeCompatibility, inspect_runtime_compatibility
|
||||||
from .paths import AppPaths
|
from .paths import AppPaths
|
||||||
from .settings import ensure_settings, update_settings
|
from .settings import Settings, ensure_settings, load_settings, update_settings
|
||||||
from .self_update import (
|
from .self_update import (
|
||||||
LauncherUpdateCheck,
|
LauncherUpdateCheck,
|
||||||
cache_launcher_update_check,
|
cache_launcher_update_check,
|
||||||
@@ -94,7 +100,8 @@ 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
|
||||||
PRELOADER_STATUS_CHARACTERS = 64
|
PRELOADER_STATUS_CHARACTERS = 64
|
||||||
SERVER_POPULATION_DEBUG_KEY = "<Control-Shift-F12>"
|
DASHBOARD_DEBUG_KEY = "<Control-Shift-F12>"
|
||||||
|
SERVER_STATUS_STALE_SECONDS = 5 * 60
|
||||||
DASHBOARD_STARTUP_COMPONENTS = frozenset(
|
DASHBOARD_STARTUP_COMPONENTS = frozenset(
|
||||||
{
|
{
|
||||||
"broadcast",
|
"broadcast",
|
||||||
@@ -135,6 +142,26 @@ class ServerStatusPresentation:
|
|||||||
style: str
|
style: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ServerStatusCheck:
|
||||||
|
"""One measured attempt to retrieve official realm health."""
|
||||||
|
|
||||||
|
statuses: tuple[RealmServerStatus, ...] | None
|
||||||
|
checked_at: float
|
||||||
|
duration_seconds: float
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class GameClientDebugState:
|
||||||
|
"""Live process state exposed only in the hidden dashboard debug view."""
|
||||||
|
|
||||||
|
client_id: int
|
||||||
|
phase: str
|
||||||
|
supervisor_pid: int | None
|
||||||
|
elapsed_seconds: float
|
||||||
|
|
||||||
|
|
||||||
class DashboardError(RuntimeError):
|
class DashboardError(RuntimeError):
|
||||||
"""Raised when the native dashboard cannot be displayed."""
|
"""Raised when the native dashboard cannot be displayed."""
|
||||||
|
|
||||||
@@ -142,32 +169,75 @@ class DashboardError(RuntimeError):
|
|||||||
class GameClientTracker:
|
class GameClientTracker:
|
||||||
"""Track dashboard-launched clients without retaining process objects in Tk."""
|
"""Track dashboard-launched clients without retaining process objects in Tk."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, *, clock: Callable[[], float] = time.monotonic) -> None:
|
||||||
|
self._clock = clock
|
||||||
self._next_client_id = 1
|
self._next_client_id = 1
|
||||||
self._starting: set[int] = set()
|
self._starting: set[int] = set()
|
||||||
self._running: set[int] = set()
|
self._running: set[int] = set()
|
||||||
|
self._started_at: dict[int, float] = {}
|
||||||
|
self._running_at: dict[int, float] = {}
|
||||||
|
self._supervisor_pids: dict[int, int] = {}
|
||||||
|
|
||||||
def begin(self) -> int:
|
def begin(self) -> int:
|
||||||
client_id = self._next_client_id
|
client_id = self._next_client_id
|
||||||
self._next_client_id += 1
|
self._next_client_id += 1
|
||||||
self._starting.add(client_id)
|
self._starting.add(client_id)
|
||||||
|
self._started_at[client_id] = self._clock()
|
||||||
return client_id
|
return client_id
|
||||||
|
|
||||||
|
def attach_supervisor(self, client_id: int, pid: int) -> bool:
|
||||||
|
if pid <= 0 or (
|
||||||
|
client_id not in self._starting and client_id not in self._running
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
self._supervisor_pids[client_id] = pid
|
||||||
|
return True
|
||||||
|
|
||||||
def mark_running(self, client_id: int) -> bool:
|
def mark_running(self, client_id: int) -> bool:
|
||||||
if client_id in self._running or client_id not in self._starting:
|
if client_id in self._running or client_id not in self._starting:
|
||||||
return False
|
return False
|
||||||
self._starting.remove(client_id)
|
self._starting.remove(client_id)
|
||||||
self._running.add(client_id)
|
self._running.add(client_id)
|
||||||
|
self._running_at[client_id] = self._clock()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def finish(self, client_id: int) -> str | None:
|
def finish(self, client_id: int) -> str | None:
|
||||||
if client_id in self._starting:
|
if client_id in self._starting:
|
||||||
self._starting.remove(client_id)
|
self._starting.remove(client_id)
|
||||||
return "starting"
|
phase = "starting"
|
||||||
if client_id in self._running:
|
elif client_id in self._running:
|
||||||
self._running.remove(client_id)
|
self._running.remove(client_id)
|
||||||
return "running"
|
phase = "running"
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
|
self._started_at.pop(client_id, None)
|
||||||
|
self._running_at.pop(client_id, None)
|
||||||
|
self._supervisor_pids.pop(client_id, None)
|
||||||
|
return phase
|
||||||
|
|
||||||
|
def debug_states(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
timestamp: float | None = None,
|
||||||
|
) -> tuple[GameClientDebugState, ...]:
|
||||||
|
now = self._clock() if timestamp is None else timestamp
|
||||||
|
return tuple(
|
||||||
|
GameClientDebugState(
|
||||||
|
client_id=client_id,
|
||||||
|
phase="running" if client_id in self._running else "starting",
|
||||||
|
supervisor_pid=self._supervisor_pids.get(client_id),
|
||||||
|
elapsed_seconds=max(
|
||||||
|
0.0,
|
||||||
|
now
|
||||||
|
- (
|
||||||
|
self._running_at.get(client_id, now)
|
||||||
|
if client_id in self._running
|
||||||
|
else self._started_at.get(client_id, now)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for client_id in sorted(self._starting | self._running)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def starting_count(self) -> int:
|
def starting_count(self) -> int:
|
||||||
@@ -191,6 +261,59 @@ def game_client_activity_text(running_count: int) -> str:
|
|||||||
return f"{running_count} {label} running"
|
return f"{running_count} {label} running"
|
||||||
|
|
||||||
|
|
||||||
|
def debug_elapsed_text(elapsed_seconds: float) -> str:
|
||||||
|
"""Format a live debug duration without sub-second UI churn."""
|
||||||
|
|
||||||
|
seconds = max(0, int(elapsed_seconds))
|
||||||
|
hours, remainder = divmod(seconds, 60 * 60)
|
||||||
|
minutes, seconds = divmod(remainder, 60)
|
||||||
|
if hours:
|
||||||
|
return f"{hours}h {minutes}m {seconds}s"
|
||||||
|
if minutes:
|
||||||
|
return f"{minutes}m {seconds}s"
|
||||||
|
return f"{seconds}s"
|
||||||
|
|
||||||
|
|
||||||
|
def operation_activity_text(
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
started_at: float | None,
|
||||||
|
step: int = 0,
|
||||||
|
total: int = 0,
|
||||||
|
detail: str | None = None,
|
||||||
|
timestamp: float | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Render live operation progress without dropping its elapsed timer."""
|
||||||
|
|
||||||
|
if detail is not None:
|
||||||
|
base = detail
|
||||||
|
elif total > 0:
|
||||||
|
base = f"Step {step}/{total} · {message}"
|
||||||
|
else:
|
||||||
|
return message
|
||||||
|
if started_at is None:
|
||||||
|
return base
|
||||||
|
now = time.monotonic() if timestamp is None else timestamp
|
||||||
|
elapsed = int(max(0.0, now - started_at))
|
||||||
|
return f"{base} · {elapsed}s elapsed"
|
||||||
|
|
||||||
|
|
||||||
|
def should_refresh_operation_elapsed(
|
||||||
|
*,
|
||||||
|
active: bool,
|
||||||
|
started_at: float | None,
|
||||||
|
stage_total: int,
|
||||||
|
detail: str | None,
|
||||||
|
) -> bool:
|
||||||
|
"""Return whether the dashboard has operation text with a live timer."""
|
||||||
|
|
||||||
|
return (
|
||||||
|
active
|
||||||
|
and started_at is not None
|
||||||
|
and (stage_total > 0 or detail is not None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def game_sync_presentation(
|
def game_sync_presentation(
|
||||||
*,
|
*,
|
||||||
installed: bool,
|
installed: bool,
|
||||||
@@ -224,29 +347,96 @@ def game_file_progress_status(current_file: str, verb: str) -> str:
|
|||||||
return f"{verb} {current_file}"
|
return f"{verb} {current_file}"
|
||||||
|
|
||||||
|
|
||||||
|
def perform_server_status_check(
|
||||||
|
*,
|
||||||
|
fetcher: Callable[[], tuple[RealmServerStatus, ...]] = fetch_server_status,
|
||||||
|
monotonic: Callable[[], float] = time.monotonic,
|
||||||
|
wall_clock: Callable[[], float] = time.time,
|
||||||
|
) -> ServerStatusCheck:
|
||||||
|
"""Measure one official status request without leaking its endpoint."""
|
||||||
|
|
||||||
|
started_at = monotonic()
|
||||||
|
try:
|
||||||
|
statuses = fetcher()
|
||||||
|
except ServerStatusError as error:
|
||||||
|
duration_seconds = max(0.0, monotonic() - started_at)
|
||||||
|
return ServerStatusCheck(
|
||||||
|
statuses=None,
|
||||||
|
checked_at=wall_clock(),
|
||||||
|
duration_seconds=duration_seconds,
|
||||||
|
error=str(error),
|
||||||
|
)
|
||||||
|
duration_seconds = max(0.0, monotonic() - started_at)
|
||||||
|
return ServerStatusCheck(
|
||||||
|
statuses=statuses,
|
||||||
|
checked_at=wall_clock(),
|
||||||
|
duration_seconds=duration_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def server_status_check_text(
|
||||||
|
check: ServerStatusCheck,
|
||||||
|
*,
|
||||||
|
timestamp: float | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Render fetch age, latency, and freshness for the hidden debug view."""
|
||||||
|
|
||||||
|
now = time.time() if timestamp is None else timestamp
|
||||||
|
age_seconds = max(0.0, now - check.checked_at)
|
||||||
|
whole_seconds = int(age_seconds)
|
||||||
|
if whole_seconds < 60:
|
||||||
|
age = f"{whole_seconds}s"
|
||||||
|
elif whole_seconds < 60 * 60:
|
||||||
|
age = f"{whole_seconds // 60}m"
|
||||||
|
elif whole_seconds < 24 * 60 * 60:
|
||||||
|
age = f"{whole_seconds // (60 * 60)}h"
|
||||||
|
else:
|
||||||
|
age = f"{whole_seconds // (24 * 60 * 60)}d"
|
||||||
|
latency_milliseconds = max(0, round(check.duration_seconds * 1000))
|
||||||
|
timing = f"checked {age} ago · {latency_milliseconds} ms"
|
||||||
|
if check.error is not None:
|
||||||
|
return f"failed · {timing} · {check.error}"
|
||||||
|
freshness = "stale" if age_seconds >= SERVER_STATUS_STALE_SECONDS else "fresh"
|
||||||
|
return f"{freshness} · {timing}"
|
||||||
|
|
||||||
|
|
||||||
|
def _server_component_text(name: str, online: bool | None) -> str:
|
||||||
|
if online is True:
|
||||||
|
state = "up"
|
||||||
|
elif online is False:
|
||||||
|
state = "down"
|
||||||
|
else:
|
||||||
|
state = "unknown"
|
||||||
|
return f"{name} {state}"
|
||||||
|
|
||||||
|
|
||||||
def server_status_presentation(
|
def server_status_presentation(
|
||||||
status: RealmServerStatus,
|
status: RealmServerStatus,
|
||||||
*,
|
*,
|
||||||
show_player_count: bool = False,
|
show_debug_details: bool = False,
|
||||||
) -> ServerStatusPresentation:
|
) -> ServerStatusPresentation:
|
||||||
if status.online is True:
|
if status.online is True:
|
||||||
players = ""
|
text = f"● {status.name}"
|
||||||
if show_player_count and status.players_online is not None:
|
style = "ServerOnline.TLabel"
|
||||||
|
elif status.online is False:
|
||||||
|
text = f"● {status.name} · offline"
|
||||||
|
style = "ServerOffline.TLabel"
|
||||||
|
else:
|
||||||
|
text = f"○ {status.name} · status unknown"
|
||||||
|
style = "ServerUnknown.TLabel"
|
||||||
|
if show_debug_details:
|
||||||
|
details: list[str] = []
|
||||||
|
if status.players_online is not None:
|
||||||
label = "player" if status.players_online == 1 else "players"
|
label = "player" if status.players_online == 1 else "players"
|
||||||
players = f" · {status.players_online} {label}"
|
details.append(f"{status.players_online} {label}")
|
||||||
return ServerStatusPresentation(
|
details.extend(
|
||||||
text=f"● {status.name}{players}",
|
(
|
||||||
style="ServerOnline.TLabel",
|
_server_component_text("login", status.login_online),
|
||||||
|
_server_component_text("game", status.game_online),
|
||||||
)
|
)
|
||||||
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",
|
|
||||||
)
|
)
|
||||||
|
text = " · ".join((text, *details))
|
||||||
|
return ServerStatusPresentation(text=text, style=style)
|
||||||
|
|
||||||
|
|
||||||
def patch_change_colors(type_key: str) -> tuple[str, str]:
|
def patch_change_colors(type_key: str) -> tuple[str, str]:
|
||||||
@@ -1257,6 +1447,12 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
).pack(side="left", fill="y", padx=(0, 8))
|
).pack(side="left", fill="y", padx=(0, 8))
|
||||||
server_status_content = ttk.Frame(server_status_row, style="Surface.TFrame")
|
server_status_content = ttk.Frame(server_status_row, style="Surface.TFrame")
|
||||||
server_status_content.pack(side="left", fill="y", anchor="w")
|
server_status_content.pack(side="left", fill="y", anchor="w")
|
||||||
|
server_status_debug_text = tk.StringVar(value="")
|
||||||
|
ttk.Label(
|
||||||
|
server_status_row,
|
||||||
|
textvariable=server_status_debug_text,
|
||||||
|
style="Muted.TLabel",
|
||||||
|
).pack(side="right", anchor="e", padx=(8, 0))
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
server_status_content,
|
server_status_content,
|
||||||
text="Checking official server status…",
|
text="Checking official server status…",
|
||||||
@@ -1382,7 +1578,11 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
preloader_status: tk.StringVar | None = None
|
preloader_status: tk.StringVar | None = None
|
||||||
preloader_progress: ttk.Progressbar | None = None
|
preloader_progress: ttk.Progressbar | None = None
|
||||||
settings_window: tk.Toplevel | None = None
|
settings_window: tk.Toplevel | None = None
|
||||||
|
dashboard_debug_window: tk.Toplevel | None = None
|
||||||
|
dashboard_debug_output: tk.Text | None = None
|
||||||
game_clients = GameClientTracker()
|
game_clients = GameClientTracker()
|
||||||
|
client_runtime_debug: dict[int, RuntimeDebugInfo] = {}
|
||||||
|
client_launch_settings: dict[int, Settings] = {}
|
||||||
active_game_launch_id: int | None = None
|
active_game_launch_id: int | None = None
|
||||||
operation_active = False
|
operation_active = False
|
||||||
game_window_active = False
|
game_window_active = False
|
||||||
@@ -1391,15 +1591,22 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
operation_stage = 0
|
operation_stage = 0
|
||||||
operation_stage_total = 0
|
operation_stage_total = 0
|
||||||
operation_message = ""
|
operation_message = ""
|
||||||
|
operation_progress_detail: str | None = None
|
||||||
operation_refreshes_compatibility = False
|
operation_refreshes_compatibility = False
|
||||||
operation_file_progress_verb = "Updating"
|
operation_file_progress_verb = "Updating"
|
||||||
last_elapsed_second = -1
|
last_elapsed_second = -1
|
||||||
official_snapshot: DepotSnapshot | None = None
|
official_snapshot: DepotSnapshot | None = None
|
||||||
|
latest_installation_status: GameInstallationStatus | None = None
|
||||||
available_start_options: tuple[DepotStartOption, ...] = (DEFAULT_DEPOT_START_OPTION,)
|
available_start_options: tuple[DepotStartOption, ...] = (DEFAULT_DEPOT_START_OPTION,)
|
||||||
preferred_realm = startup_settings.realm
|
preferred_realm = startup_settings.realm
|
||||||
latest_server_statuses: tuple[RealmServerStatus, ...] | None = None
|
latest_server_statuses: tuple[RealmServerStatus, ...] | None = None
|
||||||
|
latest_server_status_check: ServerStatusCheck | None = None
|
||||||
server_status_placeholder = "Checking official server status…"
|
server_status_placeholder = "Checking official server status…"
|
||||||
show_server_population = False
|
show_dashboard_debug = False
|
||||||
|
dashboard_runtime_debug: RuntimeDebugInfo | None = None
|
||||||
|
dashboard_verification_debug: VerificationDebugInfo | None = None
|
||||||
|
dashboard_debug_state_error: str | None = None
|
||||||
|
last_dashboard_debug_render = ""
|
||||||
launcher_update_check: LauncherUpdateCheck | None = None
|
launcher_update_check: LauncherUpdateCheck | None = None
|
||||||
runtime_compatibility: RuntimeCompatibility | None = None
|
runtime_compatibility: RuntimeCompatibility | None = None
|
||||||
compatibility_check_generation = 0
|
compatibility_check_generation = 0
|
||||||
@@ -1456,6 +1663,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
button.configure(state="disabled")
|
button.configure(state="disabled")
|
||||||
|
|
||||||
def update_progress(event: Mapping[str, object]) -> None:
|
def update_progress(event: Mapping[str, object]) -> None:
|
||||||
|
nonlocal operation_progress_detail
|
||||||
phase = event.get("phase")
|
phase = event.get("phase")
|
||||||
files_completed = _as_int(event.get("files_completed"))
|
files_completed = _as_int(event.get("files_completed"))
|
||||||
files_total = _as_int(event.get("files_total"))
|
files_total = _as_int(event.get("files_total"))
|
||||||
@@ -1471,7 +1679,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
progress_bar.configure(mode="determinate")
|
progress_bar.configure(mode="determinate")
|
||||||
if bytes_total:
|
if bytes_total:
|
||||||
progress_bar.configure(maximum=bytes_total, value=min(bytes_completed, bytes_total))
|
progress_bar.configure(maximum=bytes_total, value=min(bytes_completed, bytes_total))
|
||||||
progress_text.set(
|
operation_progress_detail = (
|
||||||
f"{files_completed}/{files_total} files · "
|
f"{files_completed}/{files_total} files · "
|
||||||
f"{_format_bytes(bytes_completed)} / {_format_bytes(bytes_total)}"
|
f"{_format_bytes(bytes_completed)} / {_format_bytes(bytes_total)}"
|
||||||
)
|
)
|
||||||
@@ -1484,9 +1692,10 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
speed_text.set("")
|
speed_text.set("")
|
||||||
else:
|
else:
|
||||||
progress_bar.configure(maximum=3, value=3)
|
progress_bar.configure(maximum=3, value=3)
|
||||||
progress_text.set("Game files are already up to date.")
|
operation_progress_detail = "Game files are already up to date."
|
||||||
transfer_rate.reset()
|
transfer_rate.reset()
|
||||||
speed_text.set("")
|
speed_text.set("")
|
||||||
|
progress_text.set(activity_text())
|
||||||
if isinstance(current_file, str) and current_file:
|
if isinstance(current_file, str) and current_file:
|
||||||
status.set(
|
status.set(
|
||||||
game_file_progress_status(current_file, operation_file_progress_verb)
|
game_file_progress_status(current_file, operation_file_progress_verb)
|
||||||
@@ -1495,19 +1704,21 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
status.set("Game files are up to date and verified.")
|
status.set("Game files are up to date and verified.")
|
||||||
|
|
||||||
def activity_text() -> str:
|
def activity_text() -> str:
|
||||||
if operation_started_at is None or operation_stage_total <= 0:
|
return operation_activity_text(
|
||||||
return operation_message
|
operation_message,
|
||||||
elapsed = int(time.monotonic() - operation_started_at)
|
started_at=operation_started_at,
|
||||||
return (
|
step=operation_stage,
|
||||||
f"Step {operation_stage}/{operation_stage_total} · "
|
total=operation_stage_total,
|
||||||
f"{operation_message} · {elapsed}s elapsed"
|
detail=operation_progress_detail,
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_operation_stage(step: int, total: int, message: str) -> None:
|
def set_operation_stage(step: int, total: int, message: str) -> None:
|
||||||
nonlocal operation_stage, operation_stage_total, operation_message
|
nonlocal operation_stage, operation_stage_total, operation_message
|
||||||
|
nonlocal operation_progress_detail
|
||||||
operation_stage = min(max(step, 0), total)
|
operation_stage = min(max(step, 0), total)
|
||||||
operation_stage_total = max(total, 1)
|
operation_stage_total = max(total, 1)
|
||||||
operation_message = message
|
operation_message = message
|
||||||
|
operation_progress_detail = None
|
||||||
transfer_rate.reset()
|
transfer_rate.reset()
|
||||||
speed_text.set("")
|
speed_text.set("")
|
||||||
progress_bar.stop()
|
progress_bar.stop()
|
||||||
@@ -1522,12 +1733,14 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
|
|
||||||
def update_elapsed_indicator() -> None:
|
def update_elapsed_indicator() -> None:
|
||||||
nonlocal last_elapsed_second
|
nonlocal last_elapsed_second
|
||||||
if (
|
if not should_refresh_operation_elapsed(
|
||||||
not operation_active
|
active=operation_active,
|
||||||
or operation_started_at is None
|
started_at=operation_started_at,
|
||||||
or operation_stage_total != LAUNCH_STAGE_TOTAL
|
stage_total=operation_stage_total,
|
||||||
|
detail=operation_progress_detail,
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
assert operation_started_at is not None
|
||||||
elapsed = int(time.monotonic() - operation_started_at)
|
elapsed = int(time.monotonic() - operation_started_at)
|
||||||
if elapsed == last_elapsed_second:
|
if elapsed == last_elapsed_second:
|
||||||
return
|
return
|
||||||
@@ -1619,6 +1832,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
nonlocal operation_stage
|
nonlocal operation_stage
|
||||||
nonlocal operation_stage_total
|
nonlocal operation_stage_total
|
||||||
nonlocal operation_message
|
nonlocal operation_message
|
||||||
|
nonlocal operation_progress_detail
|
||||||
nonlocal operation_refreshes_compatibility
|
nonlocal operation_refreshes_compatibility
|
||||||
nonlocal operation_file_progress_verb
|
nonlocal operation_file_progress_verb
|
||||||
nonlocal last_elapsed_second
|
nonlocal last_elapsed_second
|
||||||
@@ -1628,6 +1842,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
operation_stage = 0
|
operation_stage = 0
|
||||||
operation_stage_total = 0
|
operation_stage_total = 0
|
||||||
operation_message = ""
|
operation_message = ""
|
||||||
|
operation_progress_detail = None
|
||||||
operation_refreshes_compatibility = game_files_may_change
|
operation_refreshes_compatibility = game_files_may_change
|
||||||
operation_file_progress_verb = file_progress_verb
|
operation_file_progress_verb = file_progress_verb
|
||||||
last_elapsed_second = -1
|
last_elapsed_second = -1
|
||||||
@@ -1669,6 +1884,10 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
else:
|
else:
|
||||||
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:
|
||||||
|
events.put(
|
||||||
|
("game_supervisor_started", (game_client_id, process.pid))
|
||||||
|
)
|
||||||
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 "):
|
||||||
@@ -2204,7 +2423,19 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
|
|
||||||
threading.Thread(target=worker, name="ptlinux-logo", daemon=True).start()
|
threading.Thread(target=worker, name="ptlinux-logo", daemon=True).start()
|
||||||
|
|
||||||
|
def update_server_status_debug_text() -> None:
|
||||||
|
value = ""
|
||||||
|
if show_dashboard_debug:
|
||||||
|
value = (
|
||||||
|
server_status_check_text(latest_server_status_check)
|
||||||
|
if latest_server_status_check is not None
|
||||||
|
else "status check pending"
|
||||||
|
)
|
||||||
|
if server_status_debug_text.get() != value:
|
||||||
|
server_status_debug_text.set(value)
|
||||||
|
|
||||||
def render_server_status() -> None:
|
def render_server_status() -> None:
|
||||||
|
update_server_status_debug_text()
|
||||||
for child in server_status_content.winfo_children():
|
for child in server_status_content.winfo_children():
|
||||||
child.destroy()
|
child.destroy()
|
||||||
if latest_server_statuses is None:
|
if latest_server_statuses is None:
|
||||||
@@ -2232,7 +2463,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
for index, server in enumerate(visible):
|
for index, server in enumerate(visible):
|
||||||
presentation = server_status_presentation(
|
presentation = server_status_presentation(
|
||||||
server,
|
server,
|
||||||
show_player_count=show_server_population,
|
show_debug_details=show_dashboard_debug,
|
||||||
)
|
)
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
server_status_content,
|
server_status_content,
|
||||||
@@ -2245,24 +2476,315 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def show_server_status(
|
def show_server_status(
|
||||||
statuses: tuple[RealmServerStatus, ...] | None,
|
check: ServerStatusCheck,
|
||||||
*,
|
|
||||||
placeholder: str = "Server status unavailable.",
|
|
||||||
) -> None:
|
) -> None:
|
||||||
nonlocal latest_server_statuses, server_status_placeholder
|
nonlocal latest_server_statuses, latest_server_status_check
|
||||||
latest_server_statuses = statuses
|
nonlocal server_status_placeholder
|
||||||
server_status_placeholder = placeholder
|
latest_server_status_check = check
|
||||||
|
latest_server_statuses = check.statuses
|
||||||
|
server_status_placeholder = (
|
||||||
|
"Server status unavailable."
|
||||||
|
if check.error is not None
|
||||||
|
else "Official status is unavailable for this realm."
|
||||||
|
)
|
||||||
render_server_status()
|
render_server_status()
|
||||||
|
|
||||||
def toggle_server_population_debug(_event: object | None = None) -> str:
|
def refresh_dashboard_debug_disk_state() -> None:
|
||||||
nonlocal show_server_population
|
nonlocal dashboard_runtime_debug, dashboard_verification_debug
|
||||||
show_server_population = not show_server_population
|
nonlocal dashboard_debug_state_error
|
||||||
|
dashboard_debug_state_error = None
|
||||||
|
try:
|
||||||
|
settings = load_settings(paths)
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
dashboard_runtime_debug = None
|
||||||
|
dashboard_debug_state_error = f"settings unavailable: {error}"
|
||||||
|
else:
|
||||||
|
dashboard_runtime_debug = inspect_runtime_debug(paths, settings)
|
||||||
|
try:
|
||||||
|
dashboard_verification_debug = read_verification_debug(paths)
|
||||||
|
except OSError as error:
|
||||||
|
dashboard_verification_debug = None
|
||||||
|
detail = f"verification state unavailable: {error}"
|
||||||
|
dashboard_debug_state_error = (
|
||||||
|
f"{dashboard_debug_state_error}; {detail}"
|
||||||
|
if dashboard_debug_state_error
|
||||||
|
else detail
|
||||||
|
)
|
||||||
|
|
||||||
|
def current_debug_start_option() -> DepotStartOption | None:
|
||||||
|
selected_name = realm_value.get().casefold()
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
for option in available_start_options
|
||||||
|
if option.name.casefold() == selected_name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def dashboard_debug_report() -> str:
|
||||||
|
lines = ["PT-LINUX DASHBOARD DEBUG", ""]
|
||||||
|
lines.extend(("SERVERS", "-------"))
|
||||||
|
lines.append(
|
||||||
|
server_status_check_text(latest_server_status_check)
|
||||||
|
if latest_server_status_check is not None
|
||||||
|
else "status check pending"
|
||||||
|
)
|
||||||
|
if latest_server_statuses:
|
||||||
|
lines.extend(
|
||||||
|
server_status_presentation(
|
||||||
|
server,
|
||||||
|
show_debug_details=True,
|
||||||
|
).text
|
||||||
|
for server in latest_server_statuses
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(server_status_placeholder)
|
||||||
|
|
||||||
|
lines.extend(("", "LAUNCH", "------"))
|
||||||
|
start_option = current_debug_start_option()
|
||||||
|
lines.append(f"Realm: {realm_value.get() or 'unknown'}")
|
||||||
|
lines.append(
|
||||||
|
f"Executable: {start_option.path if start_option is not None else 'unknown'}"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"Arguments: {start_option.arguments if start_option is not None else 'unknown'}"
|
||||||
|
)
|
||||||
|
if official_snapshot is None:
|
||||||
|
lines.append("Official depot: unavailable")
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f"Official depot: build {official_snapshot.build} · "
|
||||||
|
f"version {official_snapshot.version}"
|
||||||
|
)
|
||||||
|
if latest_installation_status is None:
|
||||||
|
lines.append("Installed depot: unchecked")
|
||||||
|
else:
|
||||||
|
local_build = latest_installation_status.local_build
|
||||||
|
lines.append(
|
||||||
|
f"Installed depot: {local_build if local_build is not None else 'unknown'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client_states = game_clients.debug_states()
|
||||||
|
active_runtime = next(
|
||||||
|
(
|
||||||
|
client_runtime_debug[state.client_id]
|
||||||
|
for state in client_states
|
||||||
|
if state.client_id in client_runtime_debug
|
||||||
|
),
|
||||||
|
dashboard_runtime_debug,
|
||||||
|
)
|
||||||
|
lines.extend(("", "RUNTIME", "-------"))
|
||||||
|
lines.append(
|
||||||
|
"Source: active client launch snapshot"
|
||||||
|
if client_states and active_runtime is not dashboard_runtime_debug
|
||||||
|
else "Source: next-launch selection"
|
||||||
|
)
|
||||||
|
if active_runtime is None:
|
||||||
|
lines.append(dashboard_debug_state_error or "Runtime state unavailable")
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f"Requested / selected: {active_runtime.requested_kind} / "
|
||||||
|
f"{active_runtime.selected_kind or 'unavailable'}"
|
||||||
|
)
|
||||||
|
lines.append(f"Runtime executable: {active_runtime.executable or 'unavailable'}")
|
||||||
|
lines.append(f"Proton: {active_runtime.proton_path or 'not applicable'}")
|
||||||
|
lines.append(f"Prefix: {active_runtime.prefix or 'unavailable'}")
|
||||||
|
lines.append(
|
||||||
|
f"Synchronization: {active_runtime.sync_configured} / "
|
||||||
|
f"{active_runtime.sync_effective or 'runtime managed'}"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"Prepared runtime: {active_runtime.prepared_kind or 'not recorded'} · "
|
||||||
|
f"{active_runtime.prepared_at or 'time unavailable'}"
|
||||||
|
)
|
||||||
|
if active_runtime.prepared_proton_path is not None:
|
||||||
|
lines.append(f"Prepared Proton: {active_runtime.prepared_proton_path}")
|
||||||
|
marker_match = active_runtime.marker_matches_selection
|
||||||
|
lines.append(
|
||||||
|
"Preparation marker matches selection: "
|
||||||
|
+ (
|
||||||
|
"yes"
|
||||||
|
if marker_match is True
|
||||||
|
else "no"
|
||||||
|
if marker_match is False
|
||||||
|
else "unknown"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"DXVK D3D9 override: "
|
||||||
|
f"{active_runtime.dxvk_d3d9_override or 'not recorded'}"
|
||||||
|
)
|
||||||
|
lines.append(f"DXVK state cache: {paths.dxvk_cache}")
|
||||||
|
if active_runtime.error:
|
||||||
|
lines.append(f"Runtime selection error: {active_runtime.error}")
|
||||||
|
|
||||||
|
lines.extend(("", "CLIENTS", "-------"))
|
||||||
|
lines.append(
|
||||||
|
f"Starting: {game_clients.starting_count} · "
|
||||||
|
f"running: {game_clients.running_count}"
|
||||||
|
)
|
||||||
|
if not client_states:
|
||||||
|
lines.append("No active game clients")
|
||||||
|
for state in client_states:
|
||||||
|
pid = str(state.supervisor_pid) if state.supervisor_pid is not None else "pending"
|
||||||
|
lines.append(
|
||||||
|
f"Client {state.client_id}: {state.phase} · "
|
||||||
|
f"{debug_elapsed_text(state.elapsed_seconds)} · supervisor PID {pid}"
|
||||||
|
)
|
||||||
|
client_runtime = client_runtime_debug.get(state.client_id)
|
||||||
|
if client_runtime is not None:
|
||||||
|
lines.append(
|
||||||
|
f" runtime {client_runtime.selected_kind or 'unavailable'} · "
|
||||||
|
f"prefix {client_runtime.prefix or 'unavailable'}"
|
||||||
|
)
|
||||||
|
if state.client_id == active_game_launch_id and operation_stage_total > 0:
|
||||||
|
lines.append(
|
||||||
|
f" launch stage {operation_stage}/{operation_stage_total} · "
|
||||||
|
f"{operation_message or 'waiting'}"
|
||||||
|
)
|
||||||
|
if operation_active and active_game_launch_id is None:
|
||||||
|
lines.append(
|
||||||
|
f"Launcher operation: {operation_stage}/{operation_stage_total} · "
|
||||||
|
f"{operation_message or status.get()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.extend(("", "VERIFICATION", "------------"))
|
||||||
|
verification = dashboard_verification_debug
|
||||||
|
if verification is None:
|
||||||
|
lines.append(dashboard_debug_state_error or "Verification state unavailable")
|
||||||
|
else:
|
||||||
|
lines.append(f"Imported-tree trust: {verification.import_state}")
|
||||||
|
if verification.import_verified_at is not None:
|
||||||
|
lines.append(
|
||||||
|
f"Import verified: build "
|
||||||
|
f"{verification.import_build if verification.import_build is not None else 'unknown'} · "
|
||||||
|
f"{verification.import_verified_at}"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"Last full verification: "
|
||||||
|
f"{verification.last_full_verification_at or 'not recorded'}"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"Last depot sync: {verification.last_sync_at or 'not recorded'} · "
|
||||||
|
f"{verification.mode or 'mode unknown'}"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"Cached integrity: build "
|
||||||
|
f"{verification.build if verification.build is not None else 'unknown'} · "
|
||||||
|
f"version {verification.version or 'unknown'} · "
|
||||||
|
f"{verification.files_total if verification.files_total is not None else '?'} files"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
"Last sync changes: "
|
||||||
|
f"downloaded {verification.files_downloaded if verification.files_downloaded is not None else '?'} · "
|
||||||
|
f"deleted {verification.files_deleted if verification.files_deleted is not None else '?'} · "
|
||||||
|
f"created {verification.files_created if verification.files_created is not None else '?'} · "
|
||||||
|
f"quarantined {verification.files_quarantined if verification.files_quarantined is not None else '?'}"
|
||||||
|
)
|
||||||
|
if latest_installation_status is not None:
|
||||||
|
missing = latest_installation_status.missing_files
|
||||||
|
lines.append(
|
||||||
|
"Required files: complete"
|
||||||
|
if not missing
|
||||||
|
else "Required files missing: " + ", ".join(str(path) for path in missing)
|
||||||
|
)
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
def update_dashboard_debug_view(*, force: bool = False) -> None:
|
||||||
|
nonlocal last_dashboard_debug_render
|
||||||
|
if (
|
||||||
|
not show_dashboard_debug
|
||||||
|
or dashboard_debug_output is None
|
||||||
|
or not dashboard_debug_output.winfo_exists()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
rendered = dashboard_debug_report()
|
||||||
|
if not force and rendered == last_dashboard_debug_render:
|
||||||
|
return
|
||||||
|
position = dashboard_debug_output.yview()[0]
|
||||||
|
dashboard_debug_output.configure(state="normal")
|
||||||
|
dashboard_debug_output.delete("1.0", "end")
|
||||||
|
dashboard_debug_output.insert("1.0", rendered)
|
||||||
|
dashboard_debug_output.configure(state="disabled")
|
||||||
|
dashboard_debug_output.yview_moveto(position)
|
||||||
|
last_dashboard_debug_render = rendered
|
||||||
|
|
||||||
|
def ensure_dashboard_debug_window() -> None:
|
||||||
|
nonlocal dashboard_debug_window, dashboard_debug_output
|
||||||
|
if dashboard_debug_window is not None and dashboard_debug_window.winfo_exists():
|
||||||
|
return
|
||||||
|
window = tk.Toplevel(root)
|
||||||
|
dashboard_debug_window = window
|
||||||
|
window.title("PristonTale EU — Debug View")
|
||||||
|
window.geometry("900x640")
|
||||||
|
window.minsize(700, 440)
|
||||||
|
window.configure(background="#0c0908")
|
||||||
|
debug_header = tk.Frame(window, background="#24170f", padx=16, pady=12)
|
||||||
|
debug_header.pack(fill="x")
|
||||||
|
tk.Label(
|
||||||
|
debug_header,
|
||||||
|
text="DASHBOARD DEBUG VIEW",
|
||||||
|
background="#24170f",
|
||||||
|
foreground="#f5d18b",
|
||||||
|
font=("Sans", 10, "bold"),
|
||||||
|
).pack(anchor="w")
|
||||||
|
tk.Label(
|
||||||
|
debug_header,
|
||||||
|
text="Read-only live launcher, runtime, client, and trust state",
|
||||||
|
background="#24170f",
|
||||||
|
foreground="#c9baaa",
|
||||||
|
font=("Sans", 9),
|
||||||
|
).pack(anchor="w", pady=(2, 0))
|
||||||
|
body = tk.Frame(window, background="#0c0908")
|
||||||
|
body.pack(fill="both", expand=True, padx=12, pady=12)
|
||||||
|
body.grid_rowconfigure(0, weight=1)
|
||||||
|
body.grid_columnconfigure(0, weight=1)
|
||||||
|
output = tk.Text(
|
||||||
|
body,
|
||||||
|
background="#100d0c",
|
||||||
|
foreground="#ded1c4",
|
||||||
|
insertbackground="#f8fafc",
|
||||||
|
font=("Monospace", 10),
|
||||||
|
wrap="none",
|
||||||
|
state="disabled",
|
||||||
|
padx=12,
|
||||||
|
pady=10,
|
||||||
|
relief="flat",
|
||||||
|
)
|
||||||
|
dashboard_debug_output = output
|
||||||
|
vertical = ttk.Scrollbar(body, orient="vertical", command=output.yview)
|
||||||
|
horizontal = ttk.Scrollbar(body, orient="horizontal", command=output.xview)
|
||||||
|
output.configure(yscrollcommand=vertical.set, xscrollcommand=horizontal.set)
|
||||||
|
output.grid(row=0, column=0, sticky="nsew")
|
||||||
|
vertical.grid(row=0, column=1, sticky="ns")
|
||||||
|
horizontal.grid(row=1, column=0, sticky="ew")
|
||||||
|
window.protocol("WM_DELETE_WINDOW", lambda: set_dashboard_debug(False))
|
||||||
|
|
||||||
|
def set_dashboard_debug(enabled: bool) -> None:
|
||||||
|
nonlocal show_dashboard_debug, last_dashboard_debug_render
|
||||||
|
show_dashboard_debug = enabled
|
||||||
|
last_dashboard_debug_render = ""
|
||||||
|
if enabled:
|
||||||
|
refresh_dashboard_debug_disk_state()
|
||||||
|
ensure_dashboard_debug_window()
|
||||||
|
assert dashboard_debug_window is not None
|
||||||
|
dashboard_debug_window.deiconify()
|
||||||
|
dashboard_debug_window.lift()
|
||||||
|
dashboard_debug_window.focus_set()
|
||||||
|
else:
|
||||||
|
if dashboard_debug_window is not None and dashboard_debug_window.winfo_exists():
|
||||||
|
dashboard_debug_window.withdraw()
|
||||||
render_server_status()
|
render_server_status()
|
||||||
|
update_dashboard_debug_view(force=True)
|
||||||
|
|
||||||
|
def toggle_dashboard_debug(_event: object | None = None) -> str:
|
||||||
|
set_dashboard_debug(not show_dashboard_debug)
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
root.bind(
|
root.bind_all(
|
||||||
SERVER_POPULATION_DEBUG_KEY,
|
DASHBOARD_DEBUG_KEY,
|
||||||
toggle_server_population_debug,
|
toggle_dashboard_debug,
|
||||||
add="+",
|
add="+",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2320,7 +2842,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
*,
|
*,
|
||||||
update_activity: bool = True,
|
update_activity: bool = True,
|
||||||
) -> GameInstallationStatus:
|
) -> GameInstallationStatus:
|
||||||
nonlocal official_snapshot
|
nonlocal official_snapshot, latest_installation_status
|
||||||
if snapshot is not None:
|
if snapshot is not None:
|
||||||
official_snapshot = snapshot
|
official_snapshot = snapshot
|
||||||
installation = inspect_game_installation(
|
installation = inspect_game_installation(
|
||||||
@@ -2332,6 +2854,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
latest_installation_status = installation
|
||||||
additional_client_ready = game_clients.running_count > 0 and installation.installed
|
additional_client_ready = game_clients.running_count > 0 and installation.installed
|
||||||
client_activity_text.set(game_client_activity_text(game_clients.running_count))
|
client_activity_text.set(game_client_activity_text(game_clients.running_count))
|
||||||
play_button.configure(
|
play_button.configure(
|
||||||
@@ -2463,12 +2986,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
events.put(("patches", fetch_patch_notes(paths.patches_cache)))
|
events.put(("patches", fetch_patch_notes(paths.patches_cache)))
|
||||||
|
|
||||||
def server_status_worker() -> None:
|
def server_status_worker() -> None:
|
||||||
try:
|
events.put(("server_status_check", perform_server_status_check()))
|
||||||
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:
|
def version_worker() -> None:
|
||||||
try:
|
try:
|
||||||
@@ -2554,6 +3072,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
refresh_runtime_compatibility(update_activity=returncode == 0)
|
refresh_runtime_compatibility(update_activity=returncode == 0)
|
||||||
else:
|
else:
|
||||||
refresh_primary_action(update_activity=returncode == 0)
|
refresh_primary_action(update_activity=returncode == 0)
|
||||||
|
if show_dashboard_debug:
|
||||||
|
refresh_dashboard_debug_disk_state()
|
||||||
|
update_dashboard_debug_view(force=True)
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
status.set(f"Operation failed (exit {returncode})")
|
status.set(f"Operation failed (exit {returncode})")
|
||||||
progress_text.set("See the activity log for details.")
|
progress_text.set("See the activity log for details.")
|
||||||
@@ -2596,6 +3117,14 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
message = value.get("message")
|
message = value.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_supervisor_started"
|
||||||
|
and isinstance(value, tuple)
|
||||||
|
and len(value) == 2
|
||||||
|
and isinstance(value[0], int)
|
||||||
|
and isinstance(value[1], int)
|
||||||
|
):
|
||||||
|
game_clients.attach_supervisor(value[0], value[1])
|
||||||
elif (
|
elif (
|
||||||
kind == "game_operation_status"
|
kind == "game_operation_status"
|
||||||
and isinstance(value, tuple)
|
and isinstance(value, tuple)
|
||||||
@@ -2610,6 +3139,12 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
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):
|
||||||
|
launch_settings = client_launch_settings.get(value)
|
||||||
|
if launch_settings is not None:
|
||||||
|
client_runtime_debug[value] = inspect_runtime_debug(
|
||||||
|
paths,
|
||||||
|
launch_settings,
|
||||||
|
)
|
||||||
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):
|
||||||
@@ -2625,6 +3160,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
):
|
):
|
||||||
client_id, returncode = value
|
client_id, returncode = value
|
||||||
phase = game_clients.finish(client_id)
|
phase = game_clients.finish(client_id)
|
||||||
|
client_runtime_debug.pop(client_id, None)
|
||||||
|
client_launch_settings.pop(client_id, None)
|
||||||
game_window_active = game_clients.running_count > 0
|
game_window_active = game_clients.running_count > 0
|
||||||
append_log(f"Client {client_id} exited with status {returncode}.")
|
append_log(f"Client {client_id} exited with status {returncode}.")
|
||||||
if phase == "starting":
|
if phase == "starting":
|
||||||
@@ -2645,28 +3182,28 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
progress_text.set("Click Play to start a new client.")
|
progress_text.set("Click Play to start a new client.")
|
||||||
root.deiconify()
|
root.deiconify()
|
||||||
root.lift()
|
root.lift()
|
||||||
|
if show_dashboard_debug:
|
||||||
|
refresh_dashboard_debug_disk_state()
|
||||||
elif kind == "broadcast":
|
elif kind == "broadcast":
|
||||||
show_broadcast(value if isinstance(value, Broadcast) else None)
|
show_broadcast(value if isinstance(value, Broadcast) else None)
|
||||||
mark_startup_component_ready(
|
mark_startup_component_ready(
|
||||||
"broadcast",
|
"broadcast",
|
||||||
"Broadcast ready",
|
"Broadcast ready",
|
||||||
)
|
)
|
||||||
elif (
|
elif kind == "server_status_check" and isinstance(
|
||||||
kind == "server_status"
|
value,
|
||||||
and isinstance(value, tuple)
|
ServerStatusCheck,
|
||||||
and all(isinstance(item, RealmServerStatus) for item in value)
|
|
||||||
):
|
):
|
||||||
show_server_status(value)
|
show_server_status(value)
|
||||||
|
if value.error is not None:
|
||||||
|
append_log(f"Server status check unavailable: {value.error}")
|
||||||
mark_startup_component_ready(
|
mark_startup_component_ready(
|
||||||
"server_status",
|
"server_status",
|
||||||
"Server status ready",
|
(
|
||||||
)
|
"Server status ready"
|
||||||
elif kind == "server_status_error" and isinstance(value, str):
|
if value.error is None
|
||||||
show_server_status(None)
|
else "Server status check complete"
|
||||||
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):
|
elif kind == "news" and isinstance(value, tuple):
|
||||||
show_news(value)
|
show_news(value)
|
||||||
@@ -2773,6 +3310,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
if speed_text.get() != "0 B/s" and transfer_rate.is_idle():
|
if speed_text.get() != "0 B/s" and transfer_rate.is_idle():
|
||||||
speed_text.set("0 B/s")
|
speed_text.set("0 B/s")
|
||||||
update_elapsed_indicator()
|
update_elapsed_indicator()
|
||||||
|
update_server_status_debug_text()
|
||||||
|
update_dashboard_debug_view()
|
||||||
poll_delay = (
|
poll_delay = (
|
||||||
ACTIVE_POLL_INTERVAL_MILLISECONDS
|
ACTIVE_POLL_INTERVAL_MILLISECONDS
|
||||||
if not startup_transition_started
|
if not startup_transition_started
|
||||||
@@ -2853,7 +3392,19 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
parent=root,
|
parent=root,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
launch_settings: Settings | None = None
|
||||||
|
runtime_debug: RuntimeDebugInfo | None = None
|
||||||
|
try:
|
||||||
|
loaded_launch_settings = load_settings(paths)
|
||||||
|
except (OSError, ValueError) as settings_error:
|
||||||
|
append_log(f"Could not inspect launch runtime settings: {settings_error}")
|
||||||
|
else:
|
||||||
|
launch_settings = loaded_launch_settings
|
||||||
|
runtime_debug = inspect_runtime_debug(paths, loaded_launch_settings)
|
||||||
client_id = game_clients.begin()
|
client_id = game_clients.begin()
|
||||||
|
if runtime_debug is not None and launch_settings is not None:
|
||||||
|
client_runtime_debug[client_id] = runtime_debug
|
||||||
|
client_launch_settings[client_id] = launch_settings
|
||||||
active_game_launch_id = client_id
|
active_game_launch_id = client_id
|
||||||
run_operation(
|
run_operation(
|
||||||
f"Starting PristonTale EU {realm_value.get()} client {client_id}…",
|
f"Starting PristonTale EU {realm_value.get()} client {client_id}…",
|
||||||
@@ -3077,6 +3628,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
)
|
)
|
||||||
status.set("Settings saved. They will be used on the next game launch.")
|
status.set("Settings saved. They will be used on the next game launch.")
|
||||||
progress_text.set("Open Settings again at any time to adjust the launcher.")
|
progress_text.set("Open Settings again at any time to adjust the launcher.")
|
||||||
|
if show_dashboard_debug:
|
||||||
|
refresh_dashboard_debug_disk_state()
|
||||||
|
update_dashboard_debug_view(force=True)
|
||||||
close_settings()
|
close_settings()
|
||||||
|
|
||||||
display_card = ttk.LabelFrame(
|
display_card = ttk.LabelFrame(
|
||||||
@@ -3224,6 +3778,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
# Closing the window therefore minimizes the dashboard until all clients
|
# Closing the window therefore minimizes the dashboard until all clients
|
||||||
# and any active launcher operation have finished.
|
# and any active launcher operation have finished.
|
||||||
if operation_active or game_clients.active_count:
|
if operation_active or game_clients.active_count:
|
||||||
|
set_dashboard_debug(False)
|
||||||
root.iconify()
|
root.iconify()
|
||||||
return
|
return
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user