Merge pull request #9586 from pchote/irc-common

Overhaul IRC in preparation for the global chat UI
This commit is contained in:
Oliver Brakmann
2015-10-18 19:31:07 +02:00
24 changed files with 809 additions and 454 deletions

View File

@@ -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;
}
}

View File

@@ -78,9 +78,6 @@
<HintPath>..\thirdparty\download\ICSharpCode.SharpZipLib.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SmarIrc4net">
<HintPath>..\thirdparty\download\SmarIrc4net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
@@ -601,8 +598,6 @@
<Compile Include="Widgets\Logic\Installation\DownloadPackagesLogic.cs" />
<Compile Include="Widgets\Logic\Installation\InstallLogic.cs" />
<Compile Include="Widgets\Logic\Installation\InstallMusicLogic.cs" />
<Compile Include="Widgets\Logic\IrcLogic.cs" />
<Compile Include="Irc.cs" />
<Compile Include="Widgets\Logic\Lobby\ClientTooltipLogic.cs" />
<Compile Include="Widgets\Logic\Lobby\KickClientLogic.cs" />
<Compile Include="Widgets\Logic\Lobby\KickSpectatorsLogic.cs" />
@@ -707,6 +702,7 @@
<Compile Include="Traits\World\MusicPlaylist.cs" />
<Compile Include="Scripting\Global\LightingGlobal.cs" />
<Compile Include="Traits\SupportPowers\ProduceActorPower.cs" />
<Compile Include="Widgets\Logic\GlobalChatLogic.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>

View File

@@ -56,6 +56,12 @@ namespace OpenRA.Mods.Common.Widgets
// This is crap
public override int UsableWidth { get { return Bounds.Width - Bounds.Height; } } /* space for button */
public override void Hidden()
{
base.Hidden();
RemovePanel();
}
public override void Removed()
{
base.Removed();

View File

@@ -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<ScrollPanelWidget>("HISTORY_PANEL");
historyTemplate = historyPanel.Get<LabelWidget>("HISTORY_TEMPLATE");
nicknamePanel = widget.Get<ScrollPanelWidget>("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<TextFieldWidget>("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<TextFieldWidget>("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<ButtonWidget>("DISCONNECT_BUTTON");
disconnectButton.OnClick = Game.GlobalChat.Disconnect;
var connectAutomaticallyCheckBox = connectPanel.Get<CheckboxWidget>("CONNECT_AUTOMATICALLY_CHECKBOX");
connectAutomaticallyCheckBox.IsChecked = () => Game.Settings.Chat.ConnectAutomatically;
connectAutomaticallyCheckBox.OnClick = () => { Game.Settings.Chat.ConnectAutomatically ^= true; Game.Settings.Save(); };
var connectButton = connectPanel.Get<ButtonWidget>("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<LabelWidget>("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<Color>("GlobalChatNotificationColor") :
ChromeMetrics.Get<Color>("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<LabelWidget>("NICK");
name.GetText = () => client.Name;
name.IsVisible = () => true;
// TODO: Add custom image for voice
var indicator = item.Get<ImageWidget>("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;
}
}
}

View File

@@ -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<ScrollPanelWidget>("HISTORY_PANEL");
historyTemplate = widget.Get<LabelWidget>("HISTORY_TEMPLATE");
nicknamePanel = widget.Get<ScrollPanelWidget>("NICKNAME_PANEL");
nicknameTemplate = widget.Get<LabelWidget>("NICKNAME_TEMPLATE");
inputBox = widget.Get<TextFieldWidget>("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<TextFieldWidget>("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<ButtonWidget>("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<CheckboxWidget>("CONNECT_AUTOMATICALLY_CHECKBOX");
connectAutomaticallyCheckBox.IsChecked = () => Game.Settings.Irc.ConnectAutomatically;
connectAutomaticallyCheckBox.OnClick = () => Game.Settings.Irc.ConnectAutomatically ^= true;
var connectButton = connectBG.Get<ButtonWidget>("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();
}
}
}

View File

@@ -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();