GameLoop/GameManager.Stage.cs
using System.Diagnostics;

public partial class GameManager
{
	public enum GameStage
	{
		// Show warmup
		Warmup,

		// play game
		Game,

		// Show scoreboard
		Results,

		// game finished, next map
		Finished

	}

	/// <summary>
	/// How long has this game been going on for?
	/// </summary>
	[Sync] public TimeSince TimeElapsed { get; set; }

	public GameStage CurrentGameStage
	{
		get
		{
			if ( !GameSettings.GameLoop )
				return GameStage.Game;

			if ( Connection.All.Count < 1 )
				return GameStage.Warmup;

			if ( TimeElapsed < GameSettings.WarmupTime ) return GameStage.Warmup;
			if ( TimeElapsed < GameSettings.WarmupTime + GameSettings.RoundTime ) return GameStage.Game;
			if ( TimeElapsed < GameSettings.WarmupTime + GameSettings.RoundTime + GameSettings.ResultTime ) return GameStage.Results;

			return GameStage.Finished;
		}
	}

	public float GameTimeRemaining
	{
		get
		{
			return (GameSettings.WarmupTime + GameSettings.RoundTime) - TimeElapsed;
		}
	}

	GameStage _previousStage = GameStage.Finished;

	/// <summary>
	/// Rewards the winners of the game. 
	/// I've decided for now that if there's multiple people who share the same kills, they also win.
	/// </summary>
	void RewardWinners()
	{
		Assert.True( Networking.IsHost, "GameManager.RewardWinners should always run on the host!" );

		// This can happen on dedicated servers - but maybe we shouldn't be doing anything at all..
		if ( !PlayerData.All.Any() ) return;

		var winningGroup = PlayerData.All.GroupBy( x => x.Kills )
			.OrderByDescending( g => g.Key )
			.First();

		// No kills? No stats
		if ( winningGroup.Any( x => x.Kills < 1 ) ) return;

		foreach ( var winner in winningGroup )
		{
			winner.AddStat( $"wins" );
		}
	}

	/// <summary>
	/// What maps can we switch to? Exclude the current map
	/// </summary>
	public IEnumerable<string> Maps => GameSettings.MapList.Split( ";" ).Where( x => x != Networking.MapName );

	[ConCmd( "sbdm.restart" )]
	public static void RestartGame()
	{
		Log.Info( $"Restarting the game" );
		Current.TimeElapsed = 0;
	}

	void StageUpdate()
	{
		var stage = CurrentGameStage;
		if ( _previousStage != stage )
		{

			_previousStage = stage;

			if ( stage == GameStage.Game )
			{
				StartGame();
			}

			if ( stage == GameStage.Results )
			{
				Results();
			}

			if ( stage == GameStage.Finished )
			{
				FinishGame();
			}
		}

		UpdateCurrentStage();
	}

	Sandbox.Services.BenchmarkSystem _benchmarkSystem;

	void StartGame()
	{
		if ( !Networking.IsHost ) return;

		Stopwatch sw = null;
		if ( GameSettings.IsBenchmark )
		{

			GameSettings.BotCount = 9; // 9 Bots + 1 player = 10, seems like a decent amount for datacore
			GameSettings.RoundTime = 150; // 2:30 minutes should be enough for a benchmark

			_benchmarkSystem = new Sandbox.Services.BenchmarkSystem();

			_benchmarkSystem.Start( "deathmatch_botmatch" );

			// Capture time it takes to spawn players
			sw = Stopwatch.StartNew();
		}

		// Spawn all players
		foreach ( var connection in PlayerData.All )
		{
			SpawnPlayer( connection );
		}

		// Spawn bots
		SpawnBot( GameSettings.BotCount );

		if ( GameSettings.IsBenchmark && sw != null )
		{
			_benchmarkSystem.SetMetric( "StartDuration", sw.Elapsed.TotalSeconds );
		}
	}

	async void Results()
	{
		if ( !Networking.IsHost ) return;

		if ( GameSettings.IsBenchmark )
		{
			// 0 because there is not really a shutdown window we can capture?
			_benchmarkSystem.SetMetric( "EndDuration", 0 );

			_benchmarkSystem.Finish();

			await _benchmarkSystem.SendAsync();
			_benchmarkSystem = default;

			Game.Close();

			return;
		}

		RewardWinners();
	}

	void FinishGame()
	{
		if ( !Networking.IsHost ) return;

		// TODO - maps in order (once we have more than one map in the cycle)
		LaunchArguments.Map = Random.Shared.FromArray( Maps.ToArray() );

		// Next Round
		Game.Load( Game.Ident, true );
	}

	void UpdateCurrentStage()
	{
		if ( GameSettings.IsBenchmark )
		{
			_benchmarkSystem?.Sample();
		}

		if ( CurrentGameStage != GameStage.Game )
		{
			CycleSpawnCameras();
		}
	}

	GameObject[] spawnCams;

	void CycleSpawnCameras()
	{
		spawnCams ??= Scene.GetAllObjects( true ).Where( x => x.Tags.Contains( "spawncam" ) ).ToArray();
		if ( spawnCams.Length == 0 )
			return;

		var secondsPerCamera = 3.0f;
		var x = (Time.Now / secondsPerCamera).FloorToInt();

		var cam = spawnCams[x % (spawnCams.Length - 1)];

		Scene.Camera.WorldTransform = cam.WorldTransform;

	}
}