Code/SboxSuperuser/Systems/ChatBoxSystem.cs

A GameObjectSystem for the Superuser admin package that swaps the built-in chat box for a custom ChatBoxRenderer while the host has prefixed chat enabled, and restores the built-in chat when it goes off. It collects ChatMessageEvent messages, delivers them one frame later, handles mentions, chat key input, autocomplete for /commands pulled from an admin query, and player-profile opening.

using Sandbox;
using SboxSuperuser.UI;

namespace SboxSuperuser.Systems;

/// <summary>
/// Swaps the built-in chat box for Superuser's while the host has prefixes and Prefixed Chat on, and restores it
/// when they go off. Messages arrive through the bootstrap's chat events and show a frame later, after every other
/// listener has had the chance to suppress them.
/// </summary>
internal sealed class ChatBoxSystem : GameObjectSystem<ChatBoxSystem>
{
	private readonly List<ChatMessageEvent> _pending = new();
	private ChatBoxRenderer _box;
	private bool _builtInShown;
	private (string Sender, string Text, float At) _last;

	public ChatBoxSystem( Scene scene ) : base( scene )
	{
		Listen( Stage.StartUpdate, -60, Update, nameof( ChatBoxSystem ) );
		Listen( Stage.FinishUpdate, 90, Render, nameof( ChatBoxSystem ) );
	}

	internal bool Active => _box is not null;

	internal bool IsOpen => _box?.IsOpen == true;

	internal void Observe( ChatMessageEvent message )
	{
		if ( Active && message is not null && !message.Suppress ) _pending.Add( message );
	}

	/// <summary>Shows a tagged public announcement; returns false when the built-in chat should show it.</summary>
	internal bool ShowAnnouncement( string tag, string message )
	{
		if ( !Active ) return false;
		_box.AddAnnouncement( tag, message );
		return true;
	}

	/// <summary>Shows Superuser's own private replies; returns false when the built-in chat should show them.</summary>
	internal bool ShowPrivate( IEnumerable<string> lines )
	{
		if ( !Active ) return false;
		foreach ( var line in lines ) _box.AddSystem( line );
		return true;
	}

	private void Update()
	{
		var wanted = !Application.IsDedicatedServer && Game.IsPlaying && Networking.IsActive
			&& Scene.GetSystem<PrefixSystem>()?.ChatActive == true;
		if ( wanted != Active )
		{
			if ( wanted ) Start();
			else Stop();
		}
		if ( !Active ) return;

		Deliver();
		var menuOpen = Scene.GetSystem<AdminMenuSystem>()?.IsOpen == true;
		if ( !_box.IsOpen && !menuOpen && Input.Pressed( "chat" ) ) _box.Open();
		// However the box opened, fetch the commands this player may run for autocomplete.
		if ( _box.IsOpen && !_wasOpen ) RequestCommands();
		_wasOpen = _box.IsOpen;
		if ( menuOpen ) _box.Close();
		_box.CaptureInput();
	}

	private void Deliver()
	{
		foreach ( var message in _pending )
		{
			if ( message.Suppress ) continue;
			if ( message.Sender is null )
			{
				_box.AddSystem( message.Message );
				continue;
			}
			// The host can see the same message twice (validation, then local delivery); show it once.
			var sender = message.Sender.SteamId.ToString();
			if ( _last.Sender == sender && _last.Text == message.Message && RealTime.Now - _last.At < 0.25f ) continue;
			_last = (sender, message.Message, RealTime.Now);
			var names = Connection.All.Select( connection => connection.DisplayName ).ToArray();
			var mentions = Core.Chat.Mentions.Find( message.Message, names );
			var mentionsMe = Connection.Local is { } local && mentions.Any( mention => string.Equals( mention.Name, local.DisplayName, StringComparison.OrdinalIgnoreCase ) );
			_box.AddPlayer( sender, message.Sender.DisplayName, message.Message, mentions, mentionsMe );
		}
		_pending.Clear();
	}

	private void Render()
	{
		try { _box?.Draw(); }
		catch ( Exception error )
		{
			Log.Warning( $"[Superuser] Chat box failed; the built-in chat is back. type={error.GetType().Name}." );
			Stop();
		}
	}

	private void Start()
	{
		_builtInShown = ProjectSettings.Platform.ChatShowUI;
		ProjectSettings.Platform.ChatShowUI = false;
		_box = new ChatBoxRenderer( Scene, OpenPlayer, () => Scene.GetSystem<AdminMenuSystem>()?.IsAvailable == true, Complete );
	}

	// Commands this player may run, refreshed each time the chat opens since permissions can change.
	private IReadOnlyList<Core.Commands.CompletionCommand> _commands = Array.Empty<Core.Commands.CompletionCommand>();
	private Guid _commandsRequest;
	private bool _wasOpen;

	private void RequestCommands()
	{
		try { _commandsRequest = Superuser.RequestAdminQuery( new AdminQuery( AdminQueryKind.Commands ) ); }
		catch ( InvalidOperationException ) { _commandsRequest = Guid.Empty; }
	}

	/// <summary>Takes the command list for autocomplete; every other admin reply is someone else's.</summary>
	internal void Receive( AdminReply reply )
	{
		if ( reply.RequestId != _commandsRequest || reply.Status != CommandStatus.Success ) return;
		_commands = reply.Commands.Select( command => new Core.Commands.CompletionCommand( command.Name.Replace( '.', ' ' ),
			command.Arguments.Select( argument => new Core.Commands.CompletionArgument( argument.Name, Kind( argument ), argument.Optional,
				argument.Remainder, argument.Choices, argument.Description ) ).ToArray(), command.Description ) ).ToArray();
		_box?.RefreshCompletion();
	}

	private static Core.Commands.CompletionKind Kind( AdminCommandArgumentRow argument ) => argument.Kind switch
	{
		CommandArgumentKind.Player => Core.Commands.CompletionKind.Player,
		CommandArgumentKind.Duration => Core.Commands.CompletionKind.Duration,
		CommandArgumentKind.Boolean => Core.Commands.CompletionKind.Boolean,
		CommandArgumentKind.Integer or CommandArgumentKind.Number => Core.Commands.CompletionKind.Number,
		_ => argument.Choices.Length > 0 ? Core.Commands.CompletionKind.Choice : Core.Commands.CompletionKind.Text
	};

	private Core.Commands.Completion Complete( string line )
	{
		var players = Connection.All.Select( connection => connection.DisplayName ).ToArray();
		if ( !line.StartsWith( '/' ) ) return Core.Chat.Mentions.Complete( line, players );
		var root = Scene.GetAllComponents<Components.SuperuserBootstrap>().FirstOrDefault()?.CommandRoot ?? "admin";
		return Core.Commands.CommandCompletion.Complete( line, root, _commands, players );
	}

	private void OpenPlayer( string steamId )
	{
		_box?.Close();
		Scene.GetSystem<AdminMenuSystem>()?.ShowPlayer( steamId );
	}

	private void Stop()
	{
		_box?.Dispose();
		_box = null;
		_pending.Clear();
		ProjectSettings.Platform.ChatShowUI = _builtInShown;
	}

	public override void Dispose()
	{
		if ( Active ) Stop();
		base.Dispose();
	}
}