A Razor UI component for the player slots panel. It renders a fixed number of player slots, shows avatars, local/ready indicators, and opens the friends list when clicking an empty slot. It orders connections with host first then by SteamId and computes a hash for UI change detection.
@using Sandbox;
@using Sandbox.UI;
@using Machines.Components;
@namespace Machines.UI
@inherits Panel
@{
var lobbyState = LobbyFlow.Current?.State ?? LobbyState.Browsing;
var showReadyChip = lobbyState != LobbyState.Launching;
var connections = SortedConnections();
}
@for ( int i = 0; i < VisibleSlotCount; i++ )
{
var slotIndex = i;
// Stable sort so every machine renders the same order.
var connection = slotIndex < connections.Count ? connections[slotIndex] : null;
var occupied = connection != null;
var isReady = occupied && IsReady( connection );
var isLocal = occupied && connection == Connection.Local;
var steamId = connection?.SteamId;
<div @key=@($"slot-{slotIndex}-{(occupied ? (long)steamId : -1)}") class="slot slot-@(slotIndex + 1) @(occupied ? "occupied" : "empty") @(isLocal ? "is-local" : "") @(showReadyChip && isReady ? "is-ready" : "")" onclick=@(() => OnSlotClicked( slotIndex ))>
@if ( occupied )
{
<div class="avatar-wrap">
<image class="avatar" src="avatar:@(steamId)" />
<div class="avatar-fade"></div>
</div>
@if ( isLocal )
{
<div class="number">@(Input.UsingController ? "🎮" : "⌨️")</div>
}
@if ( showReadyChip && isReady )
{
<div class="ready-check">✓</div>
}
<div class="player-label">@(isLocal ? "YOU" : $"P{slotIndex + 1}")</div>
}
else
{
<div class="placeholder question">?</div>
<div class="placeholder invite">+</div>
}
</div>
}
@code
{
/// <summary>
/// Number of base empty slots always shown.
/// </summary>
public int BaseSlotCount { get; set; } = 4;
/// <summary>
/// Maximum total slots (extra slots only appear when occupied).
/// </summary>
public int MaxSlotCount { get; set; } = 8;
/// <summary>
/// Stable connection order: host first, then by SteamId.
/// </summary>
private static List<Connection> SortedConnections()
{
return Connection.All
.OrderByDescending( c => c.IsHost )
.ThenBy( c => c.SteamId.ValueUnsigned )
.ToList();
}
private static bool IsReady( Connection c )
{
var ready = LobbyFlow.Current?.Ready;
return ready != null && ready.TryGetValue( c.Id, out var r ) && r;
}
/// <summary>
/// Slots to render: base count plus any occupied extras.
/// </summary>
private int VisibleSlotCount => Math.Min( Math.Max( BaseSlotCount, Connection.All.Count ), MaxSlotCount );
private void OnSlotClicked( int slotIndex )
{
// Empty slot: open friends list to invite.
if ( slotIndex < Connection.All.Count )
return;
Game.Overlay.ShowFriendsList( new Sandbox.Modals.FriendsListModalOptions() );
}
protected override int BuildHash()
{
int h = System.HashCode.Combine( Connection.All.Count, LobbyFlow.Current?.State, VisibleSlotCount );
foreach ( var c in SortedConnections() )
{
h = System.HashCode.Combine( h, c.SteamId, IsReady( c ), c == Connection.Local );
}
return h;
}
}