MatchmakingManager.cs
using Sandbox;
using Sandbox.Network;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Threading.Tasks;

public sealed class MatchmakingManager : Component
{
	[Property, Category( "Settings" )] 
	public List<SceneFile> GameScenes { get; set; } = new();

	[Property, Category( "Settings" )] 
	public int RequiredPlayers { get; set; } = 6;

	public bool IsSearching { get; private set; }
	public string StatusMessage { get; private set; } = "Waiting...";

	public int ConnectedPlayers => Connection.All.Count;

	[Sync] public NetList<Guid> ReadyPlayers { get; set; } = new();

	protected override void OnStart()
	{
		IsSearching = false;
		if ( Networking.IsHost )
		{
			ReadyPlayers.Clear();
		}
	}

	/// <summary>
	/// Start matchmaking (auto-join or host lobby)
	/// </summary>
	public async void StartMatchmaking()
	{
		if ( IsSearching ) return;

		IsSearching = true;
		StatusMessage = "Searching for active games...";

		try
		{
			// 1. Query for all active lobbies of our game
			var lobbies = await Networking.QueryLobbies();
			
			if ( !IsSearching ) return; // Safety check in case search was cancelled during async query

			foreach ( var lobby in lobbies )
			{
				// If lobby has empty slots
				if ( lobby.Members < RequiredPlayers )
				{
					StatusMessage = $"Connecting to lobby {lobby.LobbyId}...";
					Networking.Connect( lobby.LobbyId );
					StatusMessage = "Waiting for players...";
					return;
				}
			}

			// 2. If no open lobbies found, host our own lobby
			StatusMessage = "Creating a new lobby...";
			Networking.CreateLobby( new LobbyConfig { MaxPlayers = RequiredPlayers } );
			StatusMessage = "Waiting for players...";
		}
		catch ( System.Exception ex )
		{
			Log.Error( $"Matchmaking error: {ex.Message}" );
			StatusMessage = "Connection error!";
			IsSearching = false;
		}
	}

	/// <summary>
	/// Cancel search or disconnect from lobby
	/// </summary>
	public void CancelMatchmaking()
	{
		IsSearching = false;
		StatusMessage = "Matchmaking cancelled";
		if ( Networking.IsActive )
		{
			Networking.Disconnect();
		}
		if ( Networking.IsHost )
		{
			ReadyPlayers.Clear();
		}
	}

	[Rpc.Broadcast]
	public void ToggleReady( Guid connectionId )
	{
		// Only host processes the state to avoid client desync
		if ( !Networking.IsHost ) return;

		if ( ReadyPlayers.Contains( connectionId ) )
		{
			ReadyPlayers.Remove( connectionId );
		}
		else
		{
			ReadyPlayers.Add( connectionId );
		}
	}

	protected override void OnUpdate()
	{
		if ( !IsSearching ) return;

		// 3. If connected to network lobby
		if ( Networking.IsActive )
		{
			if ( ConnectedPlayers < RequiredPlayers )
			{
				StatusMessage = $"Waiting for players: {ConnectedPlayers} of {RequiredPlayers}";
				
				// Optional: Clear ready states if a player leaves and drops us below requirement
				if ( Networking.IsHost && ReadyPlayers.Count > 0 )
				{
					ReadyPlayers.Clear();
				}
			}
			else
			{
				int readyCount = ReadyPlayers.Count;
				StatusMessage = $"Players ready: {readyCount} / {RequiredPlayers}";

				// 4. Start game only if EVERYONE is ready
				if ( Networking.IsHost && readyCount >= RequiredPlayers && readyCount == ConnectedPlayers )
				{
					StatusMessage = "Game starting!";
					StartGame();
					// Disable searching so we don't spam StartGame
					IsSearching = false;
				}
			}
		}
	}

	private void StartGame()
	{
		Log.Info( "Starting game!" );
		if ( GameScenes == null || GameScenes.Count == 0 )
		{
			Log.Error( "No game scenes assigned in MatchmakingManager!" );
			return;
		}

		// Randomly select one of the configured scenes
		int randomIndex = Random.Shared.Int( 0, GameScenes.Count - 1 );
		SceneFile chosenScene = GameScenes[randomIndex];

		Log.Info( $"Chosen map: {chosenScene.ResourceName}" );

		var options = new SceneLoadOptions();
		options.SetScene( chosenScene );
		Game.ChangeScene( options );
	}
}