StreamGameManager.cs
/// <summary>
/// The hub of the example, and the one place that talks to the streaming system.
///
/// It implements <see cref="Streamer.IEvents"/>, which any <c>Component</c> can do to receive stream
/// events. The engine dispatches these to the active scene on the main thread, so it's safe to touch
/// GameObjects directly from them.
///
/// The flow is:
///   - a viewer joins chat   -> spawn them an avatar (a <see cref="StreamPlayer"/>)
///   - a viewer leaves chat  -> remove their avatar
///   - a viewer sends a chat message -> turn it into a command and route it to their avatar
///
/// Keeping all of the stream handling here - rather than letting every avatar listen to chat itself -
/// gives us one clear entry point, and lets <see cref="StreamPlayer"/> stay a simple component that just
/// does what it's told.
/// </summary>
public sealed class StreamGameManager : Component, Streamer.IEvents
{
	/// <summary>
	/// The prefab cloned for each viewer. Assign your avatar prefab to this in the inspector.
	/// </summary>
	[Property]
	public PrefabFile ViewerPrefab { get; set; }

	/// <summary>
	/// The avatar we've spawned for each viewer currently in chat. We keep this map so we can find a
	/// viewer's avatar when they chat and tear it down when they leave. A <see cref="Streamer.Viewer"/> is
	/// a stable object for as long as that viewer is in chat, so it's safe to use as a dictionary key.
	/// </summary>
	readonly Dictionary<Streamer.Viewer, StreamPlayer> _avatars = new();

	/// <summary>
	/// A viewer joined chat - give them an avatar.
	/// </summary>
	void Streamer.IEvents.OnStreamJoin( Streamer.Viewer viewer )
	{
		Log.Info( $"{viewer.DisplayName} joined" );
		SpawnAvatar( viewer );
	}

	/// <summary>
	/// A viewer left chat - remove their avatar.
	/// </summary>
	void Streamer.IEvents.OnStreamLeave( Streamer.Viewer viewer )
	{
		Log.Info( $"{viewer.DisplayName} left" );
		RemoveAvatar( viewer );
	}

	/// <summary>
	/// A viewer sent a chat message. "spawn" (re)spawns their avatar; every other message is forwarded to
	/// that viewer's avatar as a command (see <see cref="StreamPlayer.OnChatCommand"/>).
	/// </summary>
	void Streamer.IEvents.OnStreamMessage( Streamer.ChatMessage message )
	{
		var viewer = message.Viewer;

		if ( message.Message == "spawn" )
		{
			SpawnAvatar( viewer );
			return;
		}

		// Route the message to this viewer's avatar, if they have one. This is the part that used to live
		// on StreamPlayer; doing it here keeps every stream interaction in a single component.
		if ( _avatars.TryGetValue( viewer, out var avatar ) && avatar.IsValid() )
		{
			avatar.OnChatCommand( message.Message );
		}
	}

	/// <summary>
	/// Clone the avatar prefab for a viewer and register it. If they already had an avatar it's removed
	/// first, so sending "spawn" again acts as a respawn.
	/// </summary>
	void SpawnAvatar( Streamer.Viewer viewer )
	{
		RemoveAvatar( viewer );

		// Clone disabled so we can configure the avatar in Setup() before its components start running,
		// then enable it once it's ready.
		var go = GameObject.Clone( ViewerPrefab, new CloneConfig { StartEnabled = false } );
		go.Name = viewer.Username;
		go.WorldPosition = (Vector3.Random * 300f).WithZ( 0 );

		var avatar = go.GetComponent<StreamPlayer>( true );
		avatar.Setup( viewer );

		go.Enabled = true;

		_avatars[viewer] = avatar;
	}

	/// <summary>
	/// Remove and destroy a viewer's avatar, if they have one.
	/// </summary>
	void RemoveAvatar( Streamer.Viewer viewer )
	{
		if ( !_avatars.Remove( viewer, out var avatar ) )
			return;

		avatar?.GameObject?.Destroy();
	}

	protected override void OnUpdate()
	{
		OrbitCamera();
	}

	/// <summary>
	/// Slowly orbit the scene camera so there's always something to look at.
	/// </summary>
	void OrbitCamera()
	{
		const float distance = 1100f;

		var angle = (Rotation)new Angles( 45, Time.Now * 0.2f, 0 );

		Scene.Camera.WorldPosition = angle.Backward * distance;
		Scene.Camera.WorldRotation = angle;
	}
}