SoundBoardPanel.razor

A Blazor-style Razor UI component for a soundboard panel in s&box. It renders the UI for boards/tabs, search, pad grid, voice meter, controls (loop, coop, kill), handles input (click, wheel), caching/filtering of pads, and reacts to various manager services for playback, coop, preload and achievements.

NetworkingFile Access
@using System;
@using System.Collections.Generic;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace SSoundboard

<root class="sb @LayoutClass @(SoundBoardManager.ActiveVoiceCount > 0 ? "has-voices" : "")">
	<div class="sb-shell">
		<header class="sb-topbar">
			<div class="sb-brand-block">
				<span class="sb-status-led @(SoundBoardManager.ActiveVoiceCount > 0 ? "is-live" : "")"></span>
				<div class="sb-brand-copy">
					<span class="sb-title">S&amp;Soundboard</span>
				</div>
			</div>

			<div class="sb-voice-chip">
				<div class="sb-voice-chip-head">
					<span class="sb-voice-chip-label">VOICES</span>
					<span class="sb-voice-chip-value" style="color: @ActiveVoiceColor">@SoundBoardManager.ActiveVoiceCount</span>
				</div>
				<div class="sb-voice-meter-stack">
					@for ( var tier = 0; tier < VoiceMeterTierCount; tier++ )
					{
						var state = GetVoiceMeterTier( tier );
						<div class="sb-voice-meter-tier">
							<div class="sb-voice-meter-fill @(state.IsActive ? "is-active" : "") @(state.IsFull ? "is-full" : "") @(state.IsChaos ? "is-chaos" : "")"
								 style="width: @(state.FillPercent)%; background-color: @state.Color"></div>
						</div>
					}
				</div>
			</div>
		</header>

		<div class="sb-tabs-bar">
			<div class="sb-tabs-arrow @(CanScrollTabsLeft ? "" : "is-disabled")"
				 onmousedown="@(() => ScrollTabs( -1 ))">‹</div>
			<div @ref="_tabsNav" class="sb-tabs-scroll">
				<div @ref="_tabsTrack" class="sb-tabs-track"
					 style="margin-left: @(-_tabsScrollX)px">
					@if ( IsSearchTab )
					{
						<div class="sb-tab-chip is-active is-search">
							<span class="sb-tab-chip-icon">⌕</span>
							<span class="sb-tab-chip-label">SEARCH</span>
						</div>
					}
					@foreach ( var board in SoundBoardData.Boards )
					{
						<div class="sb-tab-chip @(!IsSearchTab && ActiveBoardId == board.Id ? "is-active" : "")"
							 onmousedown="@(() => SetBoard( board.Id ))">
							<span class="sb-tab-chip-icon">@board.Icon</span>
							<span class="sb-tab-chip-label">@board.Label</span>
						</div>
					}
				</div>
			</div>
			<div class="sb-tabs-arrow @(CanScrollTabsRight ? "" : "is-disabled")"
				 onmousedown="@(() => ScrollTabs( 1 ))">›</div>
		</div>

		<div class="sb-search-bar">
			<span class="sb-search-icon">⌕</span>
			<TextEntry class="sb-search-input"
					   Placeholder="Search all tabs… e.g. dolphin, ocean"
					   Text=@SearchQuery
					   OnTextEdited=@OnSearchEdited />
			@if ( HasSearchQuery )
			{
				<div class="sb-search-clear" onmousedown="@ClearSearch">✕</div>
				<span class="sb-search-count">@ActivePadCount match@(ActivePadCount == 1 ? "" : "es")</span>
			}
			else
			{
				<span class="sb-search-count">@BoardPadCount</span>
			}
		</div>

		<section @ref="_boardArea" class="sb-board">
			<div @ref="_boardScroll" class="sb-board-scroll">
				<div class="sb-pad-content" style="min-height: @(PadGridMinHeight)px">
					@if ( ActivePadCount == 0 )
					{
						<div class="sb-empty">
							@if ( HasSearchQuery )
							{
								<span class="sb-empty-title">No matches</span>
								<span class="sb-empty-body">No sounds match “@SearchQuery.Trim()” across all tabs</span>
							}
							else
							{
								<span class="sb-empty-title">No sounds</span>
								<span class="sb-empty-body">This board is empty</span>
							}
						</div>
					}
					else
					{
						<div class="tile-grid sb-pad-grid">
							@foreach ( var pad in ActivePads )
							{
								var remoteName = "";
								var remoteSteamId = 0L;
								var hasRemote = false;
								if ( pad is not null && SoundBoardCoop.TryGetHighlight( pad.SoundId, out var remote ) )
								{
									hasRemote = true;
									remoteName = remote.PlayerName ?? "";
									remoteSteamId = remote.SteamId;
								}
								<div class="tile @(GetPadClass( pad, hasRemote ))"
									 onmousedown="@(() => PlayPad( pad ))">
									<div class="tile-swatch" style="background-color: @(pad?.Color ?? "#ede19e")">
										@if ( hasRemote && remoteSteamId != 0 )
										{
											<div class="tile-remote-avatar"
												 style="background-image: url( avatar:@remoteSteamId )"></div>
										}
										@if ( hasRemote && !string.IsNullOrWhiteSpace( remoteName ) )
										{
											<div class="tile-remote-name">@ShortPlayerName( remoteName )</div>
										}
										<div class="tile-vignette"></div>
									</div>
									<div class="tile-label">@ShortLabel( pad?.Label )</div>
								</div>
							}
						</div>
					}
				</div>
			</div>
		</section>

		<footer class="sb-controls">
			<div class="sb-toggle-row">
				<div class="sb-toggle @(SoundBoardManager.LoopEnabled ? "is-on" : "is-off")"
					 onmousedown="@OnLoopPressed">
					LOOP
				</div>

				<div class="sb-toggle @(SoundBoardCoop.Enabled ? "is-on is-coop" : "is-off")"
					 onmousedown="@OnCoopPressed">
					@(SoundBoardCoop.Enabled ? "CO-OP" : "SINGLE")
				</div>
			</div>

			<div class="sb-now-playing">
				<div class="sb-now-playing-dot @(SoundBoardManager.ActiveVoiceCount > 0 ? "is-live" : "")"></div>
				<div class="sb-now-playing-text">@FooterStatusText</div>
			</div>

			<div class="btn danger sb-kill-btn" onmousedown="@OnKillPressed">☠ KILL ALL</div>
		</footer>
	</div>

	@if ( SoundBoardAchievements.ToastVisible )
	{
		<div class="sb-ach">
			<div class="sb-ach-icon">🔥</div>
			<div class="sb-ach-copy">
				<div class="sb-ach-kicker">ACHIEVEMENT UNLOCKED</div>
				<div class="sb-ach-title">@SoundBoardAchievements.ToastTitle</div>
				<div class="sb-ach-body">@SoundBoardAchievements.ToastBody</div>
			</div>
		</div>
	}
</root>

@code
{
	const int TileLabelHeight = 42;
	const int TileBorder = 2;
	const int GridGap = 12;
	const int GridPadTop = 4;
	const int GridPadBottom = 24;
	const int VoicesPerTier = 7;
	const int VoiceMeterTierCount = 4;

	const string SearchBoardId = "search";

	string ActiveBoardId { get; set; } = "arnold";
	string SearchQuery { get; set; } = "";
	string _boardBeforeSearch = "arnold";
	int _screenWidth;
	int _screenHeight;
	Panel _boardArea;
	Panel _boardScroll;
	Panel _tabsNav;
	Panel _tabsTrack;

	// Manual horizontal offset for category tabs (ScrollSize is unreliable in s&box UI).
	float _tabsScrollX;
	const float TabsScrollStep = 200f;
	const float TabChipEstimateWidth = 128f;

	// Cached pad list for the active tab (meme tab can be 1000+ pads).
	string _padsBoardId;
	IReadOnlyList<SoundPad> _boardPads = Array.Empty<SoundPad>();

	// Search / filtered view cache.
	string _filterBoardId;
	string _filterQuery;
	IReadOnlyList<SoundPad> _filteredPads = Array.Empty<SoundPad>();

	bool HasSearchQuery => !string.IsNullOrWhiteSpace( SearchQuery );
	bool IsSearchTab => HasSearchQuery || ActiveBoardId == SearchBoardId;

	bool CanScrollTabsLeft => _tabsScrollX > 1f;
	bool CanScrollTabsRight => _tabsScrollX < GetTabsMaxScroll() - 1f;

	IReadOnlyList<SoundPad> BoardPads
	{
		get
		{
			if ( IsSearchTab )
				return Array.Empty<SoundPad>(); // search uses global pool, not a board

			if ( _padsBoardId != ActiveBoardId || _boardPads is null )
			{
				_padsBoardId = ActiveBoardId;
				_boardPads = SoundBoardData.GetPads( ActiveBoardId );
				InvalidateFilterCache();
			}

			return _boardPads;
		}
	}

	IReadOnlyList<SoundPad> ActivePads
	{
		get
		{
			EnsureFilterCache();
			return _filteredPads;
		}
	}

	int BoardPadCount => IsSearchTab ? SoundBoardData.AllUniquePads.Count : BoardPads.Count;
	int ActivePadCount => ActivePads.Count;

	Panel BoardViewport => _boardScroll;

	int TileWidth => LayoutClass switch
	{
		"is-tight" => 88,
		"is-compact" => 96,
		"is-medium" => 104,
		"is-wide" => 120,
		_ => 112
	};

	int TileSwatchHeight => LayoutClass switch
	{
		"is-tight" => 50,
		"is-compact" => 56,
		_ => 64
	};

	int ShellPaddingX => LayoutClass switch
	{
		"is-tight" => 20,
		"is-compact" => 28,
		"is-medium" => 40,
		_ => 48
	};

	int PadColumns
	{
		get
		{
			var innerWidth = Math.Max( _screenWidth - ShellPaddingX - 8, TileWidth );
			return Math.Max( 1, (innerWidth + GridGap) / (TileWidth + GridGap) );
		}
	}

	int PadGridMinHeight
	{
		get
		{
			var count = ActivePadCount;
			if ( count == 0 )
				return 0;

			var rows = (count + PadColumns - 1) / PadColumns;
			var tileHeight = TileSwatchHeight + TileLabelHeight + TileBorder;
			return GridPadTop + GridPadBottom + rows * tileHeight + (rows - 1) * GridGap;
		}
	}

	string LayoutClass
	{
		get
		{
			if ( _screenWidth < 720 )
				return "is-tight";
			if ( _screenWidth < 1000 )
				return "is-compact";
			if ( _screenWidth < 1400 )
				return "is-medium";
			return "is-wide";
		}
	}

	string ActiveVoiceColor => SoundBoardManager.ActiveVoiceCount == 0
		? "#7cf9ff"
		: TierColor( ActiveVoiceTierIndex );

	int ActiveVoiceTierIndex => SoundBoardManager.ActiveVoiceCount == 0
		? 0
		: Math.Min( (SoundBoardManager.ActiveVoiceCount - 1) / VoicesPerTier, VoiceMeterTierCount - 1 );

	string FooterStatusText
	{
		get
		{
			var status = SoundBoardManager.StatusText;
			if ( !string.IsNullOrWhiteSpace( status ) && status != "READY · PRESS ANY BUTTON" )
				return status;

			if ( SoundBoardManager.LoopEnabled )
				return "LOOP ON · PRESS A PAD";

			if ( SoundBoardCoop.Enabled )
			{
				var hint = SoundBoardCoop.StatusHint();
				return string.IsNullOrWhiteSpace( hint )
					? "CO-OP ON · HEAR EVERYONE'S PRESSES"
					: $"CO-OP ON · {hint.ToUpperInvariant()}";
			}

			if ( !SoundPreloadService.IsInitialized )
				return "STARTING UP...";

			if ( !SoundPreloadService.IsStartupComplete )
				return $"LOADING ARNOLD · {SoundPreloadService.LoadedCount} / {SoundBoardData.ArnoldPads.Count}";

			return "READY · PRESS ANY BUTTON";
		}
	}

	readonly record struct VoiceMeterTierState( int FillPercent, string Color, bool IsActive, bool IsFull, bool IsChaos );

	VoiceMeterTierState GetVoiceMeterTier( int tierIndex )
	{
		var count = SoundBoardManager.ActiveVoiceCount;
		var tierStart = tierIndex * VoicesPerTier;
		var tierEnd = (tierIndex + 1) * VoicesPerTier;
		var color = TierColor( tierIndex );

		if ( count <= tierStart )
			return new VoiceMeterTierState( 0, color, false, false, false );

		if ( count >= tierEnd )
		{
			var chaos = tierIndex == VoiceMeterTierCount - 1 && count > tierEnd;
			return new VoiceMeterTierState( 100, color, chaos, true, chaos );
		}

		var fill = (int)Math.Round( (count - tierStart) * 100f / VoicesPerTier );
		return new VoiceMeterTierState( fill, color, true, false, false );
	}

	static string TierColor( int tierIndex ) => tierIndex switch
	{
		0 => "#7cf9ff",
		1 => "#8b5cff",
		2 => "#ffd34a",
		3 => "#ff5d6a",
		_ => "#ff5d6a"
	};

	protected override void OnStart()
	{
		_screenWidth = (int)Screen.Width;
		_screenHeight = (int)Screen.Height;
		SoundPlayTracker.EnsureLoaded();
		// Local→platform catch-up (fifty_deep, click_*, etc.). Tick handles delayed retry.
		SoundBoardAchievements.ResyncPlatformFromCookies();
		SoundBoardManager.StateChanged += OnStateChanged;
		SoundPreloadService.Changed += OnStateChanged;
		SoundBoardAchievements.StateChanged += OnStateChanged;
		SoundBoardCoop.StateChanged += OnStateChanged;
	}

	protected override void OnDestroy()
	{
		SoundBoardManager.StateChanged -= OnStateChanged;
		SoundPreloadService.Changed -= OnStateChanged;
		SoundBoardAchievements.StateChanged -= OnStateChanged;
		SoundBoardCoop.StateChanged -= OnStateChanged;
	}

	protected override void OnUpdate()
	{
		var width = (int)Screen.Width;
		var height = (int)Screen.Height;
		if ( width != _screenWidth || height != _screenHeight )
		{
			_screenWidth = width;
			_screenHeight = height;
			ClampTabsScroll();
			StateHasChanged();
		}

		// Backup tick (Bootstrap also ticks; manager de-dupes per frame).
		SoundBoardManager.Tick();
		SoundBoardAchievements.Tick();
		SoundBoardCoop.Tick();
		TryWheelScrollBoard();
		TryWheelScrollTabs();
	}

	void OnStateChanged() => StateHasChanged();

	void SetBoard( string boardId )
	{
		if ( string.IsNullOrWhiteSpace( boardId ) )
			return;

		// Leaving search → clear query and open the chosen category.
		if ( HasSearchQuery )
		{
			SearchQuery = "";
			_boardBeforeSearch = boardId;
		}

		if ( ActiveBoardId == boardId && !IsSearchTab )
			return;

		ActiveBoardId = boardId;
		_padsBoardId = null;
		InvalidateFilterCache();
		ResetBoardScroll();
		StateHasChanged();
	}

	void OnSearchEdited( string value )
	{
		var next = value ?? "";
		var had = HasSearchQuery;

		// Entering search: remember which category we left.
		if ( !had && !string.IsNullOrWhiteSpace( next ) && ActiveBoardId != SearchBoardId )
			_boardBeforeSearch = ActiveBoardId;

		SearchQuery = next;

		if ( HasSearchQuery )
			ActiveBoardId = SearchBoardId;
		else if ( ActiveBoardId == SearchBoardId )
			ActiveBoardId = string.IsNullOrWhiteSpace( _boardBeforeSearch ) ? "arnold" : _boardBeforeSearch;

		_padsBoardId = null;
		InvalidateFilterCache();
		ResetBoardScroll();
		// Jump to start of tab strip so the SEARCH chip is visible.
		if ( HasSearchQuery )
			_tabsScrollX = 0f;
		StateHasChanged();
	}

	void ClearSearch()
	{
		if ( string.IsNullOrEmpty( SearchQuery ) && ActiveBoardId != SearchBoardId )
			return;

		SearchQuery = "";
		ActiveBoardId = string.IsNullOrWhiteSpace( _boardBeforeSearch ) ? "arnold" : _boardBeforeSearch;
		_padsBoardId = null;
		InvalidateFilterCache();
		ResetBoardScroll();
		StateHasChanged();
	}

	void InvalidateFilterCache()
	{
		_filterBoardId = null;
		_filterQuery = null;
	}

	void EnsureFilterCache()
	{
		var query = SearchQuery ?? "";
		var boardKey = IsSearchTab ? SearchBoardId : ActiveBoardId;

		if ( _filterBoardId == boardKey
			&& string.Equals( _filterQuery, query, StringComparison.Ordinal )
			&& _filteredPads is not null )
		{
			return;
		}

		_filterBoardId = boardKey;
		_filterQuery = query;

		// Global SEARCH tab: all boards, multi-term OR (comma-separated).
		if ( !string.IsNullOrWhiteSpace( query ) )
		{
			_filteredPads = SoundBoardData.SearchAll( query );
			return;
		}

		// Normal category browse.
		_filteredPads = BoardPads;
	}

	string GetPadClass( SoundPad pad, bool hasRemoteHighlight = false )
	{
		if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
			return hasRemoteHighlight ? "active is-remote" : "";

		// O(1) live-id lookup — avoid scanning every pad×voice each rebuild.
		if ( SoundBoardManager.IsPadPlaying( pad.SoundId ) || hasRemoteHighlight )
		{
			if ( SoundPreloadService.IsPendingPlay( pad.SoundId ) )
				return hasRemoteHighlight ? "active is-loading is-remote" : "active is-loading";
			return hasRemoteHighlight ? "active is-remote" : "active";
		}

		if ( SoundPreloadService.IsPendingPlay( pad.SoundId ) )
			return "is-loading";

		return "";
	}

	void ResetBoardScroll()
	{
		var viewport = BoardViewport;
		if ( viewport is not null && viewport.IsValid() )
			viewport.ScrollOffset = Vector2.Zero;
	}

	void PlayPad( SoundPad pad )
	{
		if ( pad is null )
			return;

		try
		{
			// Local play first for snappy feedback, then share to lobby if CO-OP is on.
			SoundBoardManager.Play( pad );

			if ( SoundBoardCoop.Enabled )
			{
				try { SoundBoardCoop.RegisterLocalPress( pad ); }
				catch ( Exception e ) { Log.Warning( $"[S&Soundboard] RegisterLocalPress: {e.Message}" ); }

				try { SoundBoardNet.SharePadPress( pad ); }
				catch ( Exception e ) { Log.Warning( $"[S&Soundboard] SharePadPress: {e.Message}" ); }
			}
		}
		catch ( Exception e )
		{
			// Last resort — never surface a raw code error from a pad click.
			Log.Warning( $"[S&Soundboard] PlayPad failed · {pad.Label}: {e.GetType().Name}: {e.Message}" );
		}
	}

	void OnLoopPressed()
	{
		SoundBoardManager.ToggleLoop();
		StateHasChanged();
	}

	void OnCoopPressed()
	{
		// Instant toggle — off stops receiving/showing lobby presses immediately.
		SoundBoardCoop.Toggle();
		StateHasChanged();
	}

	void OnKillPressed() => SoundBoardManager.KillAll();

	float GetTabsViewportWidth()
	{
		if ( _tabsNav is not null && _tabsNav.IsValid() )
		{
			var w = _tabsNav.Box.Rect.Width;
			if ( w > 8f )
				return w;
		}

		// Fallback: full width minus two arrows + padding.
		return Math.Max( 120f, _screenWidth - 100f );
	}

	float GetTabsContentWidth()
	{
		if ( _tabsTrack is not null && _tabsTrack.IsValid() )
		{
			var w = _tabsTrack.Box.Rect.Width;
			if ( w > 8f )
				return w;
		}

		// Estimate until layout measures the track (many tabs now).
		var boards = SoundBoardData.Boards?.Count ?? 0;
		return Math.Max( boards * TabChipEstimateWidth, GetTabsViewportWidth() );
	}

	float GetTabsMaxScroll()
	{
		return Math.Max( 0f, GetTabsContentWidth() - GetTabsViewportWidth() );
	}

	void ClampTabsScroll()
	{
		var max = GetTabsMaxScroll();
		if ( _tabsScrollX > max )
			_tabsScrollX = max;
		if ( _tabsScrollX < 0f )
			_tabsScrollX = 0f;
	}

	void ScrollTabs( int direction )
	{
		if ( direction == 0 )
			return;

		// Prefer engine scroll if it actually reports horizontal overflow.
		var tabs = _tabsNav;
		if ( tabs is not null && tabs.IsValid() && tabs.ScrollSize.x > 1f )
		{
			var maxX = tabs.ScrollSize.x;
			var nextEngine = Math.Clamp( tabs.ScrollOffset.x + direction * TabsScrollStep, 0f, maxX );
			tabs.ScrollOffset = new Vector2( nextEngine, 0f );
			_tabsScrollX = nextEngine;
			StateHasChanged();
			return;
		}

		// Reliable path: shift the track with margin (works when ScrollSize stays 0).
		_tabsScrollX = Math.Clamp( _tabsScrollX + direction * TabsScrollStep, 0f, GetTabsMaxScroll() );
		StateHasChanged();
	}

	bool IsPointerOverBoard()
	{
		if ( _boardArea?.HasHovered == true )
			return true;
		return BoardViewport?.HasHovered == true;
	}

	float GetMaxScrollY( Panel viewport )
	{
		var engineScroll = Math.Max( viewport.ScrollSize.y, 0f );
		if ( engineScroll > 0.5f )
			return engineScroll;

		return Math.Max( PadGridMinHeight - viewport.Box.Rect.Height, 0f );
	}

	void TryWheelScrollBoard()
	{
		var viewport = BoardViewport;
		if ( viewport is null || !viewport.IsValid() || !IsPointerOverBoard() )
			return;

		if ( _tabsNav is not null && _tabsNav.IsValid() && _tabsNav.HasHovered )
			return;

		var wheel = Input.MouseWheel;
		if ( Math.Abs( wheel.y ) < 0.01f )
			return;

		if ( viewport.TryScroll( wheel ) )
			return;

		const float wheelSpeed = 96f;
		var maxScroll = GetMaxScrollY( viewport );
		if ( maxScroll <= 0.5f )
			return;

		var nextY = Math.Clamp( viewport.ScrollOffset.y + wheel.y * wheelSpeed, 0f, maxScroll );
		viewport.ScrollOffset = new Vector2( viewport.ScrollOffset.x, nextY );
	}

	void TryWheelScrollTabs()
	{
		var tabs = _tabsNav;
		if ( tabs is null || !tabs.IsValid() || !tabs.HasHovered )
			return;

		var wheel = Input.MouseWheel;
		// Prefer horizontal wheel; fall back to vertical as "scroll tabs sideways".
		var delta = Math.Abs( wheel.x ) > 0.01f ? wheel.x : wheel.y;
		if ( Math.Abs( delta ) < 0.01f )
			return;

		if ( tabs.ScrollSize.x > 1f )
		{
			if ( tabs.TryScroll( new Vector2( delta, 0f ) ) )
			{
				_tabsScrollX = tabs.ScrollOffset.x;
				return;
			}

			const float wheelSpeed = 72f;
			var nextX = Math.Clamp( tabs.ScrollOffset.x + delta * wheelSpeed, 0f, tabs.ScrollSize.x );
			tabs.ScrollOffset = new Vector2( nextX, 0f );
			_tabsScrollX = nextX;
			return;
		}

		// Margin-offset path (same as arrow buttons).
		const float wheelStep = 80f;
		var before = _tabsScrollX;
		_tabsScrollX = Math.Clamp( _tabsScrollX + delta * wheelStep, 0f, GetTabsMaxScroll() );
		if ( Math.Abs( _tabsScrollX - before ) > 0.1f )
			StateHasChanged();
	}

	string ShortLabel( string label )
	{
		if ( string.IsNullOrWhiteSpace( label ) )
			return "???";

		var max = _screenWidth < 720 ? 9 : _screenWidth < 1000 ? 11 : _screenWidth < 1400 ? 13 : 16;
		if ( label.Length <= max )
			return label;

		return label[..(max - 1)] + "…";
	}

	string ShortPlayerName( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			return "?";

		var max = _screenWidth < 720 ? 8 : 10;
		if ( name.Length <= max )
			return name;

		return name[..(max - 1)] + "…";
	}

	protected override int BuildHash()
	{
		// Avoid O(pads) scans here — use revision counters from manager/preload.
		var hash = new HashCode();
		hash.Add( ActiveBoardId );
		hash.Add( SearchQuery );
		hash.Add( IsSearchTab );
		hash.Add( ActivePadCount );
		hash.Add( SoundBoardManager.LoopEnabled );
		hash.Add( SoundBoardManager.LoopLayerCount );
		hash.Add( SoundBoardManager.VoiceRevision );
		hash.Add( SoundBoardManager.ActiveVoiceCount );
		hash.Add( SoundBoardManager.StatusText );
		hash.Add( SoundBoardManager.LastPlayedLabel );
		hash.Add( SoundBoardCoop.Enabled );
		hash.Add( SoundBoardCoop.Revision );
		hash.Add( SoundPreloadService.LoadedCount );
		hash.Add( SoundPreloadService.IsStartupComplete );
		hash.Add( SoundPreloadService.PendingRevision );
		hash.Add( PadGridMinHeight );
		hash.Add( _screenWidth );
		hash.Add( _screenHeight );
		hash.Add( _tabsScrollX );
		hash.Add( SoundBoardAchievements.ToastVisible );
		hash.Add( SoundBoardAchievements.ToastTitle );
		return hash.ToHashCode();
	}
}