UI/HelpOverlay.razor

A Razor UI component for an in-game Help overlay. It renders controls and tips, toggles visibility on the configured "Help" input, shows on first run by checking a local flag file, and persists dismiss state to FileSystem.Data.

File Access
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace VehicleProto

@* First-run controls / help overlay (public UX) — toggled by I ("Help"). Auto-shows ONCE on a
   fresh clone (dismiss persisted to FileSystem.Data), reopenable any time with I. F1/Esc are stolen
   by the s&box editor/host, so Help rides a letter key. All binds are the REAL Input.config values
   (keyboard + gamepad), not guessed. Cursor-interactive while open.

   GLYPH-CORRUPTION DISCIPLINE: computed text nodes can render blank or as solid blocks, and glyph
   corruption is cumulative in the text-run count — so keep the text-run count low and NEVER mix a
   bare text node with an element child in one flex row (that corrupted the descriptions to solid
   blocks on the first cut) — every text is wrapped in a <span>, key labels are single chips, and the
   gamepad binds are folded into one footer line. Verified clean on a play-mode screenshot. *@

<root>
	@if ( UiState.HelpOpen )
	{
		<div class="backdrop">
			<div class="card">
				<div class="head">
					<span class="title">Vehicle Prototyping</span>
					<span class="sub">A free s&box driving field kit.</span>
				</div>

				<span class="section">Drive</span>
				<div class="rows">
					<div class="row"><span class="key">W A S D</span><span class="d">Throttle, brake &amp; steer</span></div>
					<div class="row"><span class="key">Space</span><span class="d">Handbrake</span></div>
					<div class="row"><span class="key">G</span><span class="d">Auto / manual gearbox</span></div>
					<div class="row"><span class="key">E / Q</span><span class="d">Shift up / down (manual)</span></div>
					<div class="row"><span class="key">R</span><span class="d">Reset / unflip the car</span></div>
					<div class="row"><span class="key">Mouse</span><span class="d">Orbit look · wheel zooms</span></div>
				</div>

				<span class="section">Panels</span>
				<div class="rows">
					<div class="row"><span class="key">Tab</span><span class="d">Change car · respawn</span></div>
					<div class="row"><span class="key">T</span><span class="d">Live tuning dials</span></div>
						<div class="row"><span class="key">L</span><span class="d">Telemetry overlay</span></div>
					<div class="row"><span class="key">H</span><span class="d">Hide / show the HUD</span></div>
					<div class="row"><span class="key">I</span><span class="d">Reopen this help</span></div>
				</div>

				<div class="footwrap">
					<span class="foot">Gamepad: left stick · RT/LT pedals · Start menu</span>
					<span class="foot">A handbrake · R1/L1 shift · X reset · d-pad switch car</span>
					<span class="foot">@BuildLine</span>
				</div>
				<div class="btn" onclick=@Dismiss>Got it — let's drive</div>
			</div>
		</div>
	}
</root>

@code {
	// FileSystem.Data flag file — the sanctioned, whitelisted persistence path for
	// small local UI state that should survive between sessions.
	const string SeenFlagFile = "vp_seen_help.flag";

	// Version + build line, resolved to ONE string: interpolating multiple @values inline with
	// literal text in a single fragment renders with the text runs interleaved — a whole-line
	// string property rendered as the element's sole child is the safe pattern.
	static string BuildLine => $"v{VpBuild.Version} · build {VpBuild.PublishStamp} · {VpBuild.PublishStampNote}";

	protected override void OnStart()
	{
		// First run on a fresh clone: no flag yet → show the overlay once. Reopen is always I.
		if ( !HasSeen() )
		{
			UiState.HelpOpen = true;
			Mouse.Visibility = MouseVisibility.Visible;
		}
	}

	protected override void OnUpdate()
	{
		if ( Input.Pressed( "Help" ) )
		{
			UiState.HelpOpen = !UiState.HelpOpen;
			Mouse.Visibility = UiState.HelpOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
		}

		if ( UiState.HelpOpen )
			Mouse.Visibility = MouseVisibility.Visible;
	}

	void Dismiss()
	{
		UiState.HelpOpen = false;
		Mouse.Visibility = UiState.AnyCursorModalOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
		MarkSeen();
		StateHasChanged();
	}

	static bool HasSeen()
	{
		try { return FileSystem.Data.FileExists( SeenFlagFile ); }
		catch { return false; }
	}

	static void MarkSeen()
	{
		try
		{
			if ( !FileSystem.Data.FileExists( SeenFlagFile ) )
				FileSystem.Data.WriteAllText( SeenFlagFile, "1" );
		}
		catch { /* persistence is best-effort — a failed write just reshows the overlay next launch */ }
	}

	protected override int BuildHash() => System.HashCode.Combine( UiState.HelpOpen );
}