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

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

371
OpenRA.Game/GlobalChat.cs Normal file
View File

@@ -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<string, ChatUser> Users = new ObservableSortedDictionary<string, ChatUser>(StringComparer.InvariantCultureIgnoreCase);
public readonly ObservableList<ChatMessage> History = new ObservableList<ChatMessage>();
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();
}
}
}

View File

@@ -84,6 +84,9 @@
<HintPath>..\thirdparty\download\MaxMind.Db.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SmarIrc4net">
<HintPath>..\thirdparty\download\SmarIrc4net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Activities\Activity.cs" />
@@ -246,6 +249,8 @@
<Compile Include="Renderer.cs" />
<Compile Include="Platform.cs" />
<Compile Include="GameSpeed.cs" />
<Compile Include="GlobalChat.cs" />
<Compile Include="Primitives\ObservableList.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="FileSystem\D2kSoundResources.cs" />

View File

@@ -0,0 +1,118 @@
#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;
namespace OpenRA.Primitives
{
public class ObservableList<T> : IList<T>, IObservableCollection
{
protected IList<T> innerList;
public event Action<object> OnAdd = k => { };
public event Action<object> OnRemove = k => { };
// TODO Workaround for https://github.com/OpenRA/OpenRA/issues/6101
#pragma warning disable 67
public event Action<int> OnRemoveAt = i => { };
public event Action<object, object> OnSet = (o, n) => { };
#pragma warning restore
public event Action OnRefresh = () => { };
protected void FireOnRefresh()
{
OnRefresh();
}
public ObservableList()
{
innerList = new List<T>();
}
public virtual void Add(T item)
{
innerList.Add(item);
OnAdd(item);
}
public bool Remove(T item)
{
var found = innerList.Remove(item);
if (found)
OnRemove(item);
return found;
}
public void Clear()
{
innerList.Clear();
OnRefresh();
}
public void Insert(int index, T item)
{
innerList.Insert(index, item);
OnRefresh();
}
public int Count { get { return innerList.Count; } }
public int IndexOf(T item) { return innerList.IndexOf(item); }
public bool Contains(T item) { return innerList.Contains(item); }
public void RemoveAt(int index)
{
innerList.RemoveAt(index);
OnRemoveAt(index);
}
public T this[int index]
{
get
{
return innerList[index];
}
set
{
var oldValue = innerList[index];
innerList[index] = value;
OnSet(oldValue, value);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
innerList.CopyTo(array, arrayIndex);
}
public IEnumerator<T> GetEnumerator()
{
return innerList.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return innerList.GetEnumerator();
}
public IEnumerable ObservedItems
{
get { return innerList; }
}
public bool IsReadOnly
{
get { return innerList.IsReadOnly; }
}
}
}

View File

@@ -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<string, object> Sections;
@@ -332,7 +332,7 @@ namespace OpenRA
{ "Server", Server },
{ "Debug", Debug },
{ "Keys", Keys },
{ "Irc", Irc }
{ "Chat", Chat }
};
// Override fieldloader to ignore invalid entries

View File

@@ -46,7 +46,7 @@ namespace OpenRA.Widgets
{
var window = Game.ModData.WidgetLoader.LoadWidget(args, Root, id);
if (WindowList.Count > 0)
Root.RemoveChild(WindowList.Peek());
Root.HideChild(WindowList.Peek());
WindowList.Push(window);
return window;
}
@@ -448,12 +448,32 @@ namespace OpenRA.Widgets
}
}
public virtual void HideChild(Widget child)
{
if (child != null)
{
Children.Remove(child);
child.Hidden();
}
}
public virtual void RemoveChildren()
{
while (Children.Count > 0)
RemoveChild(Children[Children.Count - 1]);
}
public virtual void Hidden()
{
// Using the forced versions because the widgets
// have been removed
ForceYieldKeyboardFocus();
ForceYieldMouseFocus();
foreach (var c in Children.OfType<Widget>().Reverse())
c.Hidden();
}
public virtual void Removed()
{
// Using the forced versions because the widgets
@@ -463,6 +483,11 @@ namespace OpenRA.Widgets
foreach (var c in Children.OfType<Widget>().Reverse())
c.Removed();
if (LogicObjects != null)
foreach (var l in LogicObjects)
if (l is IDisposable)
((IDisposable)l).Dispose();
}
public Widget GetOrNull(string id)