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.
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();
}
}
}