Player/Ghost/GhostPlayer.cs

Component that plays back a ghost recording by binding a MovieClip's tracks to this GameObject and updating playback over time. It exposes properties for the recording, player name, lap time, looping, and reports minimap blip info.

NetworkingFile Access
using Machines.UI;
using Sandbox.MovieMaker;
using Sandbox.MovieMaker.Compiled;

namespace Machines.Ghost;

/// <summary>
/// Plays back a ghost recording by rebinding clip tracks to this GameObject.
/// </summary>
public sealed class GhostPlayer : Component, IMinimapBlip
{
	Color IMinimapBlip.BlipColor => new( 0.6f, 0.6f, 0.6f );
	string IMinimapBlip.BlipClass => "ghost";
	int IMinimapBlip.BlipPriority => 0;
	bool IMinimapBlip.ShowOnMinimap => IsPlaying;

	/// <summary>
	/// The currently active ghost (last-wins, singleplayer only).
	/// </summary>
	public static GhostPlayer Current { get; private set; }

	/// <summary>
	/// The recording to play back.
	/// </summary>
	public GhostRecording Recording { get; set; }

	/// <summary>
	/// Player name shown on the ghost nameplate.
	/// </summary>
	public string PlayerName { get; set; }

	/// <summary>
	/// Lap time this ghost represents (seconds).
	/// </summary>
	public float LapTime { get; set; }

	/// <summary>
	/// Whether to loop playback when it reaches the end.
	/// </summary>
	[Property]
	public bool Loop { get; set; } = true;

	/// <summary>
	/// Whether playback is currently active.
	/// </summary>
	public bool IsPlaying { get; private set; }

	public double CurrentTime { get; private set; }

	private MovieClip _clip;
	private TrackBinder _binder;
	private double _duration;

	/// <summary>
	/// Start playback from the beginning.
	/// </summary>
	public void StartPlayback()
	{
		if ( Recording == null || !Recording.IsValid )
			return;

		_clip = Recording.Clip;
		_duration = _clip.Duration.TotalSeconds;
		_binder = new TrackBinder( Scene );

		foreach ( var track in _clip.Tracks )
		{
			if ( track is ICompiledReferenceTrack refTrack )
			{
				_binder.Get( refTrack ).Bind( GameObject );
				break;
			}
		}

		CurrentTime = 0f;
		IsPlaying = true;
	}

	protected override void OnEnabled()
	{
		Current = this;
	}

	protected override void OnDisabled()
	{
		if ( Current == this )
			Current = null;
	}

	protected override void OnFixedUpdate()
	{
		if ( !IsPlaying || _clip is null || _duration <= 0f )
			return;

		CurrentTime += Time.Delta;

		if ( CurrentTime >= _duration )
		{
			CurrentTime = Loop ? CurrentTime % _duration : _duration;
		}

		var time = MovieTime.FromSeconds( CurrentTime );
		_clip.Update( time, _binder );
	}
}