diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 249a82fece..c68c711e2e 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -16,6 +16,7 @@ using System.IO; using System.Linq; using System.Net; using System.Threading; +using OpenRA.Chat; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Network; @@ -45,6 +46,8 @@ namespace OpenRA public static Sound Sound; public static bool HasInputFocus = false; + public static GlobalChat GlobalChat; + public static OrderManager JoinServer(string host, int port, string password, bool recordReplay = true) { IConnection connection = new NetworkConnection(host, port); @@ -204,6 +207,7 @@ namespace OpenRA Log.AddChannel("sound", "sound.log"); Log.AddChannel("graphics", "graphics.log"); Log.AddChannel("geoip", "geoip.log"); + Log.AddChannel("irc", "irc.log"); if (Settings.Server.DiscoverNatDevices) UPnP.TryNatDiscovery(); @@ -237,6 +241,8 @@ namespace OpenRA Sound = new Sound(Settings.Server.Dedicated ? "Null" : Settings.Sound.Engine); + GlobalChat = new GlobalChat(); + Console.WriteLine("Available mods:"); foreach (var mod in ModMetadata.AllMods) Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Title, mod.Value.Version); @@ -688,6 +694,8 @@ namespace OpenRA worldRenderer.Dispose(); ModData.Dispose(); ChromeProvider.Deinitialize(); + + GlobalChat.Dispose(); Sound.Dispose(); Renderer.Dispose(); diff --git a/OpenRA.Game/GlobalChat.cs b/OpenRA.Game/GlobalChat.cs new file mode 100644 index 0000000000..59e7ad5b46 --- /dev/null +++ b/OpenRA.Game/GlobalChat.cs @@ -0,0 +1,371 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Meebey.SmartIrc4net; +using OpenRA; +using OpenRA.Primitives; + +namespace OpenRA.Chat +{ + public enum ChatConnectionStatus { Disconnected, Connecting, Connected, Disconnecting, Joined, Error } + public enum ChatMessageType { Message, Notification } + + public sealed class ChatUser + { + public readonly string Name; + public bool IsOp; + public bool IsVoiced; + + public ChatUser(string name, bool isOp, bool isVoice) + { + Name = name; + IsOp = isOp; + IsVoiced = isVoice; + } + } + + public sealed class ChatMessage + { + static long nextUID; + + public readonly DateTime Time; + public readonly ChatMessageType Type; + public readonly string Nick; + public readonly string Message; + public readonly string UID; + + public ChatMessage(DateTime time, ChatMessageType type, string nick, string message) + { + Time = time; + Type = type; + Nick = nick; + Message = message; + + UID = Interlocked.Increment(ref nextUID).ToString(); + } + + public override string ToString() + { + var time = Time.ToString(Game.Settings.Chat.TimestampFormat); + if (Type == ChatMessageType.Notification) + return "{0} {1}".F(time, Message); + + return "{0} {1}: {2}".F(time, Nick, Message); + } + } + + public class GlobalChat : IDisposable + { + readonly IrcClient client = new IrcClient(); + volatile Channel channel; + + public readonly ObservableSortedDictionary Users = new ObservableSortedDictionary(StringComparer.InvariantCultureIgnoreCase); + public readonly ObservableList History = new ObservableList(); + + volatile string topic; + public string Topic { get { return topic; } } + + volatile ChatConnectionStatus connectionStatus = ChatConnectionStatus.Disconnected; + public ChatConnectionStatus ConnectionStatus { get { return connectionStatus; } } + + public GlobalChat() + { + client.Encoding = System.Text.Encoding.UTF8; + client.SendDelay = 100; + client.ActiveChannelSyncing = true; + + client.OnConnecting += OnConnecting; + client.OnConnected += OnConnected; + client.OnDisconnecting += OnDisconnecting; + client.OnDisconnected += OnDisconnected; + client.OnError += OnError; + client.OnKick += OnKick; + + client.OnRawMessage += (_, e) => Game.RunAfterTick(() => Log.Write("irc", e.Data.RawMessage)); + client.OnJoin += OnJoin; + client.OnChannelActiveSynced += OnChannelActiveSynced; + client.OnTopic += (_, e) => topic = e.Topic; + client.OnTopicChange += (_, e) => topic = e.NewTopic; + client.OnNickChange += OnNickChange; + + client.OnChannelMessage += (_, e) => AddMessage(e.Data.Nick, e.Data.Message); + client.OnOp += (_, e) => SetUserOp(e.Whom, true); + client.OnDeop += (_, e) => SetUserOp(e.Whom, false); + client.OnVoice += (_, e) => SetUserVoiced(e.Whom, true); + client.OnDevoice += (_, e) => SetUserVoiced(e.Whom, false); + client.OnPart += OnPart; + client.OnQuit += OnQuit; + } + + void SetUserOp(string whom, bool isOp) + { + Game.RunAfterTick(() => + { + ChatUser user; + if (Users.TryGetValue(whom, out user)) + user.IsOp = isOp; + }); + } + + void SetUserVoiced(string whom, bool isVoiced) + { + Game.RunAfterTick(() => + { + ChatUser user; + if (Users.TryGetValue(whom, out user)) + user.IsVoiced = isVoiced; + }); + } + + public void Connect() + { + if (client.IsConnected) + return; + + new Thread(() => + { + try + { + client.Connect(Game.Settings.Chat.Hostname, Game.Settings.Chat.Port); + } + catch (Exception e) + { + connectionStatus = ChatConnectionStatus.Error; + AddNotification(e.Message); + Game.RunAfterTick(() => Log.Write("irc", e.ToString())); + + return; + } + + client.Listen(); + }) { Name = "IrcListenThread" }.Start(); + } + + void AddNotification(string text) + { + var message = new ChatMessage(DateTime.Now, ChatMessageType.Notification, null, text); + Game.RunAfterTick(() => + { + History.Add(message); + Log.Write("irc", text); + }); + } + + void AddMessage(string nick, string text) + { + var message = new ChatMessage(DateTime.Now, ChatMessageType.Message, nick, text); + Game.RunAfterTick(() => + { + History.Add(message); + Log.Write("irc", text); + }); + } + + void OnConnecting(object sender, EventArgs e) + { + AddNotification("Connecting to {0}:{1}...".F(Game.Settings.Chat.Hostname, Game.Settings.Chat.Port)); + connectionStatus = ChatConnectionStatus.Connecting; + } + + void OnConnected(object sender, EventArgs e) + { + AddNotification("Connected."); + connectionStatus = ChatConnectionStatus.Connected; + + // Guard against settings.yaml modification + var nick = SanitizedName(Game.Settings.Chat.Nickname); + if (nick != Game.Settings.Chat.Nickname) + Game.RunAfterTick(() => Game.Settings.Chat.Nickname = nick); + + client.Login(nick, "in-game IRC client", 0, "OpenRA"); + client.RfcJoin("#" + Game.Settings.Chat.Channel); + } + + void OnDisconnecting(object sender, EventArgs e) + { + if (connectionStatus != ChatConnectionStatus.Error) + connectionStatus = ChatConnectionStatus.Disconnecting; + } + + void OnDisconnected(object sender, EventArgs e) + { + Game.RunAfterTick(Users.Clear); + + // Keep the chat window open if there is an error + // It will be cleared by the Disconnect button + if (connectionStatus != ChatConnectionStatus.Error) + { + Game.RunAfterTick(History.Clear); + topic = null; + connectionStatus = ChatConnectionStatus.Disconnected; + } + } + + void OnError(object sender, ErrorEventArgs e) + { + // Ignore any errors that happen during disconnect + if (connectionStatus != ChatConnectionStatus.Disconnecting) + { + connectionStatus = ChatConnectionStatus.Error; + AddNotification("Error: " + e.ErrorMessage); + } + } + + void OnKick(object sender, KickEventArgs e) + { + Disconnect(); + connectionStatus = ChatConnectionStatus.Error; + AddNotification("Error: You were kicked from the chat by {0}".F(e.Who)); + } + + void OnJoin(object sender, JoinEventArgs e) + { + if (e.Who == client.Nickname || e.Channel != channel.Name) + return; + + AddNotification("{0} joined the chat.".F(e.Who)); + Game.RunAfterTick(() => Users.Add(e.Who, new ChatUser(e.Who, false, false))); + } + + void OnChannelActiveSynced(object sender, IrcEventArgs e) + { + channel = client.GetChannel(e.Data.Channel); + AddNotification("{0} users online".F(channel.Users.Count)); + connectionStatus = ChatConnectionStatus.Joined; + + foreach (DictionaryEntry user in channel.Users) + { + var u = (ChannelUser)user.Value; + Game.RunAfterTick(() => Users.Add(u.Nick, new ChatUser(u.Nick, u.IsOp, u.IsVoice))); + } + } + + void OnNickChange(object sender, NickChangeEventArgs e) + { + AddNotification("{0} is now known as {1}.".F(e.OldNickname, e.NewNickname)); + + Game.RunAfterTick(() => + { + ChatUser user; + if (!Users.TryGetValue(e.OldNickname, out user)) + return; + + Users.Remove(e.OldNickname); + Users.Add(e.NewNickname, new ChatUser(e.NewNickname, user.IsOp, user.IsVoiced)); + }); + } + + void OnQuit(object sender, QuitEventArgs e) + { + AddNotification("{0} left the chat.".F(e.Who)); + Game.RunAfterTick(() => Users.Remove(e.Who)); + } + + void OnPart(object sender, PartEventArgs e) + { + if (e.Data.Channel != channel.Name) + return; + + AddNotification("{0} left the chat.".F(e.Who)); + Game.RunAfterTick(() => Users.Remove(e.Who)); + } + + public string SanitizedName(string dirty) + { + if (string.IsNullOrEmpty(dirty)) + return null; + + // There is no need to mangle the nick if it is already valid + if (Rfc2812.IsValidNickname(dirty)) + return dirty; + + // TODO: some special chars are allowed as well, but not at every position + var clean = new string(dirty.Where(c => char.IsLetterOrDigit(c)).ToArray()); + + if (string.IsNullOrEmpty(clean)) + return null; + + if (char.IsDigit(clean[0])) + return SanitizedName(clean.Substring(1)); + + // Source: https://tools.ietf.org/html/rfc2812#section-1.2.1 + if (clean.Length > 9) + clean = clean.Substring(0, 9); + + return clean; + } + + public bool IsValidNickname(string name) + { + return Rfc2812.IsValidNickname(name); + } + + public void SendMessage(string text) + { + if (connectionStatus != ChatConnectionStatus.Joined) + return; + + // Guard against a last-moment disconnection + try + { + client.SendMessage(SendType.Message, channel.Name, text); + AddMessage(client.Nickname, text); + } + catch (NotConnectedException) { } + } + + public bool TrySetNickname(string nick) + { + // TODO: This is inconsistent with the other check + if (Rfc2812.IsValidNickname(nick)) + { + client.RfcNick(nick); + Game.Settings.Chat.Nickname = nick; + return true; + } + + return false; + } + + public void Disconnect() + { + // Error is an alias for disconnect, but keeps the panel open + // so that clients can see the error + if (connectionStatus == ChatConnectionStatus.Error) + { + Game.RunAfterTick(History.Clear); + topic = null; + connectionStatus = ChatConnectionStatus.Disconnected; + } + else + connectionStatus = ChatConnectionStatus.Disconnecting; + + if (!client.IsConnected) + return; + + client.RfcQuit(Game.Settings.Chat.QuitMessage); + + AddNotification("Disconnecting from {0}...".F(client.Address)); + + Game.RunAfterTick(() => Game.Settings.Chat.ConnectAutomatically = false); + } + + public void Dispose() + { + if (client.IsConnected) + client.Disconnect(); + } + } +} \ No newline at end of file diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 8f93aea7c9..b90d93ebd1 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -84,6 +84,9 @@ ..\thirdparty\download\MaxMind.Db.dll False + + ..\thirdparty\download\SmarIrc4net.dll + diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index c6d932247a..db3c30012e 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -294,9 +294,9 @@ namespace OpenRA } } - public class IrcSettings + public class ChatSettings { - public string[] Hostname = { "irc.openra.net" }; + public string Hostname = "irc.openra.net"; public int Port = 6667; public string Channel = "lobby"; public string Nickname = "Newbie"; @@ -316,7 +316,7 @@ namespace OpenRA public ServerSettings Server = new ServerSettings(); public DebugSettings Debug = new DebugSettings(); public KeySettings Keys = new KeySettings(); - public IrcSettings Irc = new IrcSettings(); + public ChatSettings Chat = new ChatSettings(); public Dictionary Sections; @@ -332,7 +332,7 @@ namespace OpenRA { "Server", Server }, { "Debug", Debug }, { "Keys", Keys }, - { "Irc", Irc } + { "Chat", Chat } }; // Override fieldloader to ignore invalid entries diff --git a/OpenRA.Mods.Common/Irc.cs b/OpenRA.Mods.Common/Irc.cs deleted file mode 100644 index 29279d1b28..0000000000 --- a/OpenRA.Mods.Common/Irc.cs +++ /dev/null @@ -1,19 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2015 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. For more information, - * see COPYING. - */ -#endregion - -using Meebey.SmartIrc4net; - -namespace OpenRA.Mods.Common -{ - public static class Irc - { - public static volatile IrcClient Client; - } -} \ No newline at end of file diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index b587ca21e8..e02d6f9008 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -78,9 +78,6 @@ ..\thirdparty\download\ICSharpCode.SharpZipLib.dll False - - ..\thirdparty\download\SmarIrc4net.dll - @@ -601,8 +598,6 @@ - - @@ -707,6 +702,7 @@ + diff --git a/OpenRA.Mods.Common/Widgets/Logic/GlobalChatLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/GlobalChatLogic.cs new file mode 100644 index 0000000000..2a925fada4 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/GlobalChatLogic.cs @@ -0,0 +1,163 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Threading; +using OpenRA.Chat; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + class GlobalChatLogic : IDisposable + { + readonly ScrollPanelWidget historyPanel; + readonly LabelWidget historyTemplate; + readonly ScrollPanelWidget nicknamePanel; + readonly Widget nicknameTemplate; + readonly TextFieldWidget inputBox; + + [ObjectCreator.UseCtor] + public GlobalChatLogic(Widget widget) + { + historyPanel = widget.Get("HISTORY_PANEL"); + historyTemplate = historyPanel.Get("HISTORY_TEMPLATE"); + nicknamePanel = widget.Get("NICKNAME_PANEL"); + nicknameTemplate = nicknamePanel.Get("NICKNAME_TEMPLATE"); + + historyPanel.Bind(Game.GlobalChat.History, MakeHistoryWidget, HistoryWidgetEquals, true); + nicknamePanel.Bind(Game.GlobalChat.Users, MakeUserWidget, UserWidgetEquals, false); + + inputBox = widget.Get("CHAT_TEXTFIELD"); + inputBox.IsDisabled = () => Game.GlobalChat.ConnectionStatus != ChatConnectionStatus.Joined; + inputBox.OnEnterKey = EnterPressed; + + // Set a random default nick + if (Game.Settings.Chat.Nickname == new ChatSettings().Nickname) + Game.Settings.Chat.Nickname += Game.CosmeticRandom.Next(100, 999); + + var nicknameBox = widget.Get("NICKNAME_TEXTFIELD"); + nicknameBox.Text = Game.GlobalChat.SanitizedName(Game.Settings.Chat.Nickname); + nicknameBox.OnTextEdited = () => + { + nicknameBox.Text = Game.GlobalChat.SanitizedName(nicknameBox.Text); + }; + + var connectPanel = widget.Get("GLOBALCHAT_CONNECT_PANEL"); + connectPanel.IsVisible = () => Game.GlobalChat.ConnectionStatus == ChatConnectionStatus.Disconnected; + + var disconnectButton = widget.Get("DISCONNECT_BUTTON"); + disconnectButton.OnClick = Game.GlobalChat.Disconnect; + + var connectAutomaticallyCheckBox = connectPanel.Get("CONNECT_AUTOMATICALLY_CHECKBOX"); + connectAutomaticallyCheckBox.IsChecked = () => Game.Settings.Chat.ConnectAutomatically; + connectAutomaticallyCheckBox.OnClick = () => { Game.Settings.Chat.ConnectAutomatically ^= true; Game.Settings.Save(); }; + + var connectButton = connectPanel.Get("CONNECT_BUTTON"); + connectButton.IsDisabled = () => !Game.GlobalChat.IsValidNickname(nicknameBox.Text); + connectButton.OnClick = () => + { + Game.Settings.Chat.Nickname = nicknameBox.Text; + Game.Settings.Save(); + Game.GlobalChat.Connect(); + }; + + var mainPanel = widget.Get("GLOBALCHAT_MAIN_PANEL"); + mainPanel.IsVisible = () => Game.GlobalChat.ConnectionStatus != ChatConnectionStatus.Disconnected; + + mainPanel.Get("CHANNEL_TOPIC").GetText = () => Game.GlobalChat.Topic; + + if (Game.Settings.Chat.ConnectAutomatically && Game.GlobalChat.IsValidNickname(Game.Settings.Chat.Nickname)) + Game.GlobalChat.Connect(); + } + + Widget MakeHistoryWidget(object o) + { + var message = (ChatMessage)o; + var widget = (LabelWidget)historyTemplate.Clone(); + var font = Game.Renderer.Fonts[widget.Font]; + + var color = message.Type == ChatMessageType.Notification ? + ChromeMetrics.Get("GlobalChatNotificationColor") : + ChromeMetrics.Get("GlobalChatTextColor"); + + var display = WidgetUtils.WrapText(message.ToString(), widget.Bounds.Width, font); + widget.Bounds.Height = font.Measure(display).Y; + widget.GetText = () => display; + widget.GetColor = () => color; + widget.Id = message.UID; + return widget; + } + + bool HistoryWidgetEquals(Widget widget, object o) + { + return ((LabelWidget)widget).Id == ((ChatMessage)o).UID; + } + + Widget MakeUserWidget(object o) + { + var nick = (string)o; + var client = Game.GlobalChat.Users[nick]; + + var item = nicknameTemplate.Clone(); + item.Id = client.Name; + item.IsVisible = () => true; + var name = item.Get("NICK"); + name.GetText = () => client.Name; + name.IsVisible = () => true; + + // TODO: Add custom image for voice + var indicator = item.Get("INDICATOR"); + indicator.IsVisible = () => client.IsOp || client.IsVoiced; + indicator.GetImageName = () => client.IsOp || client.IsVoiced ? "admin" : ""; + + return item; + } + + bool UserWidgetEquals(Widget widget, object o) + { + var nick = (string)o; + return widget.Id == nick; + } + + bool EnterPressed() + { + if (inputBox.Text.Length == 0) + return true; + + if (inputBox.Text.StartsWith("/nick ")) + { + var nick = inputBox.Text.Replace("/nick ", string.Empty); + Game.GlobalChat.TrySetNickname(nick); + } + else + Game.GlobalChat.SendMessage(inputBox.Text); + + inputBox.Text = ""; + + return true; + } + + bool disposed; + public void Dispose() + { + if (disposed) + return; + + historyPanel.Unbind(); + nicknamePanel.Unbind(); + + disposed = true; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/IrcLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/IrcLogic.cs deleted file mode 100644 index 930fb78fb2..0000000000 --- a/OpenRA.Mods.Common/Widgets/Logic/IrcLogic.cs +++ /dev/null @@ -1,374 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2015 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. For more information, - * see COPYING. - */ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Meebey.SmartIrc4net; -using OpenRA.Widgets; - -namespace OpenRA.Mods.Common.Widgets.Logic -{ - class IrcLogic - { - readonly TextFieldWidget inputBox; - readonly TextFieldWidget nicknameBox; - readonly Widget connectBG; - readonly Widget ircContainer; - - readonly ScrollPanelWidget historyPanel; - readonly LabelWidget historyTemplate; - - readonly ScrollPanelWidget nicknamePanel; - readonly LabelWidget nicknameTemplate; - - bool pingSent; - Channel channel; - - [ObjectCreator.UseCtor] - public IrcLogic(Widget widget) - { - Log.AddChannel("irc", "irc.log"); - - historyPanel = widget.Get("HISTORY_PANEL"); - historyTemplate = widget.Get("HISTORY_TEMPLATE"); - nicknamePanel = widget.Get("NICKNAME_PANEL"); - nicknameTemplate = widget.Get("NICKNAME_TEMPLATE"); - - inputBox = widget.Get("INPUT_BOX"); - inputBox.OnEnterKey = EnterPressed; - inputBox.IsDisabled = () => Irc.Client == null; - - if (Game.Settings.Irc.Nickname == new IrcSettings().Nickname) - Game.Settings.Irc.Nickname += Game.CosmeticRandom.Next(100, 999); - - nicknameBox = widget.Get("NICKNAME_BOX"); - nicknameBox.Text = SanitizedName(Game.Settings.Irc.Nickname); - nicknameBox.OnTextEdited = () => - { - nicknameBox.Text = SanitizedName(nicknameBox.Text); - Game.Settings.Irc.Nickname = nicknameBox.Text; - Game.Settings.Save(); - }; - - connectBG = widget.Get("IRC_CONNECT_BG"); - ircContainer = widget.Get("IRC_CONTAINER"); - - var disconnectButton = widget.Get("DISCONNECT_BUTTON"); - disconnectButton.IsDisabled = () => Irc.Client == null; - disconnectButton.OnClick = Disconnect; - - MaybeShowConnectPanel(); - } - - static string SanitizedName(string dirty) - { - if (string.IsNullOrEmpty(dirty)) - return null; - - // TODO: some special chars are allowed as well, but not at every position - var clean = new string(dirty.Where(c => char.IsLetterOrDigit(c)).ToArray()); - - if (string.IsNullOrEmpty(clean)) - return null; - - if (char.IsDigit(clean[0])) - return SanitizedName(clean.Substring(1)); - - // Source: https://tools.ietf.org/html/rfc2812#section-1.2.1 - if (clean.Length > 9) - clean = clean.Substring(0, 9); - - return clean; - } - - void MaybeShowConnectPanel() - { - if (Irc.Client != null && Irc.Client.IsConnected) - { - ircContainer.Visible = true; - connectBG.Visible = false; - - Initialize(); - - if (Irc.Client.JoinedChannels.Count > 0) - channel = Irc.Client.GetChannel(Irc.Client.JoinedChannels[0]); - - SyncNicknamePanel(); - - return; - } - - if (Game.Settings.Irc.ConnectAutomatically) - { - ircContainer.Visible = true; - connectBG.Visible = false; - Connect(); - return; - } - - ircContainer.Visible = false; - connectBG.Visible = true; - - var connectAutomaticallyCheckBox = connectBG.Get("CONNECT_AUTOMATICALLY_CHECKBOX"); - connectAutomaticallyCheckBox.IsChecked = () => Game.Settings.Irc.ConnectAutomatically; - connectAutomaticallyCheckBox.OnClick = () => Game.Settings.Irc.ConnectAutomatically ^= true; - - var connectButton = connectBG.Get("CONNECT_BUTTON"); - connectButton.IsDisabled = () => string.IsNullOrEmpty(nicknameBox.Text); - connectButton.OnClick = () => - { - ircContainer.Visible = true; - connectBG.Visible = false; - - Game.Settings.Irc.ConnectAutomatically = connectAutomaticallyCheckBox.IsChecked(); - Game.Settings.Save(); - Connect(); - }; - } - - void Initialize() - { - Irc.Client.OnConnected += OnConnected; - Irc.Client.OnError += OnError; - Irc.Client.OnRawMessage += OnRawMessage; - Irc.Client.OnJoin += OnJoin; - Irc.Client.OnChannelActiveSynced += OnChannelActiveSynced; - Irc.Client.OnNickChange += OnNickChange; - Irc.Client.OnPart += OnPart; - Irc.Client.OnQuit += OnQuit; - Irc.Client.OnChannelMessage += OnChannelMessage; - Irc.Client.OnPong += OnPong; - } - - void Connect() - { - Irc.Client = new IrcClient(); - Irc.Client.Encoding = System.Text.Encoding.UTF8; - Irc.Client.SendDelay = 100; - Irc.Client.ActiveChannelSyncing = true; - - Initialize(); - - Game.OnQuit += Disconnect; - - try - { - AddChatLine("Connecting to {0}...".F(Game.Settings.Irc.Hostname)); - Irc.Client.Connect(Game.Settings.Irc.Hostname, Game.Settings.Irc.Port); - } - catch (Exception e) - { - AddChatLine("Connection error: {0}".F(e.Message)); - Game.RunAfterTick(() => - { - Log.Write("irc", e.ToString()); - }); - } - - new Thread(Irc.Client.Listen) { Name = "IrcListenThread" }.Start(); - } - - void OnPong(object sender, PongEventArgs e) - { - if (pingSent) - { - AddChatLine("PONG recieved after {0} ms.".F(e.Lag.Milliseconds)); - pingSent = false; - } - else - { - Game.RunAfterTick(() => - { - Log.Write("irc", "PONG sent after {0} ms.".F(e.Lag.Milliseconds)); - }); - } - } - - Widget MakeLabelWidget(LabelWidget template, string item) - { - var widget = (LabelWidget)template.Clone(); - var font = Game.Renderer.Fonts[widget.Font]; - item = WidgetUtils.WrapText(item, widget.Bounds.Width, font); - widget.Bounds.Height = font.Measure(item).Y; - widget.GetText = () => item; - widget.Id = item; - return widget; - } - - void AddChatLine(string text) - { - Game.RunAfterTick(() => - { - Log.Write("irc", text); - - var scrolledToBottom = historyPanel.ScrolledToBottom; - - var newChild = MakeLabelWidget(historyTemplate, text); - historyPanel.AddChild(newChild); - - if (scrolledToBottom) - historyPanel.ScrollToBottom(smooth: true); - }); - } - - bool EnterPressed() - { - if (inputBox.Text.Length == 0) - return true; - - var text = inputBox.Text; - inputBox.Text = ""; - - if (text.StartsWith("/nick ")) - { - var nick = text.Replace("/nick ", string.Empty); - if (Rfc2812.IsValidNickname(nick)) - Irc.Client.RfcNick(nick); - else - AddChatLine("Invalid nickname."); - } - else if (text.StartsWith("/ping ")) - { - Irc.Client.RfcPing(Irc.Client.GetIrcUser(text.Replace("/ping ", string.Empty)).Host); - pingSent = true; - } - else if (text.StartsWith("/")) - AddChatLine("Unknown command."); - else - { - AddChatLine("[{0}] <{1}> {2}".F(DateTime.Now.ToString(Game.Settings.Irc.TimestampFormat), Irc.Client.Nickname, text)); - Irc.Client.SendMessage(SendType.Message, "#" + Game.Settings.Irc.Channel, text); - } - - return true; - } - - void OnConnected(object sender, EventArgs e) - { - AddChatLine("Connected."); - - if (!Rfc2812.IsValidNickname(Game.Settings.Irc.Nickname)) - { - AddChatLine("Invalid nickname. Can't login."); - return; - } - - Irc.Client.Login(new[] { Game.Settings.Irc.Nickname }, "in-game IRC client", 0, "OpenRA"); - - Irc.Client.RfcJoin("#" + Game.Settings.Irc.Channel); - } - - void OnError(object sender, ErrorEventArgs e) - { - AddChatLine("Error: " + e.ErrorMessage); - Game.RunAfterTick(() => - { - Log.Write("irc", e.ToString()); - }); - } - - void OnRawMessage(object sender, IrcEventArgs e) - { - Game.RunAfterTick(() => - { - Log.Write("irc", e.Data.RawMessage); - }); - } - - void OnChannelMessage(object sender, IrcEventArgs e) - { - AddChatLine("[{0}] <{1}> {2}".F(DateTime.Now.ToString(Game.Settings.Irc.TimestampFormat), e.Data.Nick, e.Data.Message)); - } - - void OnJoin(object sender, JoinEventArgs e) - { - if (e.Who == Irc.Client.Nickname) - return; - - AddChatLine("{0} joined channel {1}.".F(e.Who, e.Channel)); - channel = Irc.Client.GetChannel(e.Channel); - SyncNicknamePanel(); - } - - void OnChannelActiveSynced(object sender, IrcEventArgs e) - { - channel = Irc.Client.GetChannel(e.Data.Channel); - - AddChatLine("{0} users online".F(channel.Users.Count)); - - if (!string.IsNullOrEmpty(channel.Topic)) - AddChatLine("*** Topic: {0}".F(channel.Topic)); - - SyncNicknamePanel(); - } - - void OnNickChange(object sender, NickChangeEventArgs e) - { - AddChatLine("{0} is now known as {1}.".F(e.OldNickname, e.NewNickname)); - SyncNicknamePanel(); - } - - void SyncNicknamePanel() - { - if (channel == null) - return; - - var users = channel.Users; - Game.RunAfterTick(() => - { - nicknamePanel.RemoveChildren(); - - foreach (DictionaryEntry user in users) - { - var channeluser = (ChannelUser)user.Value; - var prefix = channeluser.IsOp ? "@" : channeluser.IsVoice ? "+" : ""; - var newChild = MakeLabelWidget(nicknameTemplate, prefix + channeluser.Nick); - nicknamePanel.AddChild(newChild); - } - }); - } - - void OnQuit(object sender, QuitEventArgs e) - { - AddChatLine("{0} quit.".F(e.Who)); - } - - void OnPart(object sender, PartEventArgs e) - { - AddChatLine("{0} left {1}.".F(e.Who, e.Data.Channel)); - channel = Irc.Client.GetChannel(e.Data.Channel); - SyncNicknamePanel(); - } - - void Disconnect() - { - if (Irc.Client == null) - return; - - Irc.Client.RfcQuit(Game.Settings.Irc.QuitMessage); - - AddChatLine("Disconnecting from {0}...".F(Irc.Client.Address)); - - if (Irc.Client.IsConnected) - Irc.Client.Disconnect(); - - nicknamePanel.RemoveChildren(); - - Game.Settings.Irc.ConnectAutomatically = false; - - Irc.Client = null; - - MaybeShowConnectPanel(); - } - } -} diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerBrowserLogic.cs index 154ba05412..e7a53f168f 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ServerBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ServerBrowserLogic.cs @@ -135,14 +135,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic showIncompatibleCheckbox.OnClick = () => { showIncompatible ^= true; RefreshServerList(); }; } - try - { - Game.LoadWidget(null, "SERVERBROWSER_IRC", panel.Get("IRC_ROOT"), new WidgetArgs()); - } - catch - { - Log.Write("debug", "Failed to load server browser IRC chrome layout"); - } + Game.LoadWidget(null, "GLOBALCHAT_PANEL", panel.Get("GLOBALCHAT_ROOT"), new WidgetArgs()); RefreshServerList(); diff --git a/mods/cnc/chrome/irc.yaml b/mods/cnc/chrome/globalchat.yaml similarity index 53% rename from mods/cnc/chrome/irc.yaml rename to mods/cnc/chrome/globalchat.yaml index 0165e25cf3..fb23ac5d0c 100644 --- a/mods/cnc/chrome/irc.yaml +++ b/mods/cnc/chrome/globalchat.yaml @@ -1,22 +1,36 @@ -Container@SERVERBROWSER_IRC: - Logic: IrcLogic - Width: 700 - Height: 250 +Container@GLOBALCHAT_PANEL: + Logic: GlobalChatLogic + Width: PARENT_RIGHT + Height: PARENT_BOTTOM Children: - Container@IRC_CONTAINER: + Container@GLOBALCHAT_MAIN_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Children: + Background@TOPIC: + Width: 565 + Height: 20 + Background: panel-transparent + Children: + Label@CHANNEL_TOPIC: + X: 10 + Y: 0-1 + Width: PARENT_RIGHT - 20 + Height: PARENT_BOTTOM + Font: TinyBold + Align: Center ScrollPanel@HISTORY_PANEL: Width: 565 - Height: PARENT_BOTTOM - 30 + Y: 19 + Height: PARENT_BOTTOM - 49 ItemSpacing: 5 - Label@HISTORY_TEMPLATE: - X: 5 - Width: 530 - Height: 25 - WordWrap: True - TextField@INPUT_BOX: + Children: + Label@HISTORY_TEMPLATE: + X: 5 + Width: 530 + Height: 25 + WordWrap: True + TextField@CHAT_TEXTFIELD: Y: PARENT_BOTTOM - 25 Width: 565 Height: 25 @@ -24,8 +38,20 @@ Container@SERVERBROWSER_IRC: X: 570 Width: 130 Height: PARENT_BOTTOM - 30 - Label@NICKNAME_TEMPLATE: - X: 5 + Children: + Container@NICKNAME_TEMPLATE: + Height: 20 + Width: PARENT_RIGHT-25 + Children: + Image@INDICATOR: + ImageCollection: lobby-bits + ImageName: admin + X: 4 + Y: 9 + Label@NICK: + X: 15 + Width: PARENT_RIGHT-15 + Height: 20 Button@DISCONNECT_BUTTON: X: 570 Y: PARENT_BOTTOM - 25 @@ -33,7 +59,7 @@ Container@SERVERBROWSER_IRC: Height: 25 Text: Disconnect Font: Bold - Background@IRC_CONNECT_BG: + Background@GLOBALCHAT_CONNECT_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Background: scrollpanel-bg @@ -48,7 +74,7 @@ Container@SERVERBROWSER_IRC: X: 200 Y: PARENT_BOTTOM / 4 + 35 Text: Nickname: - TextField@NICKNAME_BOX: + TextField@NICKNAME_TEXTFIELD: X: 270 Y: PARENT_BOTTOM / 4 + 25 Width: 150 diff --git a/mods/cnc/chrome/serverbrowser.yaml b/mods/cnc/chrome/serverbrowser.yaml index d3bc3cdab1..431baa27b8 100644 --- a/mods/cnc/chrome/serverbrowser.yaml +++ b/mods/cnc/chrome/serverbrowser.yaml @@ -18,9 +18,11 @@ Container@SERVERBROWSER_PANEL: Background: panel-black Y: 15 Children: - Container@IRC_ROOT: + Container@GLOBALCHAT_ROOT: X: 15 Y: 15 + Width: 700 + Height: 260 ScrollPanel@SERVER_LIST: X: 15 Y: 280 diff --git a/mods/cnc/metrics.yaml b/mods/cnc/metrics.yaml index 0e682d5684..b221d9049f 100644 --- a/mods/cnc/metrics.yaml +++ b/mods/cnc/metrics.yaml @@ -33,4 +33,6 @@ Metrics: WaitingGameColor: 0,255,0 IncompatibleWaitingGameColor: 50,205,50 GameStartedColor: 255,165,0 - IncompatibleGameStartedColor: 210,105,30 \ No newline at end of file + IncompatibleGameStartedColor: 210,105,30 + GlobalChatTextColor: 255,255,255 + GlobalChatNotificationColor: 211,211,211 diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 7e07ece016..979e3c7a53 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -122,7 +122,7 @@ ChromeLayout: ./mods/cnc/chrome/assetbrowser.yaml ./mods/cnc/chrome/missionbrowser.yaml ./mods/cnc/chrome/editor.yaml - ./mods/cnc/chrome/irc.yaml + ./mods/cnc/chrome/globalchat.yaml Voices: ./mods/cnc/audio/voices.yaml diff --git a/mods/d2k/metrics.yaml b/mods/d2k/metrics.yaml index 4d0f23b773..351ec7393b 100644 --- a/mods/d2k/metrics.yaml +++ b/mods/d2k/metrics.yaml @@ -33,4 +33,6 @@ Metrics: WaitingGameColor: 0,255,0 IncompatibleWaitingGameColor: 50,205,50 GameStartedColor: 255,165,0 - IncompatibleGameStartedColor: 210,105,30 \ No newline at end of file + IncompatibleGameStartedColor: 210,105,30 + GlobalChatTextColor: 255,255,255 + GlobalChatNotificationColor: 211,211,211 diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 28cf1f6583..2b616217e2 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -104,7 +104,7 @@ ChromeLayout: ./mods/d2k/chrome/missionbrowser.yaml ./mods/ra/chrome/confirmation-dialogs.yaml ./mods/ra/chrome/editor.yaml - ./mods/ra/chrome/irc.yaml + ./mods/ra/chrome/globalchat.yaml Weapons: ./mods/d2k/weapons.yaml diff --git a/mods/ra/chrome/irc.yaml b/mods/ra/chrome/globalchat.yaml similarity index 53% rename from mods/ra/chrome/irc.yaml rename to mods/ra/chrome/globalchat.yaml index 7926b90704..bae22051b9 100644 --- a/mods/ra/chrome/irc.yaml +++ b/mods/ra/chrome/globalchat.yaml @@ -1,22 +1,36 @@ -Container@SERVERBROWSER_IRC: - Logic: IrcLogic - Width: 700 - Height: 250 +Container@GLOBALCHAT_PANEL: + Logic: GlobalChatLogic + Width: PARENT_RIGHT + Height: PARENT_BOTTOM Children: - Container@IRC_CONTAINER: + Container@GLOBALCHAT_MAIN_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Children: - ScrollPanel@HISTORY_PANEL: + Background@TOPIC: Width: 565 - Height: PARENT_BOTTOM - 30 + Height: 20 + Background: dialog2 + Children: + Label@CHANNEL_TOPIC: + X: 10 + Y: 0-1 + Width: PARENT_RIGHT - 20 + Height: PARENT_BOTTOM + Font: TinyBold + Align: Center + ScrollPanel@HISTORY_PANEL: + Y:20 + Width: 565 + Height: PARENT_BOTTOM - 50 ItemSpacing: 5 - Label@HISTORY_TEMPLATE: - X: 5 - Width: 530 - Height: 25 - WordWrap: True - TextField@INPUT_BOX: + Children: + Label@HISTORY_TEMPLATE: + X: 5 + Width: 530 + Height: 25 + WordWrap: True + TextField@CHAT_TEXTFIELD: Y: PARENT_BOTTOM - 25 Width: 565 Height: 25 @@ -24,8 +38,20 @@ Container@SERVERBROWSER_IRC: X: 570 Width: 130 Height: PARENT_BOTTOM - 30 - Label@NICKNAME_TEMPLATE: - X: 5 + Children: + Container@NICKNAME_TEMPLATE: + Height: 20 + Width: PARENT_RIGHT-25 + Children: + Image@INDICATOR: + ImageCollection: lobby-bits + ImageName: admin + X: 4 + Y: 9 + Label@NICK: + X: 15 + Width: PARENT_RIGHT-15 + Height: 20 Button@DISCONNECT_BUTTON: X: 570 Y: PARENT_BOTTOM - 25 @@ -33,7 +59,7 @@ Container@SERVERBROWSER_IRC: Height: 25 Text: Disconnect Font: Bold - Background@IRC_CONNECT_BG: + Background@GLOBALCHAT_CONNECT_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Background: scrollpanel-bg @@ -48,7 +74,7 @@ Container@SERVERBROWSER_IRC: X: 200 Y: PARENT_BOTTOM / 4 + 35 Text: Nickname: - TextField@NICKNAME_BOX: + TextField@NICKNAME_TEXTFIELD: X: 270 Y: PARENT_BOTTOM / 4 + 25 Width: 150 diff --git a/mods/ra/chrome/serverbrowser.yaml b/mods/ra/chrome/serverbrowser.yaml index b748e65511..ceaa3c0cf6 100644 --- a/mods/ra/chrome/serverbrowser.yaml +++ b/mods/ra/chrome/serverbrowser.yaml @@ -118,10 +118,11 @@ Background@SERVERBROWSER_PANEL: Y: 40 Align: Right Height: 25 - Container@IRC_ROOT: + Container@GLOBALCHAT_ROOT: X: 20 Y: 370 - Width: 260 + Width: 700 + Height: 255 Label@PROGRESS_LABEL: X: (PARENT_RIGHT - WIDTH) / 2 Y: PARENT_BOTTOM / 2 - HEIGHT diff --git a/mods/ra/metrics.yaml b/mods/ra/metrics.yaml index 00e3fbb107..8e02da14b7 100644 --- a/mods/ra/metrics.yaml +++ b/mods/ra/metrics.yaml @@ -41,3 +41,5 @@ Metrics: IncompatibleWaitingGameColor: 50,205,50 GameStartedColor: 255,165,0 IncompatibleGameStartedColor: 210,105,30 + GlobalChatTextColor: 255,255,255 + GlobalChatNotificationColor: 211,211,211 diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 833f73638c..23c45be208 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -115,7 +115,7 @@ ChromeLayout: ./mods/ra/chrome/missionbrowser.yaml ./mods/ra/chrome/confirmation-dialogs.yaml ./mods/ra/chrome/editor.yaml - ./mods/ra/chrome/irc.yaml + ./mods/ra/chrome/globalchat.yaml Weapons: ./mods/ra/weapons/explosions.yaml diff --git a/mods/ts/metrics.yaml b/mods/ts/metrics.yaml index e8939a7ad4..10eab2bec8 100644 --- a/mods/ts/metrics.yaml +++ b/mods/ts/metrics.yaml @@ -33,4 +33,6 @@ Metrics: WaitingGameColor: 0,255,0 IncompatibleWaitingGameColor: 50,205,50 GameStartedColor: 255,165,0 - IncompatibleGameStartedColor: 210,105,30 \ No newline at end of file + IncompatibleGameStartedColor: 210,105,30 + GlobalChatTextColor: 255,255,255 + GlobalChatNotificationColor: 211,211,211 diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 1286af45f0..2ab7d5e2f6 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -169,7 +169,7 @@ ChromeLayout: ./mods/ra/chrome/missionbrowser.yaml ./mods/ra/chrome/confirmation-dialogs.yaml ./mods/ra/chrome/editor.yaml - ./mods/ra/chrome/irc.yaml + ./mods/ra/chrome/globalchat.yaml Voices: ./mods/ts/audio/voices.yaml