Game/MansionGame.Timer.cs

Part of the MansionGame class that implements the round/level timer. It defines configurable level duration and late-join window, holds a host-authoritative countdown, synchronizes the remaining time and active flag to clients, runs the host-side tick to decrement the timer, stops the timer on expiry and invokes a partial hook OnTimerExpired, and updates spectator state each fixed update.

Networking
using System;
using Sandbox;

namespace BrickJam;

public sealed partial class MansionGame
{
	/// <summary>Seconds each level lasts before the timer expires.</summary>
	[Property] public float TimePerLevel { get; set; } = 180f;

	/// <summary>How close to level start a late joiner may still spawn as a player rather than a spectator.</summary>
	[Property] public float TimeToJoin { get; set; } = 5f;

	// The GameManager is an unowned NetworkMode.Snapshot object, so host-written [Sync] state must be
	// FromHost or clients never receive it (plain [Sync] expects an owning connection to write it). This is
	// the same authority rule as Player.Money. Without it the timer/level never replicated → the timer (and
	// level-driven music) only worked on the host.

	/// <summary>Host-authoritative seconds remaining on the level timer.</summary>
	[Sync( SyncFlags.FromHost )] public float TimeLeft { get; set; }

	[Sync( SyncFlags.FromHost )] public bool TimerActive { get; set; }

	/// <summary>
	/// Host-local countdown clock. NOT synced: <see cref="TimeUntil"/> is <c>Time.Now</c>-relative and
	/// <c>Time.Now</c> isn't synchronized across machines, so syncing it directly gives clients a wrong
	/// remaining time. We stream the computed seconds via <see cref="TimeLeft"/> instead.
	/// </summary>
	private TimeUntil hostTimeOut;

	/// <summary>
	/// Implemented by the Level subsystem once it is ported. Called on the host when the level
	/// timer runs out (legacy <c>SimulateTimer</c> -> <c>RestartGame</c>).
	/// </summary>
	partial void OnTimerExpired();

	protected override void OnFixedUpdate()
	{
		if ( !Networking.IsHost )
			return;

		SimulateTimer();
		UpdateSpectators();
		CurrentLevel?.Compute();
	}

	public void TimerStart()
	{
		TimerActive = true;
		hostTimeOut = TimePerLevel;
		TimeLeft = TimePerLevel;
	}

	public void TimerStop()
	{
		TimerActive = false;
		TimeLeft = 0f;
	}

	private void SimulateTimer()
	{
		if ( !TimerActive )
			return;

		// Stream the remaining seconds to every client each tick (host clock is authoritative).
		TimeLeft = MathF.Max( (float)hostTimeOut, 0f );

		if ( hostTimeOut )
		{
			TimerStop();
			OnTimerExpired();
		}
	}
}