A Razor UI component for in-game chat. Renders chat entries, accepts input, fades old messages, handles sending chat via RPC broadcast, shows system messages, plays UI sounds, and listens for network connect/disconnect to add join/leave messages.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits PanelComponent
@implements Component.INetworkListener
<root>
<div class="output">
@foreach (var entry in Entries)
{
<div class="chat_entry" style="opacity:@(Utils.Map(entry.realTimeSinceAdded, 0f, CHAT_ENTRY_LIFETIME, 1f, 0f, EasingType.QuartIn) * (entry.isSpectator ? 0.5f : 1f));">
@if(entry.isSpectator)
{
<i>videocam</i>
}
@if( entry.steamid != 0 )
{
<div class="avatar" style="background-image: url( avatar:@entry.steamid )"></div>
}
<div class="author @(entry.systemText ? "system_author" : "")">@entry.author</div>
<RichText class="message @(entry.systemText ? "system_message" : "")" [email protected] />
</div>
}
</div>
<div class="input">
<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
</div>
</root>
@code
{
// todo: chat messages fade out?
TextEntry InputBox;
public record Entry ( ulong steamid, string author, string message, RealTimeSince realTimeSinceAdded, bool systemText, bool isSpectator = false, bool clearOnRestart = false );
List<Entry> Entries = new();
public const float CHAT_ENTRY_LIFETIME = 15f;
protected override void OnUpdate()
{
if (InputBox is null)
return;
Panel.AcceptsFocus = false;
if ( Input.Pressed( "chat" ) )
{
InputBox.Focus();
}
if ( Entries.RemoveAll( x => x.realTimeSinceAdded > CHAT_ENTRY_LIFETIME ) > 0 )
{
StateHasChanged();
}
SetClass( "open", InputBox.HasFocus );
Panel.Style.Bottom = Manager.Instance.GameState == GameState.Lobby ? Length.Percent( 75 ) : Length.Pixels( 200 );
if(InputBox.Text.Length > 0)
InputBox.Text = CheckChatMessage(InputBox.Text);
}
void ChatFinished()
{
var text = InputBox.Text;
InputBox.Text = "";
if (string.IsNullOrWhiteSpace(text))
return;
// Only broadcast chat to other players if chat is not disabled
if ( !GameSettingsSystem.Current.DisableChat )
AddText( GetDisplayText( text ) );
var player = Manager.Instance.LocalPlayer;
if ( player is not null )
{
player.ForEachPerk( perk => perk.OnSayChat( text ) );
player.ForEachLoadoutItem( item => item.OnSayChat( text ) );
}
}
string GetDisplayText( string text )
{
var perkType = Perk.FindTypeByLooseDisplayName( text );
if ( perkType is null )
return text;
return $"{Perk.GetRichTextNameToken( perkType.TargetType )} {Perk.GetRichTextToken( perkType )}";
}
string CheckChatMessage(string message)
{
var player = Manager.Instance.LocalPlayer;
if( player is not null && player.Stats[PlayerStat.KeyBrokenE] > 0f)
{
var oldLength = message.Length;
message = message.Replace("e", "");
message = message.Replace("E", "");
message = message.Replace("е", "");
message = message.Replace("ẹ", "");
message = message.Replace("ė", "");
message = message.Replace("é", "");
message = message.Replace("è", "");
if( oldLength != message.Length )
player.HighlightPerk( TypeLibrary.GetType( typeof( PerkBrokenKey ) ) );
}
return message;
}
[Rpc.Broadcast(NetFlags.Reliable)]
public void AddText( string message, bool systemText = false, bool clearOnRestart = false )
{
if ( GameSettingsSystem.Current.DisableChat && !systemText )
return;
message = message.Truncate( 300 );
if (string.IsNullOrWhiteSpace(message))
return;
var author = Rpc.Caller.DisplayName;
var steamid = Rpc.Caller.SteamId;
var guid = Rpc.Caller.Id;
var isSpectator = !( Manager.Instance?.Players?.Any( x => x.Network.Owner.Id == guid ) ?? false );
Log.Info($"{author}: {message}");
Manager.Instance.PlaySfxUI( "chat.message", pitch: Game.Random.Float(0.95f, 1.05f), volume: 0.75f );
Entries.Add( new Entry( steamid, author, message, 0.0f, systemText, isSpectator, clearOnRestart ) );
StateHasChanged();
}
[Rpc.Broadcast(NetFlags.Reliable)] // todo: only from host/owner
public void AddSystemText(ulong steamid, string author, string message)
{
message = message.Truncate(300);
if (string.IsNullOrWhiteSpace(message))
return;
Entries.Add(new Entry(steamid, author, message, 0.0f, systemText: true));
StateHasChanged();
}
public void AddLocalChatMessage( string message, string from )
{
if (string.IsNullOrWhiteSpace(message))
return;
Entries.Add(new Entry(0, from, message, 0.0f, systemText: true, clearOnRestart: true));
StateHasChanged();
Manager.Instance.PlaySfxUI( "chat.message", pitch: Game.Random.Float(0.8f, 0.85f), volume: 0.75f );
}
public void Restart()
{
for(int i = Entries.Count - 1; i >= 0; i--) {
if ( Entries[i].clearOnRestart )
Entries.RemoveAt(i);
}
StateHasChanged();
}
void Component.INetworkListener.OnConnected( Connection channel )
{
Manager.Instance.PlaySfxUI( "player.join", pitch: Game.Random.Float(0.95f, 1.05f), volume: 1f );
if ( IsProxy ) return;
AddSystemText( channel.SteamId, channel.DisplayName, "has joined the game" );
}
void Component.INetworkListener.OnDisconnected( Connection channel )
{
Manager.Instance.PlaySfxUI( "player.left", pitch: Game.Random.Float(0.95f, 1.05f), volume: 1.1f );
if ( IsProxy ) return;
AddSystemText( channel.SteamId, channel.DisplayName, "has left the game" );
}
protected override int BuildHash()
{
return HashCode.Combine(Entries.Count() > 0 ? RealTime.Now : 0f);
}
}