using Content.Client.UserInterface.Systems.Chat.Controls; using Content.Shared._EE.CCVars; // EE - chat stacking using Content.Shared.Chat; using Content.Shared.Input; using Robust.Client.Audio; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Audio; using Robust.Shared.Configuration; using Robust.Shared.Input; using Robust.Shared.Player; using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.LineEdit; namespace Content.Client.UserInterface.Systems.Chat.Widgets; [GenerateTypedNameReferences] [Virtual] public partial class ChatBox : UIWidget { private readonly ChatUIController _controller; private readonly IEntityManager _entManager; [Dependency] private readonly IConfigurationManager _cfg = default!; // EE - Chat stacking [Dependency] private readonly ILocalizationManager _loc = default!; // EE - Chat stacking public bool Main { get; set; } public ChatSelectChannel SelectedChannel => ChatInput.ChannelSelector.SelectedChannel; // EE - Chat stacking private int _chatStackAmount = 0; private bool ChatStackEnabled => _chatStackAmount > 0; private List _chatStackList; // End EE - Chat stacking public ChatBox() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _entManager = IoCManager.Resolve(); ChatInput.Input.OnTextEntered += OnTextEntered; ChatInput.Input.OnKeyBindDown += OnInputKeyBindDown; ChatInput.Input.OnTextChanged += OnTextChanged; ChatInput.Input.OnFocusEnter += OnFocusEnter; ChatInput.Input.OnFocusExit += OnFocusExit; ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect; ChatInput.FilterButton.Popup.OnChannelFilter += OnChannelFilter; ChatInput.FilterButton.Popup.OnNewHighlights += OnNewHighlights; _controller = UserInterfaceManager.GetUIController(); _controller.MessageAdded += OnMessageAdded; _controller.HighlightsUpdated += OnHighlightsUpdated; _controller.RegisterChat(this); // EE - Chat stacking _chatStackList = new List(_chatStackAmount); _cfg.OnValueChanged(EECVars.ChatStackLastLines, UpdateChatStack, true); // End EE - Chat stacking } // EE - Chat stacking private void UpdateChatStack(int value) { _chatStackAmount = value >= 0 ? value : 0; Repopulate(); } private void OnTextEntered(LineEditEventArgs args) { _controller.SendMessage(this, SelectedChannel); } private void OnMessageAdded(ChatMessage msg) { Logger.DebugS("chat", $"{msg.Channel}: {msg.Message}"); if (!ChatInput.FilterButton.Popup.IsActive(msg.Channel)) { return; } if (msg is { Read: false, AudioPath: { } }) _entManager.System().PlayGlobal(msg.AudioPath, Filter.Local(), false, AudioParams.Default.WithVolume(msg.AudioVolume)); msg.Read = true; var color = msg.MessageColorOverride ?? msg.Channel.TextColor(); // EE - Chat stacking var index = _chatStackList.FindIndex(data => data.Channel == msg.Channel && data.Entity == msg.SenderEntity && data.Message == msg.Message); // Frontier: add entity, channel, use message, not wrapped message if (index == -1) // this also handles chatstack being disabled, since FindIndex won't find anything in an empty array { TrackNewMessage(msg.WrappedMessage, color, msg.Message, msg.SenderEntity, msg.Channel); // Frontier: add Message, SenderEntity AddLine(msg.WrappedMessage, color); return; } UpdateRepeatingLine(index); // End EE - Chat stacking } /// /// Removing and then adding instantly nudges the chat window up before slowly dragging it back down, which makes the whole chat log shake. /// With rapid enough updates, the whole chat becomes unreadable. /// Adding first and then removing does not produce any visual effects. /// The other option is to duplicate OutputPanel functionality and everything internal to the engine it relies on. /// But OutputPanel relies on directly setting Control.Position for control embedding. (which is not exposed to Content.) /// Thanks robustengine, very cool. /// /// /// zero index is the very last line in chat, 1 is the line before the last one, 2 is the line before that, etc. /// // EE - Chat stacking private void UpdateRepeatingLine(int index) { _chatStackList[index].RepeatCount++; for (var i = index; i >= 0; i--) { var data = _chatStackList[i]; AddLine(data.WrappedMessage, data.ColorOverride, data.RepeatCount); Contents.RemoveEntry(Index.FromEnd(index + 2)); } } // EE - Chat stacking private void TrackNewMessage(string wrappedMessage, Color colorOverride, string message, NetEntity entity, ChatChannel channel) // Frontier: add message, entity, channel { if (!ChatStackEnabled) return; if (_chatStackList.Count == _chatStackList.Capacity) _chatStackList.RemoveAt(_chatStackList.Capacity - 1); _chatStackList.Insert(0, new ChatStackData(wrappedMessage, colorOverride, message, entity, channel)); // Frontier: add message, entity, channel } private void OnHighlightsUpdated(string highlights) { ChatInput.FilterButton.Popup.UpdateHighlights(highlights); } private void OnChannelSelect(ChatSelectChannel channel) { _controller.UpdateSelectedChannel(this); } public void Repopulate() { Contents.Clear(); _chatStackList = new List(_chatStackAmount); // EE - Chat stacking foreach (var message in _controller.History) { OnMessageAdded(message.Item2); } } private void OnChannelFilter(ChatChannel channel, bool active) { Contents.Clear(); foreach (var message in _controller.History) { OnMessageAdded(message.Item2); } if (active) { _controller.ClearUnfilteredUnreads(channel); } } private void OnNewHighlights(string highlighs) { _controller.UpdateHighlights(highlighs); } public void AddLine(string message, Color color, int repeat = 0) // EE - Chat stacking - repeat { var formatted = new FormattedMessage(4); // EE - Chat stacking - up from 3 formatted.PushColor(color); formatted.AddMarkupOrThrow(message); formatted.Pop(); // EE - Chat stacking if (repeat != 0) { var displayRepeat = repeat + 1; var sizeIncrease = Math.Min(displayRepeat / 6, 5); formatted.AddMarkupOrThrow(_loc.GetString("chat-system-repeated-message-counter", ("count", displayRepeat), ("size", 8 + sizeIncrease) )); } // End EE - Chat stacking Contents.AddMessage(formatted); } public void Focus(ChatSelectChannel? channel = null) { var input = ChatInput.Input; var selectStart = Index.End; if (channel != null) ChatInput.ChannelSelector.Select(channel.Value); input.IgnoreNext = true; input.GrabKeyboardFocus(); input.CursorPosition = input.Text.Length; input.SelectionStart = selectStart.GetOffset(input.Text.Length); } public void CycleChatChannel(bool forward) { var idx = Array.IndexOf(ChannelSelectorPopup.ChannelSelectorOrder, SelectedChannel); do { // go over every channel until we find one we can actually select. idx += forward ? 1 : -1; idx = MathHelper.Mod(idx, ChannelSelectorPopup.ChannelSelectorOrder.Length); } while ((_controller.SelectableChannels & ChannelSelectorPopup.ChannelSelectorOrder[idx]) == 0); SafelySelectChannel(ChannelSelectorPopup.ChannelSelectorOrder[idx]); } public void SafelySelectChannel(ChatSelectChannel toSelect) { toSelect = _controller.MapLocalIfGhost(toSelect); if ((_controller.SelectableChannels & toSelect) == 0) return; ChatInput.ChannelSelector.Select(toSelect); } private void OnInputKeyBindDown(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.TextReleaseFocus) { ChatInput.Input.ReleaseKeyboardFocus(); ChatInput.Input.Clear(); args.Handle(); return; } if (args.Function == ContentKeyFunctions.CycleChatChannelForward) { CycleChatChannel(true); args.Handle(); return; } if (args.Function == ContentKeyFunctions.CycleChatChannelBackward) { CycleChatChannel(false); args.Handle(); } } private void OnTextChanged(LineEditEventArgs args) { // Update channel select button to correct channel if we have a prefix. _controller.UpdateSelectedChannel(this); // Warn typing indicator about change _controller.NotifyChatTextChange(); } private void OnFocusEnter(LineEditEventArgs args) { // Warn typing indicator about focus _controller.NotifyChatFocus(true); } private void OnFocusExit(LineEditEventArgs args) { // Warn typing indicator about focus _controller.NotifyChatFocus(false); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) return; _controller.UnregisterChat(this); ChatInput.Input.OnTextEntered -= OnTextEntered; ChatInput.Input.OnKeyBindDown -= OnInputKeyBindDown; ChatInput.Input.OnTextChanged -= OnTextChanged; ChatInput.ChannelSelector.OnChannelSelect -= OnChannelSelect; _cfg.UnsubValueChanged(EECVars.ChatStackLastLines, UpdateChatStack); // EE - Chat stacking } // EE - Chat stacking private sealed class ChatStackData { public NetEntity Entity; // Frontier: speaker public string Message; // Frontier: base message public ChatChannel Channel; // Frontier: channel public string WrappedMessage; public Color ColorOverride; public int RepeatCount = 0; public ChatStackData(string wrappedMessage, Color colorOverride, string message, NetEntity entity, ChatChannel channel) { WrappedMessage = wrappedMessage; ColorOverride = colorOverride; Message = message; // Frontier Entity = entity; // Frontier Channel = channel; // Frontier } } // End EE - Chat stacking }