From 6af354ff991cc7253d6b4dc700dfe4ee36d9cf24 Mon Sep 17 00:00:00 2001 From: Ivaylo Draganov Date: Sat, 29 Feb 2020 22:31:29 +0200 Subject: [PATCH] Split chat lines into pools - Add a common class for passing around chat lines - Add wrapper methods for adding chat lines - Combine repeated chat lines in the display widget --- OpenRA.Game/Network/OrderManager.cs | 28 ++------- OpenRA.Game/Network/UnitOrders.cs | 8 +-- OpenRA.Game/Settings.cs | 8 +++ OpenRA.Game/TextNotification.cs | 56 ++++++++++++++++++ OpenRA.Game/TextNotificationsManager.cs | 27 +++++++-- .../Scripting/Global/MediaGlobal.cs | 2 +- .../Widgets/ChatDisplayWidget.cs | 57 +++++++++---------- .../Widgets/Logic/Ingame/IngameChatLogic.cs | 51 ++++++++++++----- .../Widgets/Logic/Lobby/LobbyLogic.cs | 8 +-- .../Widgets/Logic/Lobby/LobbyUtils.cs | 10 ++-- .../WorldInteractionControllerWidget.cs | 8 +-- 11 files changed, 175 insertions(+), 88 deletions(-) create mode 100644 OpenRA.Game/TextNotification.cs diff --git a/OpenRA.Game/Network/OrderManager.cs b/OpenRA.Game/Network/OrderManager.cs index 08b82f1efd..df6d3c4c0a 100644 --- a/OpenRA.Game/Network/OrderManager.cs +++ b/OpenRA.Game/Network/OrderManager.cs @@ -46,9 +46,9 @@ namespace OpenRA.Network readonly List localOrders = new List(); readonly List localImmediateOrders = new List(); - readonly List chatCache = new List(); + readonly List notificationsCache = new List(); - public IReadOnlyList ChatCache => chatCache; + public IReadOnlyList NotificationsCache => notificationsCache; bool disposed; bool generateSyncReport = false; @@ -94,7 +94,7 @@ namespace OpenRA.Network { Connection = conn; syncReport = new SyncReport(this); - AddChatLine += CacheChatLine; + AddTextNotification += CacheTextNotification; } public void IssueOrders(Order[] orders) @@ -111,10 +111,10 @@ namespace OpenRA.Network localOrders.Add(order); } - public Action AddChatLine = (n, nc, s, tc) => { }; - void CacheChatLine(string name, Color nameColor, string text, Color textColor) + public Action AddTextNotification = (notification) => { }; + void CacheTextNotification(TextNotification notification) { - chatCache.Add(new ChatLine(name, nameColor, text, textColor)); + notificationsCache.Add(notification); } public void TickImmediate() @@ -242,20 +242,4 @@ namespace OpenRA.Network Connection?.Dispose(); } } - - public class ChatLine - { - public readonly Color Color; - public readonly string Name; - public readonly string Text; - public readonly Color TextColor; - - public ChatLine(string name, Color nameColor, string text, Color textColor) - { - Color = nameColor; - Name = name; - Text = text; - TextColor = textColor; - } - } } diff --git a/OpenRA.Game/Network/UnitOrders.cs b/OpenRA.Game/Network/UnitOrders.cs index a4e0022995..3f1eee39e8 100644 --- a/OpenRA.Game/Network/UnitOrders.cs +++ b/OpenRA.Game/Network/UnitOrders.cs @@ -70,7 +70,7 @@ namespace OpenRA.Network if (orderManager.LocalClient != null && client != orderManager.LocalClient && client.Team > 0 && client.Team == orderManager.LocalClient.Team) suffix += " (Ally)"; - TextNotificationsManager.AddChatLine(client.Name + suffix, client.Color, message); + TextNotificationsManager.AddChatLine(client.Name + suffix, message, client.Color); break; } @@ -79,7 +79,7 @@ namespace OpenRA.Network { var prefix = order.ExtraData == uint.MaxValue ? "[Spectators] " : "[Team] "; if (orderManager.LocalClient != null && client.Team == orderManager.LocalClient.Team) - TextNotificationsManager.AddChatLine(prefix + client.Name, client.Color, message); + TextNotificationsManager.AddChatLine(prefix + client.Name, message, client.Color); break; } @@ -93,7 +93,7 @@ namespace OpenRA.Network { // Validate before adding the line if (client.IsObserver || (player != null && player.WinState != WinState.Undefined)) - TextNotificationsManager.AddChatLine("[Spectators] " + client.Name, client.Color, message); + TextNotificationsManager.AddChatLine("[Spectators] " + client.Name, message, client.Color); break; } @@ -103,7 +103,7 @@ namespace OpenRA.Network && world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Undefined; if (valid && (isSameTeam || world.IsReplay)) - TextNotificationsManager.AddChatLine("[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, client.Color, message); + TextNotificationsManager.AddChatLine("[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, message, client.Color); break; } diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index 507cbea11e..d183863b4a 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -33,6 +33,12 @@ namespace OpenRA Incompatible = 16 } + [Flags] + public enum TextNotificationPoolFilters + { + None = 0 + } + public enum WorldViewport { Native, Close, Medium, Far } public class ServerSettings @@ -265,6 +271,8 @@ namespace OpenRA [Desc("Allow mods to enable the Discord service that can interact with a local Discord client.")] public bool EnableDiscordService = true; + + public TextNotificationPoolFilters TextNotificationPoolFilters = TextNotificationPoolFilters.None; } public class Settings diff --git a/OpenRA.Game/TextNotification.cs b/OpenRA.Game/TextNotification.cs new file mode 100644 index 0000000000..71b8ee2a5b --- /dev/null +++ b/OpenRA.Game/TextNotification.cs @@ -0,0 +1,56 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Primitives; + +namespace OpenRA +{ + public enum TextNotificationPool { System, Chat, Mission, Feedback } + + public class TextNotification : IEquatable + { + public readonly TextNotificationPool Pool; + public readonly string Prefix; + public readonly string Text; + public readonly Color PrefixColor; + public readonly Color TextColor; + + public TextNotification(TextNotificationPool pool, string prefix, string text, Color prefixColor, Color textColor) + { + Pool = pool; + Prefix = prefix; + Text = text; + PrefixColor = prefixColor; + TextColor = textColor; + } + + public bool CanIncrementOnDuplicate() + { + return Pool == TextNotificationPool.Feedback || Pool == TextNotificationPool.System; + } + + public bool Equals(TextNotification other) + { + return other != null && other.GetHashCode() == GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is TextNotification && Equals((TextNotification)obj); + } + + public override int GetHashCode() + { + return string.Format("{0}{1}{2}", Prefix, Text, Pool).GetHashCode(); + } + } +} diff --git a/OpenRA.Game/TextNotificationsManager.cs b/OpenRA.Game/TextNotificationsManager.cs index 9dc564e81b..220d7d453e 100644 --- a/OpenRA.Game/TextNotificationsManager.cs +++ b/OpenRA.Game/TextNotificationsManager.cs @@ -28,24 +28,43 @@ namespace OpenRA systemMessageLabel = "Battlefield Control"; } + public static void AddFeedbackLine(string text) + { + AddTextNotification(TextNotificationPool.Feedback, systemMessageLabel, text, systemMessageColor, systemMessageColor); + } + public static void AddSystemLine(string text) { AddSystemLine(systemMessageLabel, text); } - public static void AddSystemLine(string name, string text) + public static void AddSystemLine(string prefix, string text) { - Game.OrderManager.AddChatLine(name, systemMessageColor, text, systemMessageColor); + AddTextNotification(TextNotificationPool.System, prefix, text, systemMessageColor, systemMessageColor); } - public static void AddChatLine(string name, Color nameColor, string text) + public static void AddChatLine(string prefix, string text, Color? prefixColor = null, Color? textColor = null) { - Game.OrderManager.AddChatLine(name, nameColor, text, chatMessageColor); + AddTextNotification(TextNotificationPool.Chat, prefix, text, prefixColor, textColor); } public static void Debug(string s, params object[] args) { AddSystemLine("Debug", string.Format(s, args)); } + + static void AddTextNotification(TextNotificationPool pool, string prefix, string text, Color? prefixColor = null, Color? textColor = null) + { + if (IsPoolEnabled(pool)) + Game.OrderManager.AddTextNotification(new TextNotification(pool, prefix, text, prefixColor ?? chatMessageColor, textColor ?? chatMessageColor)); + } + + static bool IsPoolEnabled(TextNotificationPool pool) + { + return pool == TextNotificationPool.Chat || + pool == TextNotificationPool.System || + pool == TextNotificationPool.Mission || + pool == TextNotificationPool.Feedback; + } } } diff --git a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs index 74ff3486c8..db4f309544 100644 --- a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs @@ -194,7 +194,7 @@ namespace OpenRA.Mods.Common.Scripting return; var c = color.HasValue ? color.Value : Color.White; - TextNotificationsManager.AddChatLine(prefix, c, text); + TextNotificationsManager.AddChatLine(prefix, text, c); } [Desc("Display a system message to the player. If 'prefix' is nil the default system prefix is used.")] diff --git a/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs b/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs index 65cd1e3ece..99983686fd 100644 --- a/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs @@ -28,7 +28,8 @@ namespace OpenRA.Mods.Common.Widgets public readonly int Space = 4; const int LogLength = 9; - List recentLines = new List(); + List recentLines = new List(); + List lineExpirations = new List(); public override Rectangle EventBounds => Rectangle.Empty; @@ -47,9 +48,9 @@ namespace OpenRA.Mods.Common.Widgets var inset = 0; string name = null; - if (!string.IsNullOrEmpty(line.Name)) + if (!string.IsNullOrEmpty(line.Prefix)) { - name = line.Name + ":"; + name = line.Prefix + ":"; inset = font.Measure(name).X + 5; } @@ -76,12 +77,12 @@ namespace OpenRA.Mods.Common.Widgets if (UseContrast) font.DrawTextWithContrast(name, namePos, - line.NameColor, BackgroundColorDark, BackgroundColorLight, 1); + line.PrefixColor, BackgroundColorDark, BackgroundColorLight, 1); else if (UseShadow) font.DrawTextWithShadow(name, namePos, - line.NameColor, BackgroundColorDark, BackgroundColorLight, 1); + line.PrefixColor, BackgroundColorDark, BackgroundColorLight, 1); else - font.DrawText(name, namePos, line.NameColor); + font.DrawText(name, namePos, line.PrefixColor); } if (UseContrast) @@ -99,21 +100,34 @@ namespace OpenRA.Mods.Common.Widgets Game.Renderer.DisableScissor(); } - public void AddLine(string name, Color nameColor, string text, Color textColor) + public void AddLine(TextNotification chatLine) { - recentLines.Add(new ChatLine(name, nameColor, text, textColor, Game.LocalTick + RemoveTime)); + recentLines.Add(chatLine); + lineExpirations.Add(Game.LocalTick + RemoveTime); if (Notification != null) Game.Sound.Play(SoundType.UI, Notification); while (recentLines.Count > LogLength) - recentLines.RemoveAt(0); + RemoveLine(); + } + + public void RemoveMostRecentLine() + { + if (recentLines.Count == 0) + return; + + recentLines.RemoveAt(recentLines.Count - 1); + lineExpirations.RemoveAt(lineExpirations.Count - 1); } public void RemoveLine() { - if (recentLines.Count > 0) - recentLines.RemoveAt(0); + if (recentLines.Count == 0) + return; + + recentLines.RemoveAt(0); + lineExpirations.RemoveAt(0); } public override void Tick() @@ -122,25 +136,8 @@ namespace OpenRA.Mods.Common.Widgets return; // This takes advantage of the fact that recentLines is ordered by expiration, from sooner to later - while (recentLines.Count > 0 && Game.LocalTick >= recentLines[0].Expiration) - recentLines.RemoveAt(0); - } - } - - class ChatLine - { - public readonly Color NameColor; - public readonly Color TextColor; - public readonly string Name, Text; - public readonly int Expiration; - - public ChatLine(string name, Color nameColor, string text, Color textColor, int expiration) - { - Name = name; - Text = text; - Expiration = expiration; - NameColor = nameColor; - TextColor = textColor; + while (recentLines.Count > 0 && Game.LocalTick >= lineExpirations[0]) + RemoveLine(); } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs index 3ee73bb022..af1ae51e84 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs @@ -41,6 +41,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly string chatLineSound = ChromeMetrics.Get("ChatLineSound"); + TextNotification lastLine; + int repetitions; + [ObjectCreator.UseCtor] public IngameChatLogic(Widget widget, OrderManager orderManager, World world, ModData modData, bool isMenuChat, Dictionary logicArgs) { @@ -195,10 +198,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic chatScrollPanel.RemoveChildren(); chatScrollPanel.ScrollToBottom(); - foreach (var chatLine in orderManager.ChatCache) - AddChatLine(chatLine.Name, chatLine.Color, chatLine.Text, chatLine.TextColor, true); + foreach (var chatLine in orderManager.NotificationsCache) + AddChatLine(chatLine, true); - orderManager.AddChatLine += AddChatLineWrapper; + orderManager.AddTextNotification += AddChatLineWrapper; chatText.IsDisabled = () => world.IsReplay && !Game.Settings.Debug.EnableDebugCommandsInReplays; @@ -245,38 +248,58 @@ namespace OpenRA.Mods.Common.Widgets.Logic Ui.ResetTooltips(); } - public void AddChatLineWrapper(string name, Color nameColor, string text, Color textColor) + public void AddChatLineWrapper(TextNotification chatLine) { - chatOverlayDisplay?.AddLine(name, nameColor, text, textColor); + var chatLineToDisplay = chatLine; + + if (chatLine.CanIncrementOnDuplicate() && chatLine.Equals(lastLine)) + { + repetitions++; + chatLineToDisplay = new TextNotification( + chatLine.Pool, + chatLine.Prefix, + $"{chatLine.Text} ({repetitions + 1})", + chatLine.PrefixColor, + chatLine.TextColor); + + chatScrollPanel.RemoveChild(chatScrollPanel.Children[chatScrollPanel.Children.Count - 1]); + chatOverlayDisplay?.RemoveMostRecentLine(); + } + else + repetitions = 0; + + lastLine = chatLine; + + chatOverlayDisplay?.AddLine(chatLineToDisplay); // HACK: Force disable the chat notification sound for the in-menu chat dialog // This works around our inability to disable the sounds for the in-game dialog when it is hidden - AddChatLine(name, nameColor, text, textColor, chatOverlay == null); + AddChatLine(chatLineToDisplay, chatOverlay == null); } - void AddChatLine(string @from, Color nameColor, string text, Color textColor, bool suppressSound) + void AddChatLine(TextNotification chatLine, bool suppressSound) { var template = chatTemplate.Clone(); var nameLabel = template.Get("NAME"); var textLabel = template.Get("TEXT"); var name = ""; - if (!string.IsNullOrEmpty(from)) - name = from + ":"; + if (!string.IsNullOrEmpty(chatLine.Prefix)) + name = chatLine.Prefix + ":"; var font = Game.Renderer.Fonts[nameLabel.Font]; - var nameSize = font.Measure(from); + var nameSize = font.Measure(chatLine.Prefix); - nameLabel.GetColor = () => nameColor; + nameLabel.GetColor = () => chatLine.PrefixColor; nameLabel.GetText = () => name; nameLabel.Bounds.Width = nameSize.X; - textLabel.GetColor = () => textColor; + textLabel.GetColor = () => chatLine.TextColor; textLabel.Bounds.X += nameSize.X; textLabel.Bounds.Width -= nameSize.X; // Hack around our hacky wordwrap behavior: need to resize the widget to fit the text - text = WidgetUtils.WrapText(text, textLabel.Bounds.Width, font); + var text = WidgetUtils.WrapText(chatLine.Text, textLabel.Bounds.Width, font); textLabel.GetText = () => text; var dh = font.Measure(text).Y - textLabel.Bounds.Height; if (dh > 0) @@ -299,7 +322,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { if (!disposed) { - orderManager.AddChatLine -= AddChatLineWrapper; + orderManager.AddTextNotification -= AddChatLineWrapper; disposed = true; } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 055e60f451..cf9ffb9d24 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -121,7 +121,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic services = modData.Manifest.Get(); - orderManager.AddChatLine += AddChatLine; + orderManager.AddTextNotification += AddChatLine; Game.LobbyInfoChanged += UpdateCurrentMap; Game.LobbyInfoChanged += UpdatePlayerList; Game.LobbyInfoChanged += UpdateDiscordStatus; @@ -466,7 +466,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (disposing && !disposed) { disposed = true; - orderManager.AddChatLine -= AddChatLine; + orderManager.AddTextNotification -= AddChatLine; Game.LobbyInfoChanged -= UpdateCurrentMap; Game.LobbyInfoChanged -= UpdatePlayerList; Game.LobbyInfoChanged -= UpdateDiscordStatus; @@ -489,10 +489,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic panel = PanelType.Players; } - void AddChatLine(string name, Color nameColor, string text, Color textColor) + void AddChatLine(TextNotification chatLine) { var template = (ContainerWidget)chatTemplate.Clone(); - LobbyUtils.SetupChatLine(template, DateTime.Now, name, nameColor, text, textColor); + LobbyUtils.SetupChatLine(template, DateTime.Now, chatLine); var scrolledToBottom = lobbyChatPanel.ScrolledToBottom; lobbyChatPanel.AddChild(template); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs index 3a0f91c882..5c257c5ed8 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs @@ -651,28 +651,28 @@ namespace OpenRA.Mods.Common.Widgets.Logic HideChildWidget(parent, "STATUS_IMAGE"); } - public static void SetupChatLine(ContainerWidget template, DateTime time, string name, Color nameColor, string text, Color textColor) + public static void SetupChatLine(ContainerWidget template, DateTime time, TextNotification chatLine) { var nameLabel = template.Get("NAME"); var timeLabel = template.Get("TIME"); var textLabel = template.Get("TEXT"); - var nameText = name + ":"; + var nameText = chatLine.Prefix + ":"; var font = Game.Renderer.Fonts[nameLabel.Font]; var nameSize = font.Measure(nameText); timeLabel.GetText = () => $"{time.Hour:D2}:{time.Minute:D2}"; - nameLabel.GetColor = () => nameColor; + nameLabel.GetColor = () => chatLine.PrefixColor; nameLabel.GetText = () => nameText; nameLabel.Bounds.Width = nameSize.X; - textLabel.GetColor = () => textColor; + textLabel.GetColor = () => chatLine.TextColor; textLabel.Bounds.X += nameSize.X; textLabel.Bounds.Width -= nameSize.X; // Hack around our hacky wordwrap behavior: need to resize the widget to fit the text - text = WidgetUtils.WrapText(text, textLabel.Bounds.Width, font); + var text = WidgetUtils.WrapText(chatLine.Text, textLabel.Bounds.Width, font); textLabel.GetText = () => text; var dh = font.Measure(text).Y - textLabel.Bounds.Height; if (dh > 0) diff --git a/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs b/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs index 6bf49d6e28..8e1375cb95 100644 --- a/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs @@ -248,12 +248,12 @@ namespace OpenRA.Mods.Common.Widgets // Check if selecting actors on the screen has selected new units if (ownUnitsOnScreen.Count > World.Selection.Actors.Count()) - TextNotificationsManager.AddSystemLine("Selected across screen"); + TextNotificationsManager.AddFeedbackLine("Selected across screen"); else { // Select actors in the world that have highest selection priority ownUnitsOnScreen = SelectActorsInWorld(World, null, eligiblePlayers).SubsetWithHighestSelectionPriority(e.Modifiers).ToList(); - TextNotificationsManager.AddSystemLine("Selected across map"); + TextNotificationsManager.AddFeedbackLine("Selected across map"); } World.Selection.Combine(World, ownUnitsOnScreen, false, false); @@ -280,12 +280,12 @@ namespace OpenRA.Mods.Common.Widgets // Check if selecting actors on the screen has selected new units if (newSelection.Count > World.Selection.Actors.Count()) - TextNotificationsManager.AddSystemLine("Selected across screen"); + TextNotificationsManager.AddFeedbackLine("Selected across screen"); else { // Select actors in the world that have the same selection class as one of the already selected actors newSelection = SelectActorsInWorld(World, selectedClasses, eligiblePlayers).ToList(); - TextNotificationsManager.AddSystemLine("Selected across map"); + TextNotificationsManager.AddFeedbackLine("Selected across map"); } World.Selection.Combine(World, newSelection, true, false);