Passa al contenuto principale

Preflight Guardrails

Status: design — implementation tracked in Linear EC-399 (parent) and EC-400 / EC-401 / EC-402 / EC-403 (children).

Preflight guardrails make setup self-correcting so a contributor can't silently record a session that won't be usable. Problems are surfaced ahead of time — at game load — rather than discovered after a wasted session. Two behaviours:

  1. Recording outside a groupwarn but allow. A clear, dismissible warning; never a hard block.
  2. On game load → a preflight surface that flags wrong resolution, poor FPS, mic not picking up anything, and group membership, so each can be fixed before play.

Design principle: reuse the environment-check path

The launcher already has the exact mechanism this needs. Each guardrail surfaced in the launcher is expressed as a warning-severity CoreUiErrorDirective and flows through the existing environment-check pipeline — no new transport, no new UI shell. Hard blockers stay reserved for true hard-stops (auth, non-16:9 window at record time, disabled mic per EC-306).

Exception (EC-400): guardrails that the player must see while in-game can't use the launcher banner (it's occluded) — they surface through the core's in-game overlay instead: a persistent PreflightOverlay panel (a sibling of the stats / mic / gpu HUDs, toggled with F8), shown only while a check is failing. That path is core-rendered, so it needs a small transport of its own: the localized row labels are pushed to the core via POST /api/overlay/preflight-labels. See §1 and Boundary consistency.

Existing building blocks

PieceLocationRole
Environment-check IPCplayroll-ui GET_ENVIRONMENT_CHECK (src/main/ipc/handlers.ts)Returns { blockers, warnings } of CoreUiErrorDirective
useEnvironmentCheckplayroll-ui src/renderer/hooks/useEnvironmentCheck.tsSplits into hasBlockers + warning list
EnvironmentCheckModalplayroll-ui src/renderer/features/launcher/components/Hard-stop modal for blockers
EnvironmentWarningBannersame dirStacked dismissible warning cards — the warn-but-allow surface
system-checkplayroll-ui src/main/system-check/Probes os/cpu/ram/disk/gpu → ok | warning | blocking
resolution-probeplayroll-ui src/main/resolution-probe/Reads game config → effective resolution (file-read only, anti-cheat-safe)
fps_profilerplayroll-cpp include/service/fps_profiler.hppIn-capture FPS warning events
Mic levelplayroll-cpp include/service/mic_overlay.hpp (MicData{muted, vuLevel})Live VU level via MicProvider
Lobby (group) stateplayroll-cpp lobby_state.hpp + playroll-ui src/main/lobby/lobby-manager.tsActive lobby in both processes; lobby.active in renderer

The four guardrails

1. Group membership (EC-400) — cpp + ui

lobby.active is known in both processes. A LobbyGameMismatch blocker already exists (recording the wrong game for the lobby), but nothing signals "you're recording solo".

Surface — a persistent in-game preflight panel, not the launcher banner. The guardrail must be visible while the player is in the game (both before and during recording) — which is exactly when the launcher is occluded by the fullscreen game, so a launcher-chrome banner would render where nobody is looking. It is therefore delivered as a row in a new in-game preflight overlay (PreflightOverlay), a persistent click-through Direct2D panel that is a sibling of the stats / mic / gpu HUDs: anchored top-left of the game window, toggled with the same F8 overlay cluster, and shown only while at least one check is failing (it hides itself when everything is in order). Each guardrail is one row with a severity marker (! warning / error) + a short label. EC-400 ships the group row; EC-401 / EC-402 / EC-403 add resolution / mic / fps rows to the same panel. This is the shared preflight surface — not a separate per-guardrail notification.

Overlay convergence (now the default). Since the cross-platform overlay shipped, the native Direct2D PreflightOverlay is suppressed by default — the Electron overlay's Alerts widget surfaces these guardrails instead, derived renderer-side from the recorder status (windowAspectOk, mic muted/silent, perf.severity, lobby/solo). The native panel and the POST /api/overlay/preflight-labels label transport described below remain the legacy path, reachable with PLAYROLL_LEGACY_OVERLAY=1 (Windows, for diagnosis). The guardrail logic is unchanged; only the surface that paints it moved from the core's D2D panel to the Electron overlay.

Why a panel, not a record-start toast. An earlier iteration fired a one-shot notification at record-start; it was rejected because record-start is already too late to fix anything, and a transient toast can't be re-checked. A persistent panel shown from game-load lets the player see and fix the issue before they record, and clears itself the moment they do (e.g. they join a lobby).

Mechanism — core-driven, UI-localized. The core owns the overlay and already holds the lobby context, so its preflight-overlay provider checks lobby.active on each tick and emits the group row when solo. The row's copy is owned by the UI (i18n): the renderer pushes the localized checklist labels (checkId → text) to the core via POST /api/overlay/preflight-labels, re-pushing on connect / language-change; the core caches them and looks each row up by id. A missing label (older UI) simply omits that row — never English-fallback.

Consequence (resolved). Recording solo has no downside: a solo session validates and pays out exactly like a grouped one. Grouping only adds an optional multi-POV view. The copy therefore frames grouping as a bonus, never solo as a penalty.

2. Wrong resolution (EC-401) — UI only

windowAspectBlocked blocks recording at start time for non-16:9, but only when the user hits record. resolution-probe already knows the game's effective resolution at detection time. A new resolution entry in the system-check report emits an early, explanatory warning (non-16:9, or below a resolution floor) so it's fixed before play. The record-time 16:9 hard block remains the backstop.

3. Mic not picking up (EC-402) — cpp + ui

MicData.vuLevel gives a live level; VoicePrerequisiteStartGate (EC-306) already blocks a disabled mic. Nothing detects an enabled-but-silent mic (wrong device, hardware mute, zero gain). A short cpp silence detector samples vuLevel over a rolling window after game load and sets a micSilent status flag; the UI maps it to a dismissible warning. Warn-but-allow.

4. Poor FPS (EC-403) — cpp + ui

True FPS cannot be measured before capture starts, so this is delivered in two parts: a predictive game-load warning (resolution + GPU + target FPS heuristic, worded as "may"), and an early in-capture warning that surfaces the existing fps_profiler WarningRaised event (≈10s warmup) into the banner while the session is still salvageable.

Boundary consistency

Real-time "FPS dropping / audio device changed" alerts belong to the in-game status island, not the Notifications tab (per the notification delivery flow boundary contract).

Surface follows visibility, not category. The original framing put all four guardrails in the launcher's EnvironmentWarningBanner ("preflight = launcher chrome"). EC-400 corrected that: a guardrail that fires at or after game load must surface where the player actually is — the in-game overlay — because the launcher is occluded by the (usually fullscreen) game by then. The launcher banner remains the right home only for guardrails surfaced before launch, while the player is still looking at the launcher. The same occlusion logic likely applies to EC-401 / EC-402 / EC-403 (which also fire at or after game load); revisit their surface when they are built rather than defaulting them to the banner.

Open questions

  • Resolution: block only on non-16:9 (current) or also warn below a floor (e.g. 720p)? What is the supported set?
  • FPS: target FPS and the "poor" cutoff? fps_profiler defaults to 95% of target — reuse it?
  • Mic: silence window length and the level threshold that counts as "silent"?
  • Group: the real consequence of recording solo, needed for honest copy. Resolved (EC-400): none — a solo recording validates and pays out the same; grouping only adds an optional multi-POV view. Copy frames grouping as a bonus, never solo as a penalty.