diff --git a/OpenRA.Game/CryptoUtil.cs b/OpenRA.Game/CryptoUtil.cs index 219adca331b8..8a3b973f4f33 100644 --- a/OpenRA.Game/CryptoUtil.cs +++ b/OpenRA.Game/CryptoUtil.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -18,6 +19,228 @@ namespace OpenRA { public static class CryptoUtil { + // Fixed byte pattern for the OID header + static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 }; + + public static string PublicKeyFingerprint(RSAParameters parameters) + { + // Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes + return SHA1Hash(parameters.Modulus.Append(parameters.Exponent).ToArray()); + } + + public static string EncodePEMPublicKey(RSAParameters parameters) + { + var data = Convert.ToBase64String(EncodePublicKey(parameters)); + var output = new StringBuilder(); + output.AppendLine("-----BEGIN PUBLIC KEY-----"); + for (var i = 0; i < data.Length; i += 64) + output.AppendLine(data.Substring(i, Math.Min(64, data.Length - i))); + output.Append("-----END PUBLIC KEY-----"); + + return output.ToString(); + } + + public static RSAParameters DecodePEMPublicKey(string key) + { + try + { + // Reconstruct original key data + var lines = key.Split('\n'); + var data = Convert.FromBase64String(lines.Skip(1).Take(lines.Length - 2).JoinWith("")); + + // Pull the modulus and exponent bytes out of the ASN.1 tree + // Expect this to blow up if the key is not correctly formatted + using (var s = new MemoryStream(data)) + { + // SEQUENCE + s.ReadByte(); + ReadTLVLength(s); + + // SEQUENCE -> fixed header junk + s.ReadByte(); + var headerLength = ReadTLVLength(s); + s.Position += headerLength; + + // SEQUENCE -> BIT_STRING + s.ReadByte(); + ReadTLVLength(s); + s.ReadByte(); + + // SEQUENCE -> BIT_STRING -> SEQUENCE + s.ReadByte(); + ReadTLVLength(s); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus) + s.ReadByte(); + var modulusLength = ReadTLVLength(s); + s.ReadByte(); + var modulus = s.ReadBytes(modulusLength - 1); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent) + s.ReadByte(); + var exponentLength = ReadTLVLength(s); + s.ReadByte(); + var exponent = s.ReadBytes(exponentLength - 1); + + return new RSAParameters + { + Modulus = modulus, + Exponent = exponent + }; + } + } + catch (Exception e) + { + throw new InvalidDataException("Invalid PEM public key", e); + } + } + + static byte[] EncodePublicKey(RSAParameters parameters) + { + using (var stream = new MemoryStream()) + { + var writer = new BinaryWriter(stream); + + var modExpLength = TripletFullLength(parameters.Modulus.Length + 1) + TripletFullLength(parameters.Exponent.Length + 1); + var bitStringLength = TripletFullLength(modExpLength + 1); + var sequenceLength = TripletFullLength(bitStringLength + OIDHeader.Length); + + // SEQUENCE + writer.Write((byte)0x30); + WriteTLVLength(writer, sequenceLength); + + // SEQUENCE -> fixed header junk + writer.Write(OIDHeader); + + // SEQUENCE -> BIT_STRING + writer.Write((byte)0x03); + WriteTLVLength(writer, bitStringLength); + writer.Write((byte)0x00); + + // SEQUENCE -> BIT_STRING -> SEQUENCE + writer.Write((byte)0x30); + WriteTLVLength(writer, modExpLength); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER + // Modulus is padded with a zero to avoid issues with the sign bit + writer.Write((byte)0x02); + WriteTLVLength(writer, parameters.Modulus.Length + 1); + writer.Write((byte)0); + writer.Write(parameters.Modulus); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER + // Exponent is padded with a zero to avoid issues with the sign bit + writer.Write((byte)0x02); + WriteTLVLength(writer, parameters.Exponent.Length + 1); + writer.Write((byte)0); + writer.Write(parameters.Exponent); + + return stream.ToArray(); + } + } + + static void WriteTLVLength(BinaryWriter writer, int length) + { + if (length < 0x80) + { + // Length < 128 is stored in a single byte + writer.Write((byte)length); + } + else + { + // If 128 <= length < 256**128 first byte encodes number of bytes required to hold the length + // High-bit is set as a flag to use this long-form encoding + var lengthBytes = BitConverter.GetBytes(length).Reverse().SkipWhile(b => b == 0).ToArray(); + writer.Write((byte)(0x80 | lengthBytes.Length)); + writer.Write(lengthBytes); + } + } + + static int ReadTLVLength(Stream s) + { + var length = s.ReadByte(); + if (length < 0x80) + return length; + + var data = new byte[4]; + s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4)); + return BitConverter.ToInt32(data.ToArray(), 0); + } + + static int TripletFullLength(int dataLength) + { + if (dataLength < 0x80) + return 2 + dataLength; + + return 2 + dataLength + BitConverter.GetBytes(dataLength).Reverse().SkipWhile(b => b == 0).Count(); + } + + public static string DecryptString(RSAParameters parameters, string data) + { + try + { + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(parameters); + return Encoding.UTF8.GetString(rsa.Decrypt(Convert.FromBase64String(data), false)); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to decrypt string with exception: {0}", e); + Console.WriteLine("String decryption failed: {0}", e); + return null; + } + } + + public static string Sign(RSAParameters parameters, string data) + { + return Sign(parameters, Encoding.UTF8.GetBytes(data)); + } + + public static string Sign(RSAParameters parameters, byte[] data) + { + try + { + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(parameters); + using (var csp = SHA1.Create()) + return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"))); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to sign string with exception: {0}", e); + Console.WriteLine("String signing failed: {0}", e); + return null; + } + } + + public static bool VerifySignature(RSAParameters parameters, string data, string signature) + { + return VerifySignature(parameters, Encoding.UTF8.GetBytes(data), signature); + } + + public static bool VerifySignature(RSAParameters parameters, byte[] data, string signature) + { + try + { + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(parameters); + using (var csp = SHA1.Create()) + return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature)); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to verify signature with exception: {0}", e); + Console.WriteLine("Signature validation failed: {0}", e); + return false; + } + } + public static string SHA1Hash(Stream data) { using (var csp = SHA1.Create()) diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index abd8c1b100c6..f0beefa47d48 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -55,6 +55,7 @@ public static class Game public static bool BenchmarkMode = false; public static string EngineVersion { get; private set; } + public static LocalPlayerProfile LocalPlayerProfile; static Task discoverNat; static bool takeScreenshot = false; @@ -407,6 +408,8 @@ public static void InitializeMod(string mod, Arguments args) ModData = new ModData(Mods[mod], Mods, true); + LocalPlayerProfile = new LocalPlayerProfile(Platform.ResolvePath(Path.Combine("^", Settings.Game.AuthProfile)), ModData.Manifest.Get()); + if (!ModData.LoadScreen.BeforeLoad()) return; @@ -882,5 +885,10 @@ public static bool IsCurrentWorld(World world) { return OrderManager != null && OrderManager.World == world && !world.Disposing; } + + public static bool SetClipboardText(string text) + { + return Renderer.Window.SetClipboardText(text); + } } } diff --git a/OpenRA.Game/LocalPlayerProfile.cs b/OpenRA.Game/LocalPlayerProfile.cs new file mode 100644 index 000000000000..614bd35d4901 --- /dev/null +++ b/OpenRA.Game/LocalPlayerProfile.cs @@ -0,0 +1,185 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using OpenRA.Network; + +namespace OpenRA +{ + public sealed class LocalPlayerProfile + { + const int AuthKeySize = 2048; + public enum LinkState { Uninitialized, GeneratingKeys, Unlinked, CheckingLink, ConnectionFailed, Linked } + + public LinkState State { get { return innerState; } } + public string Fingerprint { get { return innerFingerprint; } } + public string PublicKey { get { return innerPublicKey; } } + + public PlayerProfile ProfileData { get { return innerData; } } + + volatile LinkState innerState; + volatile PlayerProfile innerData; + volatile string innerFingerprint; + volatile string innerPublicKey; + + RSAParameters parameters; + readonly string filePath; + readonly PlayerDatabase playerDatabase; + + public LocalPlayerProfile(string filePath, PlayerDatabase playerDatabase) + { + this.filePath = filePath; + this.playerDatabase = playerDatabase; + innerState = LinkState.Uninitialized; + + try + { + if (File.Exists(filePath)) + { + using (var rsa = new RSACryptoServiceProvider()) + { + using (var data = File.OpenRead(filePath)) + { + var keyData = Convert.FromBase64String(data.ReadAllText()); + rsa.FromXmlString(new string(Encoding.ASCII.GetChars(keyData))); + } + + parameters = rsa.ExportParameters(true); + innerPublicKey = CryptoUtil.EncodePEMPublicKey(parameters); + innerFingerprint = CryptoUtil.PublicKeyFingerprint(parameters); + innerState = LinkState.Unlinked; + } + } + } + catch (Exception e) + { + Console.WriteLine("Failed to load keys: {0}", e); + Log.Write("debug", "Failed to load player keypair from `{0}` with exception: {1}", filePath, e); + } + } + + public void RefreshPlayerData(Action onComplete = null) + { + if (State != LinkState.Unlinked && State != LinkState.Linked && State != LinkState.ConnectionFailed) + return; + + Action onQueryComplete = i => + { + try + { + innerState = LinkState.Unlinked; + + if (i.Error != null) + { + innerState = LinkState.ConnectionFailed; + return; + } + + var yaml = MiniYaml.FromString(Encoding.UTF8.GetString(i.Result)).First(); + if (yaml.Key == "Player") + { + innerData = FieldLoader.Load(yaml.Value); + if (innerData.KeyRevoked) + { + Log.Write("debug", "Revoking key with fingerprint {0}", Fingerprint); + DeleteKeypair(); + } + else + innerState = LinkState.Linked; + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to parse player data result with exception: {0}", e); + innerState = LinkState.ConnectionFailed; + } + finally + { + if (onComplete != null) + onComplete(); + } + }; + + innerState = LinkState.CheckingLink; + new Download(playerDatabase.Profile + Fingerprint, _ => { }, onQueryComplete); + } + + public void GenerateKeypair() + { + if (State != LinkState.Uninitialized) + return; + + innerState = LinkState.GeneratingKeys; + new Task(() => + { + try + { + var rsa = new RSACryptoServiceProvider(AuthKeySize); + parameters = rsa.ExportParameters(true); + innerPublicKey = CryptoUtil.EncodePEMPublicKey(parameters); + innerFingerprint = CryptoUtil.PublicKeyFingerprint(parameters); + + var data = Convert.ToBase64String(Encoding.ASCII.GetBytes(rsa.ToXmlString(true))); + File.WriteAllText(filePath, data); + + innerState = LinkState.Unlinked; + } + catch (Exception e) + { + Log.Write("debug", "Failed to generate keypair with exception: {1}", e); + Console.WriteLine("Key generation failed: {0}", e); + + innerState = LinkState.Uninitialized; + } + }).Start(); + } + + public void DeleteKeypair() + { + try + { + File.Delete(filePath); + } + catch (Exception e) + { + Log.Write("debug", "Failed to delete keypair with exception: {1}", e); + Console.WriteLine("Key deletion failed: {0}", e); + } + + innerState = LinkState.Uninitialized; + parameters = new RSAParameters(); + innerFingerprint = null; + innerData = null; + } + + public string Sign(params string[] data) + { + if (State != LinkState.Linked) + return null; + + return CryptoUtil.Sign(parameters, data.Where(x => !string.IsNullOrEmpty(x)).JoinWith(string.Empty)); + } + + public string DecryptString(string data) + { + if (State != LinkState.Linked) + return null; + + return CryptoUtil.DecryptString(parameters, data); + } + } +} \ No newline at end of file diff --git a/OpenRA.Game/Network/Handshake.cs b/OpenRA.Game/Network/Handshake.cs index f612cb7667d0..d7345f7dd737 100644 --- a/OpenRA.Game/Network/Handshake.cs +++ b/OpenRA.Game/Network/Handshake.cs @@ -19,6 +19,7 @@ public class HandshakeRequest public string Mod; public string Version; public string Map; + public string AuthToken; public static HandshakeRequest Deserialize(string data) { @@ -40,6 +41,11 @@ public class HandshakeResponse public string Mod; public string Version; public string Password; + + // For player authentication + public string Fingerprint; + public string AuthSignature; + [FieldLoader.Ignore] public Session.Client Client; public static HandshakeResponse Deserialize(string data) @@ -68,7 +74,7 @@ public string Serialize() { var data = new List(); data.Add(new MiniYamlNode("Handshake", null, - new string[] { "Mod", "Version", "Password" }.Select(p => FieldSaver.SaveField(this, p)).ToList())); + new string[] { "Mod", "Version", "Password", "Fingerprint", "AuthSignature" }.Select(p => FieldSaver.SaveField(this, p)).ToList())); data.Add(new MiniYamlNode("Client", FieldSaver.Save(Client))); return data.WriteToString(); diff --git a/OpenRA.Game/Network/Session.cs b/OpenRA.Game/Network/Session.cs index de9ea4ccb38e..9f08efb1d06d 100644 --- a/OpenRA.Game/Network/Session.cs +++ b/OpenRA.Game/Network/Session.cs @@ -126,6 +126,9 @@ public static Client Deserialize(MiniYaml data) public bool IsInvalid { get { return State == ClientState.Invalid; } } public bool IsObserver { get { return Slot == null; } } + // Linked to the online player database + public string Fingerprint; + public MiniYamlNode Serialize() { return new MiniYamlNode("Client@{0}".F(Index), FieldSaver.Save(this)); diff --git a/OpenRA.Game/Network/UnitOrders.cs b/OpenRA.Game/Network/UnitOrders.cs index 31cacf920f73..855988a20b8c 100644 --- a/OpenRA.Game/Network/UnitOrders.cs +++ b/OpenRA.Game/Network/UnitOrders.cs @@ -9,9 +9,12 @@ */ #endregion +using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Security.Cryptography; +using System.Text; using OpenRA.Traits; namespace OpenRA.Network @@ -175,14 +178,19 @@ internal static void ProcessOrder(OrderManager orderManager, World world, int cl State = Session.ClientState.Invalid }; + var localProfile = Game.LocalPlayerProfile; var response = new HandshakeResponse() { Client = info, Mod = mod.Id, Version = mod.Metadata.Version, - Password = orderManager.Password + Password = orderManager.Password, + Fingerprint = localProfile.Fingerprint }; + if (request.AuthToken != null && response.Fingerprint != null) + response.AuthSignature = localProfile.Sign(request.AuthToken); + orderManager.IssueOrder(Order.HandshakeResponse(response.Serialize())); break; } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 420bf790f668..4ee3719e2905 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -261,6 +261,9 @@ + + + diff --git a/OpenRA.Game/PlayerDatabase.cs b/OpenRA.Game/PlayerDatabase.cs new file mode 100644 index 000000000000..cc00c66574a8 --- /dev/null +++ b/OpenRA.Game/PlayerDatabase.cs @@ -0,0 +1,87 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using OpenRA.Graphics; + +namespace OpenRA +{ + public class PlayerDatabase : IGlobalModData + { + public readonly string Profile = "https://forum.openra.net/openra/info/"; + + [FieldLoader.Ignore] + readonly object syncObject = new object(); + + [FieldLoader.Ignore] + readonly Dictionary spriteCache = new Dictionary(); + + // 128x128 is large enough for 25 unique 24x24 sprites + [FieldLoader.Ignore] + SheetBuilder sheetBuilder; + + public PlayerBadge LoadBadge(MiniYaml yaml) + { + if (sheetBuilder == null) + { + sheetBuilder = new SheetBuilder(SheetType.BGRA, 128); + + // We must manually force the buffer creation to avoid a crash + // that is indirectly triggered by rendering from a Sheet that + // has not yet been written to. + sheetBuilder.Current.CreateBuffer(); + } + + var labelNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Label"); + var icon24Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon24"); + if (labelNode == null || icon24Node == null) + return null; + + Sprite sprite; + lock (syncObject) + { + if (!spriteCache.TryGetValue(icon24Node.Value.Value, out sprite)) + { + sprite = spriteCache[icon24Node.Value.Value] = sheetBuilder.Allocate(new Size(24, 24)); + + Action onComplete = i => + { + if (i.Error != null) + return; + + try + { + var icon = new Bitmap(new MemoryStream(i.Result)); + if (icon.Width == 24 && icon.Height == 24) + { + Game.RunAfterTick(() => + { + Util.FastCopyIntoSprite(sprite, icon); + sprite.Sheet.CommitBufferedData(); + }); + } + } + catch { } + }; + + new Download(icon24Node.Value.Value, _ => { }, onComplete); + } + } + + return new PlayerBadge(labelNode.Value.Value, sprite); + } + } +} diff --git a/OpenRA.Game/PlayerProfile.cs b/OpenRA.Game/PlayerProfile.cs new file mode 100644 index 000000000000..1c18d19bbaa6 --- /dev/null +++ b/OpenRA.Game/PlayerProfile.cs @@ -0,0 +1,69 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; + +namespace OpenRA +{ + public class PlayerProfile + { + public readonly string Fingerprint; + public readonly string PublicKey; + public readonly bool KeyRevoked; + + public readonly int ProfileID; + public readonly string ProfileName; + public readonly string ProfileRank = "Registered Player"; + + [FieldLoader.LoadUsing("LoadBadges")] + public readonly List Badges; + + static object LoadBadges(MiniYaml yaml) + { + var badges = new List(); + + var badgesNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Badges"); + if (badgesNode != null) + { + try + { + var playerDatabase = Game.ModData.Manifest.Get(); + foreach (var badgeNode in badgesNode.Value.Nodes) + { + var badge = playerDatabase.LoadBadge(badgeNode.Value); + if (badge != null) + badges.Add(badge); + } + } + catch + { + // Discard badges on error + } + } + + return badges; + } + } + + public class PlayerBadge + { + public readonly string Label; + public readonly Sprite Icon24; + + public PlayerBadge(string label, Sprite icon24) + { + Label = label; + Icon24 = icon24; + } + } +} diff --git a/OpenRA.Game/Server/Connection.cs b/OpenRA.Game/Server/Connection.cs index ba60fecaa250..b341c6f2b43d 100644 --- a/OpenRA.Game/Server/Connection.cs +++ b/OpenRA.Game/Server/Connection.cs @@ -33,6 +33,7 @@ public class Connection /* client data */ public int PlayerIndex; + public string AuthToken; public byte[] PopBytes(int n) { diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 6f9772219612..058b4b50fd2f 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using OpenRA.Graphics; using OpenRA.Network; @@ -57,9 +58,13 @@ public class Server readonly int randomSeed; readonly TcpListener listener; readonly TypeDictionary serverTraits = new TypeDictionary(); + readonly PlayerDatabase playerDatabase; protected volatile ServerState internalState = ServerState.WaitingPlayers; + volatile ActionQueue delayedActions = new ActionQueue(); + int waitingForAuthenticationCallback = 0; + public ServerState State { get { return internalState; } @@ -132,6 +137,8 @@ public Server(IPEndPoint endpoint, ServerSettings settings, ModData modData, boo ModData = modData; + playerDatabase = modData.Manifest.Get(); + randomSeed = (int)DateTime.Now.ToBinary(); if (UPnP.Status == UPnPStatus.Enabled) @@ -173,8 +180,9 @@ public Server(IPEndPoint endpoint, ServerSettings settings, ModData modData, boo checkRead.AddRange(Conns.Select(c => c.Socket)); checkRead.AddRange(PreConns.Select(c => c.Socket)); + var localTimeout = waitingForAuthenticationCallback > 0 ? 100000 : timeout; if (checkRead.Count > 0) - Socket.Select(checkRead, null, null, timeout); + Socket.Select(checkRead, null, null, localTimeout); if (State == ServerState.ShuttingDown) { @@ -202,6 +210,8 @@ public Server(IPEndPoint endpoint, ServerSettings settings, ModData modData, boo conn.ReadData(this); } + delayedActions.PerformActions(0); + foreach (var t in serverTraits.WithInterface()) t.Tick(this); @@ -255,8 +265,13 @@ void AcceptConnection() newConn.Socket.Blocking = false; newConn.Socket.NoDelay = true; - // assign the player number. + // Validate player identity by asking them to sign a random blob of data + // which we can then verify against the player public key database + var token = Convert.ToBase64String(OpenRA.Exts.MakeArray(256, _ => (byte)Random.Next())); + + // Assign the player number. newConn.PlayerIndex = ChooseFreePlayerIndex(); + newConn.AuthToken = token; SendData(newConn.Socket, BitConverter.GetBytes(ProtocolVersion.Version)); SendData(newConn.Socket, BitConverter.GetBytes(newConn.PlayerIndex)); PreConns.Add(newConn); @@ -266,7 +281,8 @@ void AcceptConnection() { Mod = ModData.Manifest.Id, Version = ModData.Manifest.Metadata.Version, - Map = LobbyInfo.GlobalSettings.Map + Map = LobbyInfo.GlobalSettings.Map, + AuthToken = token }; DispatchOrdersToClient(newConn, 0, 0, new ServerOrder("HandshakeRequest", request.Serialize()).Serialize()); @@ -359,50 +375,133 @@ void ValidateClient(Connection newConn, string data) return; } - // Promote connection to a valid client - PreConns.Remove(newConn); - Conns.Add(newConn); - LobbyInfo.Clients.Add(client); - newConn.Validated = true; + Action completeConnection = () => + { + // Promote connection to a valid client + PreConns.Remove(newConn); + Conns.Add(newConn); + LobbyInfo.Clients.Add(client); + newConn.Validated = true; + + var clientPing = new Session.ClientPing { Index = client.Index }; + LobbyInfo.ClientPings.Add(clientPing); - var clientPing = new Session.ClientPing { Index = client.Index }; - LobbyInfo.ClientPings.Add(clientPing); + Log.Write("server", "Client {0}: Accepted connection from {1}.", + newConn.PlayerIndex, newConn.Socket.RemoteEndPoint); - Log.Write("server", "Client {0}: Accepted connection from {1}.", - newConn.PlayerIndex, newConn.Socket.RemoteEndPoint); + if (client.Fingerprint != null) + Log.Write("server", "Client {0}: Player fingerprint is {1}.", + newConn.PlayerIndex, client.Fingerprint); - foreach (var t in serverTraits.WithInterface()) - t.ClientJoined(this, newConn); + foreach (var t in serverTraits.WithInterface()) + t.ClientJoined(this, newConn); - SyncLobbyInfo(); + SyncLobbyInfo(); - Log.Write("server", "{0} ({1}) has joined the game.", - client.Name, newConn.Socket.RemoteEndPoint); + Log.Write("server", "{0} ({1}) has joined the game.", + client.Name, newConn.Socket.RemoteEndPoint); + + // Report to all other players + SendMessage("{0} has joined the game.".F(client.Name), newConn); + + // Send initial ping + SendOrderTo(newConn, "Ping", Game.RunTime.ToString(CultureInfo.InvariantCulture)); + + if (Dedicated) + { + var motdFile = Platform.ResolvePath(Platform.SupportDirPrefix, "motd.txt"); + if (!File.Exists(motdFile)) + File.WriteAllText(motdFile, "Welcome, have fun and good luck!"); + + var motd = File.ReadAllText(motdFile); + if (!string.IsNullOrEmpty(motd)) + SendOrderTo(newConn, "Message", motd); + } - // Report to all other players - SendMessage("{0} has joined the game.".F(client.Name), newConn); + if (Map.DefinesUnsafeCustomRules) + SendOrderTo(newConn, "Message", "This map contains custom rules. Game experience may change."); - // Send initial ping - SendOrderTo(newConn, "Ping", Game.RunTime.ToString(CultureInfo.InvariantCulture)); + if (!LobbyInfo.GlobalSettings.EnableSingleplayer) + SendOrderTo(newConn, "Message", TwoHumansRequiredText); + else if (Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots)) + SendOrderTo(newConn, "Message", "Bots have been disabled on this map."); + }; - if (Dedicated) + if (!string.IsNullOrEmpty(handshake.Fingerprint) && !string.IsNullOrEmpty(handshake.AuthSignature)) { - var motdFile = Platform.ResolvePath(Platform.SupportDirPrefix, "motd.txt"); - if (!File.Exists(motdFile)) - File.WriteAllText(motdFile, "Welcome, have fun and good luck!"); + waitingForAuthenticationCallback++; - var motd = File.ReadAllText(motdFile); - if (!string.IsNullOrEmpty(motd)) - SendOrderTo(newConn, "Message", motd); - } + Action onQueryComplete = i => + { + PlayerProfile profile = null; - if (Map.DefinesUnsafeCustomRules) - SendOrderTo(newConn, "Message", "This map contains custom rules. Game experience may change."); + if (i.Error == null) + { + try + { + var yaml = MiniYaml.FromString(Encoding.UTF8.GetString(i.Result)).First(); + if (yaml.Key == "Player") + { + profile = FieldLoader.Load(yaml.Value); + + var publicKey = Encoding.ASCII.GetString(Convert.FromBase64String(profile.PublicKey)); + var parameters = CryptoUtil.DecodePEMPublicKey(publicKey); + if (!profile.KeyRevoked && CryptoUtil.VerifySignature(parameters, newConn.AuthToken, handshake.AuthSignature)) + { + client.Fingerprint = handshake.Fingerprint; + Log.Write("server", "{0} authenticated as {1} (UID {2})", newConn.Socket.RemoteEndPoint, + profile.ProfileName, profile.ProfileID); + } + else if (profile.KeyRevoked) + Log.Write("server", "{0} failed to authenticate as {1} (key revoked)", newConn.Socket.RemoteEndPoint, handshake.Fingerprint); + else + Log.Write("server", "{0} failed to authenticate as {1} (signature verification failed)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint); + } + else + Log.Write("server", "{0} failed to authenticate as {1} (invalid server response: `{2}` is not `Player`)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint, yaml.Key); + } + catch (Exception ex) + { + Log.Write("server", "{0} failed to authenticate as {1} (exception occurred)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint); + Log.Write("server", ex.ToString()); + } + } + else + Log.Write("server", "{0} failed to authenticate as {1} (server error: `{2}`)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint, i.Error); - if (!LobbyInfo.GlobalSettings.EnableSingleplayer) - SendOrderTo(newConn, "Message", TwoHumansRequiredText); - else if (Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots)) - SendOrderTo(newConn, "Message", "Bots have been disabled on this map."); + delayedActions.Add(() => + { + if (Dedicated && Settings.RequireAuthIDs.Any() && + (profile == null || !Settings.RequireAuthIDs.Contains(profile.ProfileID))) + { + Log.Write("server", "Rejected connection from {0}; Not in server whitelist.", newConn.Socket.RemoteEndPoint); + SendOrderTo(newConn, "ServerError", "You are not authenticated for this server"); + DropClient(newConn); + } + else + completeConnection(); + + waitingForAuthenticationCallback--; + }, 0); + }; + + new Download(playerDatabase.Profile + handshake.Fingerprint, _ => { }, onQueryComplete); + } + else + { + if (Dedicated && Settings.RequireAuthIDs.Any()) + { + Log.Write("server", "Rejected connection from {0}; Not authenticated and whitelist is set.", newConn.Socket.RemoteEndPoint); + SendOrderTo(newConn, "ServerError", "You are not authenticated for this server"); + DropClient(newConn); + } + else + completeConnection(); + } } catch (Exception ex) { diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index aa313b2ca583..58587b76b80d 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -59,6 +59,9 @@ public class ServerSettings [Desc("Takes a comma separated list of IP addresses that are not allowed to join.")] public string[] Ban = { }; + [Desc("If non-empty, only allow authenticated players with these user IDs to join.")] + public int[] RequireAuthIDs = { }; + [Desc("For dedicated servers only, controls whether a game can be started with just one human player in the lobby.")] public bool EnableSingleplayer = false; @@ -183,6 +186,9 @@ public class GameSettings public bool AllowDownloading = true; + [Desc("Filename of the authentication profile to use.")] + public string AuthProfile = "player.oraid"; + public bool AllowZoom = true; public Modifiers ZoomModifier = Modifiers.Ctrl; diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 3d44d2ea6b70..50db00bcd6dd 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -656,7 +656,7 @@ - + @@ -917,6 +917,7 @@ + diff --git a/OpenRA.Mods.Common/Widgets/ButtonWidget.cs b/OpenRA.Mods.Common/Widgets/ButtonWidget.cs index c23466c29477..c41c8aa0a4a5 100644 --- a/OpenRA.Mods.Common/Widgets/ButtonWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ButtonWidget.cs @@ -11,6 +11,7 @@ using System; using System.Drawing; +using System.Linq; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets @@ -240,7 +241,8 @@ public override void Draw() var position = GetTextPosition(textSize, rb); - DrawBackground(rb, disabled, Depressed, Ui.MouseOverWidget == this, highlighted); + var hover = Ui.MouseOverWidget == this || Children.Any(c => c == Ui.MouseOverWidget); + DrawBackground(rb, disabled, Depressed, hover, highlighted); if (Contrast) font.DrawTextWithContrast(text, position + stateOffset, disabled ? colordisabled : color, bgDark, bgLight, 2); diff --git a/OpenRA.Mods.Common/Widgets/ClientTooltipRegionWidget.cs b/OpenRA.Mods.Common/Widgets/ClientTooltipRegionWidget.cs index 9ae59e675f20..9f78d5bbef29 100644 --- a/OpenRA.Mods.Common/Widgets/ClientTooltipRegionWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ClientTooltipRegionWidget.cs @@ -10,6 +10,7 @@ #endregion using System; +using OpenRA.Graphics; using OpenRA.Network; using OpenRA.Widgets; @@ -17,11 +18,14 @@ namespace OpenRA.Mods.Common.Widgets { public class ClientTooltipRegionWidget : Widget { - public readonly string Template; public readonly string TooltipContainer; - Lazy tooltipContainer; + readonly Lazy tooltipContainer; + + public string Template; + OrderManager orderManager; - int clientIndex; + WorldRenderer worldRenderer; + Session.Client client; public ClientTooltipRegionWidget() { @@ -35,28 +39,37 @@ protected ClientTooltipRegionWidget(ClientTooltipRegionWidget other) TooltipContainer = other.TooltipContainer; tooltipContainer = Exts.Lazy(() => Ui.Root.Get(TooltipContainer)); orderManager = other.orderManager; - clientIndex = other.clientIndex; + worldRenderer = other.worldRenderer; + client = other.client; } public override Widget Clone() { return new ClientTooltipRegionWidget(this); } - public void Bind(OrderManager orderManager, int clientIndex) + public void Bind(OrderManager orderManager, WorldRenderer worldRenderer, Session.Client client) { this.orderManager = orderManager; - this.clientIndex = clientIndex; + this.worldRenderer = worldRenderer; + this.client = client; } public override void MouseEntered() { if (TooltipContainer == null) return; - tooltipContainer.Value.SetTooltip(Template, new WidgetArgs() { { "orderManager", orderManager }, { "clientIndex", clientIndex } }); + + tooltipContainer.Value.SetTooltip(Template, new WidgetArgs() + { + { "orderManager", orderManager }, + { "worldRenderer", worldRenderer }, + { "client", client } + }); } public override void MouseExited() { if (TooltipContainer == null) return; + tooltipContainer.Value.RemoveTooltip(); } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs index 67bbfbba3b89..c123d78f65a9 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs @@ -9,9 +9,9 @@ */ #endregion -using System.Collections.Generic; using System.Drawing; using System.Linq; +using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Network; using OpenRA.Primitives; @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic class GameInfoStatsLogic : ChromeLogic { [ObjectCreator.UseCtor] - public GameInfoStatsLogic(Widget widget, World world, OrderManager orderManager) + public GameInfoStatsLogic(Widget widget, World world, OrderManager orderManager, WorldRenderer worldRenderer) { var player = world.RenderPlayer ?? world.LocalPlayer; var playerPanel = widget.Get("PLAYER_LIST"); @@ -87,7 +87,8 @@ public GameInfoStatsLogic(Widget widget, World world, OrderManager orderManager) var pp = p.First; var client = world.LobbyInfo.ClientWithIndex(pp.ClientIndex); var item = playerTemplate.Clone(); - LobbyUtils.SetupClientWidget(item, client, orderManager, client != null && client.Bot == null); + LobbyUtils.SetupProfileWidget(item, client, orderManager, worldRenderer); + var nameLabel = item.Get("NAME"); var nameFont = Game.Renderer.Fonts[nameLabel.Font]; @@ -137,7 +138,8 @@ public GameInfoStatsLogic(Widget widget, World world, OrderManager orderManager) foreach (var client in spectators) { var item = playerTemplate.Clone(); - LobbyUtils.SetupClientWidget(item, client, orderManager, client != null && client.Bot == null); + LobbyUtils.SetupProfileWidget(item, client, orderManager, worldRenderer); + var nameLabel = item.Get("NAME"); var nameFont = Game.Renderer.Fonts[nameLabel.Font]; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/ClientTooltipLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/ClientTooltipLogic.cs deleted file mode 100644 index effd21e33614..000000000000 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/ClientTooltipLogic.cs +++ /dev/null @@ -1,107 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2018 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.Graphics; -using OpenRA.Network; -using OpenRA.Widgets; - -namespace OpenRA.Mods.Common.Widgets.Logic -{ - public class ClientTooltipLogic : ChromeLogic - { - SpriteFont latencyFont; - SpriteFont latencyPrefixFont; - - [ObjectCreator.UseCtor] - public ClientTooltipLogic(Widget widget, TooltipContainerWidget tooltipContainer, OrderManager orderManager, int clientIndex) - { - var admin = widget.Get("ADMIN"); - var adminFont = Game.Renderer.Fonts[admin.Font]; - - var latency = widget.GetOrNull("LATENCY"); - if (latency != null) - latencyFont = Game.Renderer.Fonts[latency.Font]; - - var latencyPrefix = widget.GetOrNull("LATENCY_PREFIX"); - if (latencyPrefix != null) - latencyPrefixFont = Game.Renderer.Fonts[latencyPrefix.Font]; - - var ip = widget.Get("IP"); - var addressFont = Game.Renderer.Fonts[ip.Font]; - - var location = widget.Get("LOCATION"); - var locationFont = Game.Renderer.Fonts[location.Font]; - - var locationOffset = location.Bounds.Y; - var addressOffset = ip.Bounds.Y; - var latencyOffset = latency == null ? 0 : latency.Bounds.Y; - var tooltipHeight = widget.Bounds.Height; - - var margin = widget.Bounds.Width; - - widget.IsVisible = () => (orderManager.LobbyInfo.ClientWithIndex(clientIndex) != null); - tooltipContainer.BeforeRender = () => - { - if (!widget.IsVisible()) - return; - - var latencyPrefixSize = latencyPrefix == null ? 0 : latencyPrefix.Bounds.X + latencyPrefixFont.Measure(latencyPrefix.GetText() + " ").X; - var locationWidth = locationFont.Measure(location.GetText()).X; - var adminWidth = adminFont.Measure(admin.GetText()).X; - var addressWidth = addressFont.Measure(ip.GetText()).X; - var latencyWidth = latencyFont == null ? 0 : latencyPrefixSize + latencyFont.Measure(latency.GetText()).X; - var width = Math.Max(locationWidth, Math.Max(adminWidth, Math.Max(addressWidth, latencyWidth))); - widget.Bounds.Width = width + 2 * margin; - if (latency != null) - latency.Bounds.Width = widget.Bounds.Width; - ip.Bounds.Width = widget.Bounds.Width; - admin.Bounds.Width = widget.Bounds.Width; - location.Bounds.Width = widget.Bounds.Width; - - ip.Bounds.Y = addressOffset; - if (latency != null) - latency.Bounds.Y = latencyOffset; - location.Bounds.Y = locationOffset; - widget.Bounds.Height = tooltipHeight; - - if (admin.IsVisible()) - { - ip.Bounds.Y += admin.Bounds.Height; - if (latency != null) - latency.Bounds.Y += admin.Bounds.Height; - location.Bounds.Y += admin.Bounds.Height; - widget.Bounds.Height += admin.Bounds.Height; - } - - if (latencyPrefix != null) - latencyPrefix.Bounds.Y = latency.Bounds.Y; - if (latency != null) - latency.Bounds.X = latencyPrefixSize; - }; - - admin.IsVisible = () => orderManager.LobbyInfo.ClientWithIndex(clientIndex).IsAdmin; - var client = orderManager.LobbyInfo.ClientWithIndex(clientIndex); - var ping = orderManager.LobbyInfo.PingFromClient(client); - if (latency != null) - { - latency.GetText = () => LobbyUtils.LatencyDescription(ping); - latency.GetColor = () => LobbyUtils.LatencyColor(ping); - } - - var address = LobbyUtils.GetExternalIP(clientIndex, orderManager); - var cachedDescriptiveIP = LobbyUtils.DescriptiveIpAddress(address); - ip.GetText = () => cachedDescriptiveIP; - var cachedCountryLookup = GeoIP.LookupCountry(address); - location.GetText = () => cachedCountryLookup; - } - } -} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LatencyTooltipLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LatencyTooltipLogic.cs new file mode 100644 index 000000000000..146734c9569f --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LatencyTooltipLogic.cs @@ -0,0 +1,44 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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.Graphics; +using OpenRA.Network; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public class LatencyTooltipLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public LatencyTooltipLogic(Widget widget, TooltipContainerWidget tooltipContainer, OrderManager orderManager, Session.Client client) + { + var latencyPrefix = widget.Get("LATENCY_PREFIX"); + var latencyPrefixFont = Game.Renderer.Fonts[latencyPrefix.Font]; + var latency = widget.Get("LATENCY"); + var latencyFont = Game.Renderer.Fonts[latency.Font]; + var rightMargin = widget.Bounds.Width; + + latency.Bounds.X = latencyPrefix.Bounds.X + latencyPrefixFont.Measure(latencyPrefix.Text + " ").X; + + widget.IsVisible = () => client != null; + tooltipContainer.BeforeRender = () => + { + if (widget.IsVisible()) + widget.Bounds.Width = latency.Bounds.X + latencyFont.Measure(latency.GetText()).X + rightMargin; + }; + + var ping = orderManager.LobbyInfo.PingFromClient(client); + latency.GetText = () => LobbyUtils.LatencyDescription(ping); + latency.GetColor = () => LobbyUtils.LatencyColor(ping); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 77872490f748..ad2749ea0d8b 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -29,6 +29,7 @@ public class LobbyLogic : ChromeLogic readonly Action onStart; readonly Action onExit; readonly OrderManager orderManager; + readonly WorldRenderer worldRenderer; readonly bool skirmishMode; readonly Ruleset modRules; readonly World shellmapWorld; @@ -100,6 +101,7 @@ void ConnectionStateChanged(OrderManager om) lobby = widget; this.modData = modData; this.orderManager = orderManager; + this.worldRenderer = worldRenderer; this.onStart = onStart; this.onExit = onExit; this.skirmishMode = skirmishMode; @@ -558,7 +560,7 @@ void UpdatePlayerList() template = emptySlotTemplate.Clone(); if (isHost) - LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager, map); + LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager, worldRenderer, map); else LobbyUtils.SetupSlotWidget(template, slot, client); @@ -574,12 +576,12 @@ void UpdatePlayerList() if (template == null || template.Id != editablePlayerTemplate.Id) template = editablePlayerTemplate.Clone(); - LobbyUtils.SetupClientWidget(template, client, orderManager, client.Bot == null); + LobbyUtils.SetupLatencyWidget(template, client, orderManager, client.Bot == null); if (client.Bot != null) - LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager, map); + LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager, worldRenderer, map); else - LobbyUtils.SetupEditableNameWidget(template, slot, client, orderManager); + LobbyUtils.SetupEditableNameWidget(template, slot, client, orderManager, worldRenderer); LobbyUtils.SetupEditableColorWidget(template, slot, client, orderManager, shellmapWorld, colorPreview); LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, factions); @@ -593,7 +595,7 @@ void UpdatePlayerList() if (template == null || template.Id != nonEditablePlayerTemplate.Id) template = nonEditablePlayerTemplate.Clone(); - LobbyUtils.SetupClientWidget(template, client, orderManager, client.Bot == null); + LobbyUtils.SetupLatencyWidget(template, client, orderManager, client.Bot == null); LobbyUtils.SetupColorWidget(template, slot, client); LobbyUtils.SetupFactionWidget(template, slot, client, factions); @@ -601,12 +603,12 @@ void UpdatePlayerList() { LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, map); LobbyUtils.SetupEditableSpawnWidget(template, slot, client, orderManager, map); - LobbyUtils.SetupPlayerActionWidget(template, slot, client, orderManager, lobby, - () => panel = PanelType.Kick, () => panel = PanelType.Players); + LobbyUtils.SetupPlayerActionWidget(template, slot, client, orderManager, worldRenderer, + lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players); } else { - LobbyUtils.SetupNameWidget(template, slot, client); + LobbyUtils.SetupNameWidget(template, slot, client, orderManager, worldRenderer); LobbyUtils.SetupTeamWidget(template, slot, client); LobbyUtils.SetupSpawnWidget(template, slot, client); } @@ -640,7 +642,7 @@ void UpdatePlayerList() if (template == null || template.Id != editableSpectatorTemplate.Id) template = editableSpectatorTemplate.Clone(); - LobbyUtils.SetupEditableNameWidget(template, null, c, orderManager); + LobbyUtils.SetupEditableNameWidget(template, null, c, orderManager, worldRenderer); if (client.IsAdmin) LobbyUtils.SetupEditableReadyWidget(template, null, client, orderManager, map); @@ -652,16 +654,16 @@ void UpdatePlayerList() template = nonEditableSpectatorTemplate.Clone(); if (isHost) - LobbyUtils.SetupPlayerActionWidget(template, null, client, orderManager, lobby, - () => panel = PanelType.Kick, () => panel = PanelType.Players); + LobbyUtils.SetupPlayerActionWidget(template, null, client, orderManager, worldRenderer, + lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players); else - LobbyUtils.SetupNameWidget(template, null, client); + LobbyUtils.SetupNameWidget(template, null, client, orderManager, worldRenderer); if (client.IsAdmin) LobbyUtils.SetupReadyWidget(template, null, client); } - LobbyUtils.SetupClientWidget(template, c, orderManager, true); + LobbyUtils.SetupLatencyWidget(template, c, orderManager, true); template.IsVisible = () => true; if (idx >= players.Children.Count) diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs index e90cade6603d..9ff2e0c59e8f 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs @@ -296,21 +296,8 @@ public static string LatencyDescription(Session.ClientPing ping) return "Poor"; } - public static string DescriptiveIpAddress(string ip) + public static void SetupLatencyWidget(Widget parent, Session.Client c, OrderManager orderManager, bool visible) { - if (ip == null) - return "Unknown Host"; - if (ip == IPAddress.Loopback.ToString()) - return "Local Host"; - return ip; - } - - public static void SetupClientWidget(Widget parent, Session.Client c, OrderManager orderManager, bool visible) - { - var adminIndicator = parent.GetOrNull("ADMIN_INDICATOR"); - if (adminIndicator != null) - adminIndicator.IsVisible = () => c != null && c.IsAdmin; - var block = parent.GetOrNull("LATENCY"); if (block != null) { @@ -321,13 +308,36 @@ public static void SetupClientWidget(Widget parent, Session.Client c, OrderManag orderManager.LobbyInfo.PingFromClient(c)); } - var tooltip = parent.Get("CLIENT_REGION"); + var tooltip = parent.Get("LATENCY_REGION"); tooltip.IsVisible = () => c != null && visible; if (c != null) - tooltip.Bind(orderManager, c.Index); + tooltip.Bind(orderManager, null, c); } - public static void SetupEditableNameWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager) + public static void SetupProfileWidget(Widget parent, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) + { + var profile = parent.GetOrNull("PROFILE"); + if (profile != null && c.Bot == null) + { + var imageName = (c != null && c.IsAdmin ? "admin-" : "player-") + + (c.Fingerprint != null ? "registered" : "anonymous"); + + profile.GetImageName = () => imageName; + profile.IsVisible = () => true; + } + + var profileTooltip = parent.GetOrNull("PROFILE_TOOLTIP"); + if (profileTooltip != null && c.Bot == null) + { + if (c != null && c.Fingerprint != null) + profileTooltip.Template = "REGISTERED_PLAYER_TOOLTIP"; + + profileTooltip.Bind(orderManager, worldRenderer, c); + profileTooltip.IsVisible = () => true; + } + } + + public static void SetupEditableNameWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) { var name = parent.Get("NAME"); name.IsVisible = () => true; @@ -364,19 +374,24 @@ public static void SetupEditableNameWidget(Widget parent, Session.Slot s, Sessio return true; }; + SetupProfileWidget(name, c, orderManager, worldRenderer); + HideChildWidget(parent, "SLOT_OPTIONS"); } - public static void SetupNameWidget(Widget parent, Session.Slot s, Session.Client c) + public static void SetupNameWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) { var name = parent.Get("NAME"); name.IsVisible = () => true; var font = Game.Renderer.Fonts[name.Font]; var label = WidgetUtils.TruncateText(c.Name, name.Bounds.Width, font); name.GetText = () => label; + + SetupProfileWidget(parent, c, orderManager, worldRenderer); } - public static void SetupEditableSlotWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, MapPreview map) + public static void SetupEditableSlotWidget(Widget parent, Session.Slot s, Session.Client c, + OrderManager orderManager, WorldRenderer worldRenderer, MapPreview map) { var slot = parent.Get("SLOT_OPTIONS"); slot.IsVisible = () => true; @@ -398,7 +413,8 @@ public static void SetupSlotWidget(Widget parent, Session.Slot s, Session.Client HideChildWidget(parent, "SLOT_OPTIONS"); } - public static void SetupPlayerActionWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, Widget lobby, Action before, Action after) + public static void SetupPlayerActionWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, + WorldRenderer worldRenderer, Widget lobby, Action before, Action after) { var slot = parent.Get("PLAYER_ACTION"); slot.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index; @@ -406,6 +422,8 @@ public static void SetupPlayerActionWidget(Widget parent, Session.Slot s, Sessio slot.GetText = () => c != null ? c.Name : string.Empty; slot.OnMouseDown = _ => ShowPlayerActionDropDown(slot, s, c, orderManager, lobby, before, after); + SetupProfileWidget(slot, c, orderManager, worldRenderer); + // Ensure Name selector (if present) is hidden HideChildWidget(parent, "NAME"); } @@ -575,12 +593,11 @@ public static void AddPlayerFlagAndName(ScrollItemWidget template, Player player playerName.GetColor = () => player.Color.RGB; } - public static string GetExternalIP(int clientIndex, OrderManager orderManager) + public static string GetExternalIP(Session.Client client, OrderManager orderManager) { - var client = orderManager.LobbyInfo.ClientWithIndex(clientIndex); var address = client != null ? client.IpAddress : ""; var lc = orderManager.LocalClient; - if (lc != null && lc.Index == clientIndex && address == IPAddress.Loopback.ToString()) + if (lc != null && lc.Index == client.Index && address == IPAddress.Loopback.ToString()) { var externalIP = UPnP.ExternalIP; if (externalIP != null) diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index a0793b688815..d3a46e836a82 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -228,6 +228,16 @@ public MainMenuLogic(Widget widget, World world, ModData modData) menuType != MenuType.SystemInfoPrompt && webServices.ModVersionStatus == ModVersionStatus.Outdated; + var playerProfile = widget.GetOrNull("PLAYER_PROFILE_CONTAINER"); + if (playerProfile != null) + { + Func minimalProfile = () => Ui.CurrentWindow() != null; + Game.LoadWidget(world, "LOCAL_PROFILE_PANEL", playerProfile, new WidgetArgs() + { + { "minimalProfile", minimalProfile } + }); + } + // System information opt-out prompt var sysInfoPrompt = widget.Get("SYSTEM_INFO_PROMPT"); sysInfoPrompt.IsVisible = () => menuType == MenuType.SystemInfoPrompt; diff --git a/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs new file mode 100644 index 000000000000..d3a19799d2ac --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs @@ -0,0 +1,327 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 System.Linq; +using System.Net; +using System.Text; +using OpenRA.Graphics; +using OpenRA.Network; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public class LocalProfileLogic : ChromeLogic + { + readonly WorldRenderer worldRenderer; + readonly LocalPlayerProfile localProfile; + readonly Widget badgeContainer; + readonly Widget widget; + bool notFound; + bool badgesVisible; + + [ObjectCreator.UseCtor] + public LocalProfileLogic(Widget widget, WorldRenderer worldRenderer, Func minimalProfile) + { + this.worldRenderer = worldRenderer; + this.widget = widget; + localProfile = Game.LocalPlayerProfile; + + // Key registration + widget.Get("GENERATE_KEYS").IsVisible = () => localProfile.State == LocalPlayerProfile.LinkState.Uninitialized && !minimalProfile(); + widget.Get("GENERATING_KEYS").IsVisible = () => localProfile.State == LocalPlayerProfile.LinkState.GeneratingKeys && !minimalProfile(); + + var lastProfileState = LocalPlayerProfile.LinkState.CheckingLink; + widget.Get("REGISTER_FINGERPRINT").IsVisible = () => + { + // Take a copy of the state to avoid race conditions + var state = localProfile.State; + + // Copy the key to the clipboard when displaying the link instructions + if (state != lastProfileState && state == LocalPlayerProfile.LinkState.Unlinked) + Game.SetClipboardText(localProfile.PublicKey); + + lastProfileState = state; + return localProfile.State == LocalPlayerProfile.LinkState.Unlinked && !notFound && !minimalProfile(); + }; + + widget.Get("CHECKING_FINGERPRINT").IsVisible = () => localProfile.State == LocalPlayerProfile.LinkState.CheckingLink && !minimalProfile(); + widget.Get("FINGERPRINT_NOT_FOUND").IsVisible = () => localProfile.State == LocalPlayerProfile.LinkState.Unlinked && notFound && !minimalProfile(); + widget.Get("CONNECTION_ERROR").IsVisible = () => localProfile.State == LocalPlayerProfile.LinkState.ConnectionFailed && !minimalProfile(); + + widget.Get("GENERATE_KEY").OnClick = localProfile.GenerateKeypair; + + widget.Get("CHECK_KEY").OnClick = () => localProfile.RefreshPlayerData(() => RefreshComplete(true)); + + widget.Get("DELETE_KEY").OnClick = () => + { + localProfile.DeleteKeypair(); + Game.RunAfterTick(Ui.ResetTooltips); + }; + + widget.Get("FINGERPRINT_NOT_FOUND_CONTINUE").OnClick = () => + { + notFound = false; + Game.RunAfterTick(Ui.ResetTooltips); + }; + + widget.Get("CONNECTION_ERROR_RETRY").OnClick = () => localProfile.RefreshPlayerData(() => RefreshComplete(true)); + + // Profile view + widget.Get("PROFILE_HEADER").IsVisible = () => localProfile.State == LocalPlayerProfile.LinkState.Linked; + widget.Get("PROFILE_NAME").GetText = () => localProfile.ProfileData.ProfileName; + widget.Get("PROFILE_RANK").GetText = () => localProfile.ProfileData.ProfileRank; + + var destroyKey = widget.Get("DESTROY_KEY"); + destroyKey.OnClick = localProfile.DeleteKeypair; + destroyKey.IsDisabled = minimalProfile; + + badgeContainer = widget.Get("BADGES_CONTAINER"); + badgeContainer.IsVisible = () => badgesVisible && !minimalProfile() + && localProfile.State == LocalPlayerProfile.LinkState.Linked; + + localProfile.RefreshPlayerData(() => RefreshComplete(false)); + } + + public void RefreshComplete(bool updateNotFound) + { + if (updateNotFound) + notFound = localProfile.State == LocalPlayerProfile.LinkState.Unlinked; + + Game.RunAfterTick(() => + { + badgesVisible = false; + + if (localProfile.State == LocalPlayerProfile.LinkState.Linked) + { + if (localProfile.ProfileData.Badges.Any()) + { + Func negotiateWidth = _ => widget.Get("PROFILE_HEADER").Bounds.Width; + + // Remove any stale badges that may be left over from a previous session + badgeContainer.RemoveChildren(); + + var badges = Ui.LoadWidget("PLAYER_PROFILE_BADGES_INSERT", badgeContainer, new WidgetArgs() + { + { "worldRenderer", worldRenderer }, + { "profile", localProfile.ProfileData }, + { "negotiateWidth", negotiateWidth } + }); + + if (badges.Bounds.Height > 0) + { + badgeContainer.Bounds.Height = badges.Bounds.Height; + badgesVisible = true; + } + } + } + + Ui.ResetTooltips(); + }); + } + } + + public class RegisteredProfileTooltipLogic : ChromeLogic + { + readonly PlayerDatabase playerDatabase; + PlayerProfile profile; + bool profileLoaded; + + [ObjectCreator.UseCtor] + public RegisteredProfileTooltipLogic(Widget widget, WorldRenderer worldRenderer, ModData modData, Session.Client client) + { + playerDatabase = modData.Manifest.Get(); + + var header = widget.Get("HEADER"); + var badgeContainer = widget.Get("BADGES_CONTAINER"); + var badgeSeparator = badgeContainer.GetOrNull("SEPARATOR"); + + var profileHeader = header.Get("PROFILE_HEADER"); + var messageHeader = header.Get("MESSAGE_HEADER"); + var message = messageHeader.Get("MESSAGE"); + var messageFont = Game.Renderer.Fonts[message.Font]; + + profileHeader.IsVisible = () => profileLoaded; + messageHeader.IsVisible = () => !profileLoaded; + + var profileWidth = 0; + var messageText = "Loading player profile..."; + var messageWidth = messageFont.Measure(messageText).X + 2 * message.Bounds.Left; + + Action onQueryComplete = i => + { + try + { + if (i.Error == null) + { + var yaml = MiniYaml.FromString(Encoding.UTF8.GetString(i.Result)).First(); + if (yaml.Key == "Player") + { + profile = FieldLoader.Load(yaml.Value); + Game.RunAfterTick(() => + { + var nameLabel = profileHeader.Get("PROFILE_NAME"); + var nameFont = Game.Renderer.Fonts[nameLabel.Font]; + var rankLabel = profileHeader.Get("PROFILE_RANK"); + var rankFont = Game.Renderer.Fonts[rankLabel.Font]; + + var adminContainer = profileHeader.Get("GAME_ADMIN"); + var adminLabel = adminContainer.Get("LABEL"); + var adminFont = Game.Renderer.Fonts[adminLabel.Font]; + + var headerSizeOffset = profileHeader.Bounds.Height - messageHeader.Bounds.Height; + + nameLabel.GetText = () => profile.ProfileName; + rankLabel.GetText = () => profile.ProfileRank; + + profileWidth = Math.Max(profileWidth, nameFont.Measure(profile.ProfileName).X + 2 * nameLabel.Bounds.Left); + profileWidth = Math.Max(profileWidth, rankFont.Measure(profile.ProfileRank).X + 2 * rankLabel.Bounds.Left); + + header.Bounds.Height += headerSizeOffset; + badgeContainer.Bounds.Y += header.Bounds.Height; + if (client.IsAdmin) + { + profileWidth = Math.Max(profileWidth, adminFont.Measure(adminLabel.Text).X + 2 * adminLabel.Bounds.Left); + + adminContainer.IsVisible = () => true; + profileHeader.Bounds.Height += adminLabel.Bounds.Height; + header.Bounds.Height += adminLabel.Bounds.Height; + badgeContainer.Bounds.Y += adminLabel.Bounds.Height; + } + + Func negotiateWidth = badgeWidth => + { + profileWidth = Math.Min(Math.Max(badgeWidth, profileWidth), widget.Bounds.Width); + return profileWidth; + }; + + if (profile.Badges.Any()) + { + var badges = Ui.LoadWidget("PLAYER_PROFILE_BADGES_INSERT", badgeContainer, new WidgetArgs() + { + { "worldRenderer", worldRenderer }, + { "profile", profile }, + { "negotiateWidth", negotiateWidth } + }); + + if (badges.Bounds.Height > 0) + { + badgeContainer.Bounds.Height = badges.Bounds.Height; + badgeContainer.IsVisible = () => true; + } + } + + profileWidth = Math.Min(profileWidth, widget.Bounds.Width); + header.Bounds.Width = widget.Bounds.Width = badgeContainer.Bounds.Width = profileWidth; + widget.Bounds.Height = header.Bounds.Height + badgeContainer.Bounds.Height; + + if (badgeSeparator != null) + badgeSeparator.Bounds.Width = profileWidth - 2 * badgeSeparator.Bounds.X; + + profileLoaded = true; + }); + } + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to parse player data result with exception: {0}", e); + } + finally + { + if (profile == null) + { + messageText = "Failed to load player profile."; + messageWidth = messageFont.Measure(messageText).X + 2 * message.Bounds.Left; + header.Bounds.Width = widget.Bounds.Width = messageWidth; + } + } + }; + + message.GetText = () => messageText; + header.Bounds.Height += messageHeader.Bounds.Height; + header.Bounds.Width = widget.Bounds.Width = messageWidth; + widget.Bounds.Height = header.Bounds.Height; + badgeContainer.Visible = false; + + new Download(playerDatabase.Profile + client.Fingerprint, _ => { }, onQueryComplete); + } + } + + public class PlayerProfileBadgesLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public PlayerProfileBadgesLogic(Widget widget, PlayerProfile profile, Func negotiateWidth) + { + var showBadges = profile.Badges.Any(); + widget.IsVisible = () => showBadges; + + var badgeTemplate = widget.Get("BADGE_TEMPLATE"); + widget.RemoveChild(badgeTemplate); + + var width = 0; + var badgeOffset = badgeTemplate.Bounds.Y; + foreach (var badge in profile.Badges) + { + var b = badgeTemplate.Clone(); + var icon = b.Get("ICON"); + icon.GetSprite = () => badge.Icon24; + + var label = b.Get("LABEL"); + var labelFont = Game.Renderer.Fonts[label.Font]; + + var labelText = WidgetUtils.TruncateText(badge.Label, label.Bounds.Width, labelFont); + label.GetText = () => labelText; + + width = Math.Max(width, label.Bounds.Left + labelFont.Measure(labelText).X + icon.Bounds.X); + + b.Bounds.Y = badgeOffset; + widget.AddChild(b); + + badgeOffset += badgeTemplate.Bounds.Height; + } + + if (badgeOffset > badgeTemplate.Bounds.Y) + badgeOffset += 5; + + widget.Bounds.Width = negotiateWidth(width); + widget.Bounds.Height = badgeOffset; + } + } + + public class AnonymousProfileTooltipLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public AnonymousProfileTooltipLogic(Widget widget, OrderManager orderManager, Session.Client client) + { + var address = LobbyUtils.GetExternalIP(client, orderManager); + var cachedDescriptiveIP = address ?? "Unknown IP"; + + var nameLabel = widget.Get("NAME"); + var nameFont = Game.Renderer.Fonts[nameLabel.Font]; + widget.Bounds.Width = nameFont.Measure(nameLabel.Text).X + 2 * nameLabel.Bounds.Left; + + var ipLabel = widget.Get("IP"); + ipLabel.GetText = () => cachedDescriptiveIP; + + var locationLabel = widget.Get("LOCATION"); + var cachedCountryLookup = GeoIP.LookupCountry(address); + locationLabel.GetText = () => cachedCountryLookup; + + if (client.IsAdmin) + { + var adminLabel = widget.Get("GAME_ADMIN"); + adminLabel.IsVisible = () => client.IsAdmin; + widget.Bounds.Height += adminLabel.Bounds.Height; + } + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs b/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs index 3e38c94d9c58..f9209fa0d5a2 100644 --- a/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs +++ b/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs @@ -90,6 +90,8 @@ protected TextFieldWidget(TextFieldWidget widget) { Text = widget.Text; MaxLength = widget.MaxLength; + LeftMargin = widget.LeftMargin; + RightMargin = widget.RightMargin; Type = widget.Type; Font = widget.Font; TextColor = widget.TextColor; @@ -565,7 +567,7 @@ public override void Draw() var disabled = IsDisabled(); var state = disabled ? "textfield-disabled" : HasKeyboardFocus ? "textfield-focused" : - Ui.MouseOverWidget == this ? "textfield-hover" : + Ui.MouseOverWidget == this || Children.Any(c => c == Ui.MouseOverWidget) ? "textfield-hover" : "textfield"; WidgetUtils.DrawPanel(state, diff --git a/mods/cnc/chrome.yaml b/mods/cnc/chrome.yaml index e5969e266c2b..37a463d4fe35 100644 --- a/mods/cnc/chrome.yaml +++ b/mods/cnc/chrome.yaml @@ -451,6 +451,10 @@ lobby-bits: chrome.png kick: 386,115,11,11 protected: 403,97,10,13 protected-disabled: 403,113,10,13 + admin-registered: 448,112,16,16 + admin-anonymous: 464,112,16,16 + player-registered: 480,112,16,16 + player-anonymous: 496,112,16,16 reload-icon: chrome.png enabled: 256,192,16,16 diff --git a/mods/cnc/chrome/ingame-infostats.yaml b/mods/cnc/chrome/ingame-infostats.yaml index b9a6eaf83c11..7343ce30ce66 100644 --- a/mods/cnc/chrome/ingame-infostats.yaml +++ b/mods/cnc/chrome/ingame-infostats.yaml @@ -83,17 +83,24 @@ Container@SKIRMISH_STATS: Height: 25 X: 2 Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 8 + Y: 4 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 8 + Y: 4 + Width: 16 + Height: 16 + Visible: false + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 10 - Width: 210 + X: 29 + Width: 191 Height: 25 Shadow: True - ClientTooltipRegion@CLIENT_REGION: - TooltipContainer: TOOLTIP_CONTAINER - Template: INGAME_CLIENT_TOOLTIP - X: 10 - Width: 210 - Height: 25 Image@FACTIONFLAG: X: 230 Y: 6 diff --git a/mods/cnc/chrome/lobby-players.yaml b/mods/cnc/chrome/lobby-players.yaml index 3f42ef6765fc..5c54e1c9d0c2 100644 --- a/mods/cnc/chrome/lobby-players.yaml +++ b/mods/cnc/chrome/lobby-players.yaml @@ -61,11 +61,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - ImageCollection: lobby-bits - ImageName: admin - X: 2 - Visible: false Background@LATENCY: Y: 6 Width: 11 @@ -78,18 +73,32 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP TextField@NAME: X: 15 Width: 190 Height: 25 Text: Name + LeftMargin: 24 MaxLength: 16 Visible: false + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP DropDownButton@SLOT_OPTIONS: X: 15 Width: 190 @@ -158,11 +167,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Background@LATENCY: Y: 6 Width: 11 @@ -175,15 +179,27 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP + Image@PROFILE: + ImageCollection: lobby-bits + X: 18 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 18 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 20 + X: 39 Y: 0 - 1 - Width: 180 + Width: 161 Height: 25 DropDownButton@PLAYER_ACTION: X: 15 @@ -192,6 +208,20 @@ Container@LOBBY_PLAYER_BIN: Font: Regular Visible: false Align: Left + LeftMargin: 24 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP ColorBlock@COLORBLOCK: X: 215 Y: 6 @@ -273,11 +303,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Background@LATENCY: Y: 6 Width: 11 @@ -290,17 +315,31 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP TextField@NAME: X: 15 Width: 190 Height: 25 Text: Name + LeftMargin: 24 MaxLength: 16 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@SPECTATOR: X: 210 Width: 341 @@ -328,11 +367,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Background@LATENCY: Y: 6 Width: 11 @@ -345,15 +379,27 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP + Image@PROFILE: + ImageCollection: lobby-bits + X: 18 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 18 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 20 + X: 39 Y: 0 - 1 - Width: 180 + Width: 161 Height: 25 DropDownButton@PLAYER_ACTION: X: 15 @@ -362,6 +408,20 @@ Container@LOBBY_PLAYER_BIN: Font: Regular Visible: false Align: Left + LeftMargin: 24 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@SPECTATOR: X: 210 Width: 341 diff --git a/mods/cnc/chrome/mainmenu.yaml b/mods/cnc/chrome/mainmenu.yaml index d485f0841dfa..24f6f1804983 100644 --- a/mods/cnc/chrome/mainmenu.yaml +++ b/mods/cnc/chrome/mainmenu.yaml @@ -303,10 +303,9 @@ Container@MENU_BACKGROUND: Logic: PerfDebugLogic Children: Label@PERF_TEXT: - X: 40 - Y: 40 + X: WINDOW_RIGHT - WIDTH - 25 + Y: WINDOW_BOTTOM - HEIGHT - 100 Width: 170 - Height: 40 Contrast: true VAlign: Top Background@GRAPH_BG: @@ -321,3 +320,6 @@ Container@MENU_BACKGROUND: Y: 10 Width: 200 Height: 200 + Container@PLAYER_PROFILE_CONTAINER: + X: 31 + Y: 31 diff --git a/mods/cnc/chrome/playerprofile.yaml b/mods/cnc/chrome/playerprofile.yaml new file mode 100644 index 000000000000..c340c78869e6 --- /dev/null +++ b/mods/cnc/chrome/playerprofile.yaml @@ -0,0 +1,234 @@ +Container@LOCAL_PROFILE_PANEL: + Logic: LocalProfileLogic + Width: 270 + Height: 100 + Children: + Background@PROFILE_HEADER: + Width: PARENT_RIGHT + Height: 50 + Background: panel-black + Children: + Label@PROFILE_NAME: + X: 10 + Y: 3 + Width: PARENT_RIGHT - 20 + Height: 25 + Font: MediumBold + Label@PROFILE_RANK: + X: 10 + Y: 23 + Width: PARENT_RIGHT - 20 + Height: 25 + Font: TinyBold + Button@DESTROY_KEY: + X: PARENT_RIGHT - 70 + Y: 15 + Width: 60 + Height: 20 + Font: TinyBold + BaseLine: 1 + Text: Logout + Background@BADGES_CONTAINER: + Width: PARENT_RIGHT + Y: 48 + Visible: false + Background: panel-black + Background@GENERATE_KEYS: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: panel-black + Children: + Label@DESC_A: + Y: 5 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Connect to a forum account to identify + Label@DESC_B: + Y: 21 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: yourself to other players, join private + Label@DESC_C: + Y: 37 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: servers, and display badges. + Button@GENERATE_KEY: + X: (PARENT_RIGHT - WIDTH) / 2 + Y: 70 + Width: 240 + Height: 20 + Font: TinyBold + BaseLine: 1 + Text: Connect to an OpenRA forum account + Background@GENERATING_KEYS: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: panel-black + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Generating authentication key pair. + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: This will take several seconds... + ProgressBar: + X: (PARENT_RIGHT - WIDTH) / 2 + Y: 70 + Width: 240 + Height: 20 + Indeterminate: true + Background@REGISTER_FINGERPRINT: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: panel-black + Children: + Label@DESC_A: + Y: 2 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: An authentication key has been copied to your + Label@DESC_B: + Y: 18 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: clipboard. Add this to your User Control Panel + Label@DESC_C: + Y: 34 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: on the OpenRA forum then press Continue. + Button@DELETE_KEY: + X: 15 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Cancel + Button@CHECK_KEY: + X: 185 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Continue + Background@CHECKING_FINGERPRINT: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: panel-black + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Querying account details from + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: the OpenRA forum... + ProgressBar: + X: (PARENT_RIGHT - WIDTH) / 2 + Y: 70 + Width: 240 + Height: 20 + Indeterminate: true + Background@FINGERPRINT_NOT_FOUND: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: panel-black + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Your authentication key is not connected + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: to an OpenRA forum account. + Button@FINGERPRINT_NOT_FOUND_CONTINUE: + X: 185 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Back + Background@CONNECTION_ERROR: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: panel-black + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Failed to connect to the OpenRA forum. + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Please check your internet connection. + Button@CONNECTION_ERROR_RETRY: + X: 185 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Retry + +Container@PLAYER_PROFILE_BADGES_INSERT: + Logic: PlayerProfileBadgesLogic + Width: PARENT_RIGHT + Children: + Container@BADGE_TEMPLATE: + Width: PARENT_RIGHT + Height: 25 + Children: + Sprite@ICON: + X: 6 + Y: 1 + Width: 24 + Height: 24 + Label@LABEL: + X: 36 + Width: PARENT_RIGHT - 60 + Height: 24 + Font: Bold diff --git a/mods/cnc/chrome/tooltips.yaml b/mods/cnc/chrome/tooltips.yaml index bf4c08aaa8df..3cd8a9985f1a 100644 --- a/mods/cnc/chrome/tooltips.yaml +++ b/mods/cnc/chrome/tooltips.yaml @@ -188,60 +188,113 @@ Background@SPAWN_TOOLTIP: Font: TinyBold Align: center -Background@CLIENT_TOOLTIP: - Logic: ClientTooltipLogic +Background@LATENCY_TOOLTIP: + Logic: LatencyTooltipLogic Background: panel-black - Height: 47 + Height: 26 Width: 5 Children: - Label@ADMIN: - Y: 2 - Height: 18 - Font: Bold - Text: Game Admin - Align: Center - Label@IP: - Y: 5 - Width: 5 - Height: 10 - Font: TinyBold - Align: Center - Label@LOCATION: - Y: 17 - Height: 10 - Font: TinyBold - Align: Center Label@LATENCY_PREFIX: - X: 10 - Y: 29 - Height: 10 - Font: TinyBold + X: 5 + Height: 23 + Font: Bold Text: Latency: Label@LATENCY: - Y: 29 - Height: 10 - Font: TinyBold + Height: 23 + Font: Bold -Background@INGAME_CLIENT_TOOLTIP: - Logic: ClientTooltipLogic +Background@ANONYMOUS_PLAYER_TOOLTIP: + Logic: AnonymousProfileTooltipLogic Background: panel-black - Height: 35 - Width: 5 + Height: 55 + Width: 200 Children: - Label@ADMIN: - Y: 2 - Height: 18 - Font: Bold - Text: Game Admin - Align: Center - Label@IP: - Y: 5 - Width: 5 - Height: 10 - Font: TinyBold - Align: Center + Label@NAME: + X: 5 + Text: Anonymous Player + Height: 23 + Font: MediumBold Label@LOCATION: - Y: 17 - Height: 10 + X: 5 + Y: 23 + Height: 12 Font: TinyBold - Align: Center + Label@IP: + X: 5 + Y: 36 + Height: 12 + Font: TinyBold + Container@GAME_ADMIN: + X: 5 + Y: 49 + Height: 12 + Visible: False + Children: + Image@ICON: + Y: 5 + Width: 7 + Height: 5 + ImageCollection: lobby-bits + ImageName: admin + Label@LABEL: + X: 10 + Height: 12 + Text: Game Admin + Font: TinyBold + +Container@REGISTERED_PLAYER_TOOLTIP: + Logic: RegisteredProfileTooltipLogic + Width: 270 + Height: 137 + Children: + Background@HEADER: + Width: PARENT_RIGHT + Background: panel-black + Children: + Container@PROFILE_HEADER: + Width: PARENT_RIGHT + Height: 42 + Children: + Label@PROFILE_NAME: + X: 5 + Width: PARENT_RIGHT - 20 + Height: 23 + Font: MediumBold + Label@PROFILE_RANK: + X: 5 + Y: 23 + Width: PARENT_RIGHT - 20 + Height: 12 + Font: TinyBold + Container@GAME_ADMIN: + X: 5 + Y: 36 + Width: PARENT_RIGHT - 20 + Height: 12 + Visible: False + Children: + Image@ICON: + Y: 5 + Width: 7 + Height: 5 + ImageCollection: lobby-bits + ImageName: admin + Label@LABEL: + X: 10 + Height: 12 + Text: Game Admin + Font: TinyBold + Container@MESSAGE_HEADER: + Height: 26 + Width: PARENT_RIGHT + Children: + Label@MESSAGE: + X: 5 + Width: PARENT_RIGHT - 20 + Height: 23 + Font: Bold + Background@BADGES_CONTAINER: + Width: PARENT_RIGHT + Y: 0-1 + Visible: false + Background: panel-black diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 597f50dd1228..51da16f76955 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -91,6 +91,7 @@ Assemblies: ChromeLayout: cnc|chrome/mainmenu.yaml + cnc|chrome/playerprofile.yaml cnc|chrome/multiplayer-browser.yaml cnc|chrome/multiplayer-browserpanels.yaml cnc|chrome/multiplayer-createserver.yaml diff --git a/mods/cnc/uibits/chrome.png b/mods/cnc/uibits/chrome.png index cb42f9648284..64b6dc86035f 100644 Binary files a/mods/cnc/uibits/chrome.png and b/mods/cnc/uibits/chrome.png differ diff --git a/mods/common/chrome/ingame-infostats.yaml b/mods/common/chrome/ingame-infostats.yaml index 6b5f803885cf..152153b13f6d 100644 --- a/mods/common/chrome/ingame-infostats.yaml +++ b/mods/common/chrome/ingame-infostats.yaml @@ -84,17 +84,24 @@ Container@SKIRMISH_STATS: Height: 25 X: 2 Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 8 + Y: 4 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 8 + Y: 4 + Width: 16 + Height: 16 + Visible: false + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 10 - Width: 210 + X: 29 + Width: 191 Height: 25 Shadow: True - ClientTooltipRegion@CLIENT_REGION: - TooltipContainer: TOOLTIP_CONTAINER - Template: INGAME_CLIENT_TOOLTIP - X: 10 - Width: 210 - Height: 25 Image@FACTIONFLAG: X: 230 Y: 6 diff --git a/mods/common/chrome/lobby-players.yaml b/mods/common/chrome/lobby-players.yaml index a191749c7c54..d5a976f994f1 100644 --- a/mods/common/chrome/lobby-players.yaml +++ b/mods/common/chrome/lobby-players.yaml @@ -61,11 +61,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -77,17 +72,32 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP TextField@NAME: X: 15 Width: 165 Height: 25 Text: Name + LeftMargin: 24 MaxLength: 16 + Visible: false + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP DropDownButton@SLOT_OPTIONS: X: 15 Width: 165 @@ -154,11 +164,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -170,15 +175,27 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP + Image@PROFILE: + ImageCollection: lobby-bits + X: 18 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 18 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 20 + X: 39 Y: 0 - 1 - Width: 165 + Width: 146 Height: 25 Text: Name DropDownButton@PLAYER_ACTION: @@ -188,6 +205,20 @@ Container@LOBBY_PLAYER_BIN: Font: Regular Visible: false Align: Left + LeftMargin: 24 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP ColorBlock@COLORBLOCK: X: 195 Y: 6 @@ -266,11 +297,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -282,17 +308,31 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP TextField@NAME: X: 15 Width: 165 Height: 25 Text: Name + LeftMargin: 24 MaxLength: 16 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@SPECTATOR: X: 190 Width: 326 @@ -320,11 +360,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -336,16 +371,28 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP + Image@PROFILE: + ImageCollection: lobby-bits + X: 18 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 18 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - Width: 160 - Height: 25 - X: 20 + X: 39 Y: 0 - 1 + Width: 179 + Height: 25 Text: Name DropDownButton@PLAYER_ACTION: X: 15 diff --git a/mods/common/chrome/mainmenu.yaml b/mods/common/chrome/mainmenu.yaml index 2b493d473248..b17af2669022 100644 --- a/mods/common/chrome/mainmenu.yaml +++ b/mods/common/chrome/mainmenu.yaml @@ -320,3 +320,6 @@ Container@MAINMENU: Align: Center Shadow: true Text: Download the latest version from www.openra.net + Container@PLAYER_PROFILE_CONTAINER: + X: 25 + Y: 25 diff --git a/mods/common/chrome/playerprofile.yaml b/mods/common/chrome/playerprofile.yaml new file mode 100644 index 000000000000..c8a4986fa2fa --- /dev/null +++ b/mods/common/chrome/playerprofile.yaml @@ -0,0 +1,235 @@ +Container@LOCAL_PROFILE_PANEL: + Logic: LocalProfileLogic + Width: 270 + Height: 100 + Children: + Background@PROFILE_HEADER: + Width: PARENT_RIGHT + Height: 50 + Background: dialog2 + Children: + Label@PROFILE_NAME: + X: 10 + Y: 3 + Width: PARENT_RIGHT - 20 + Height: 25 + Font: MediumBold + Label@PROFILE_RANK: + X: 10 + Y: 23 + Width: PARENT_RIGHT - 20 + Height: 25 + Font: TinyBold + Button@DESTROY_KEY: + X: PARENT_RIGHT - 70 + Y: 15 + Width: 60 + Height: 20 + Font: TinyBold + BaseLine: 1 + Text: Logout + Background@BADGES_CONTAINER: + Width: PARENT_RIGHT + Y: 48 + Visible: false + Background: dialog3 + Background@GENERATE_KEYS: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: dialog2 + Children: + Label@DESC_A: + Y: 5 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Connect to a forum account to identify + Label@DESC_B: + Y: 21 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: yourself to other players, join private + Label@DESC_C: + Y: 37 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: servers, and display badges. + Button@GENERATE_KEY: + X: (PARENT_RIGHT - WIDTH) / 2 + Y: 70 + Width: 240 + Height: 20 + Font: TinyBold + BaseLine: 1 + Text: Connect to an OpenRA forum account + Background@GENERATING_KEYS: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: dialog2 + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Generating authentication key pair. + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: This will take several seconds... + ProgressBar: + X: (PARENT_RIGHT - WIDTH) / 2 + Y: 70 + Width: 240 + Height: 20 + Indeterminate: true + Background@REGISTER_FINGERPRINT: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: dialog2 + Children: + Label@DESC_A: + Y: 2 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: An authentication key has been copied to your + Label@DESC_B: + Y: 18 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: clipboard. Add this to your User Control Panel + Label@DESC_C: + Y: 34 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: on the OpenRA forum then press Continue. + Button@DELETE_KEY: + X: 15 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Cancel + Button@CHECK_KEY: + X: 185 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Continue + Background@CHECKING_FINGERPRINT: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: dialog2 + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Querying account details from + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: the OpenRA forum... + ProgressBar: + X: (PARENT_RIGHT - WIDTH) / 2 + Y: 70 + Width: 240 + Height: 20 + Indeterminate: true + Background@FINGERPRINT_NOT_FOUND: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: dialog2 + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Your authentication key is not connected + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: to an OpenRA forum account. + Button@FINGERPRINT_NOT_FOUND_CONTINUE: + X: 185 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Back + Background@CONNECTION_ERROR: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Background: dialog2 + Children: + Label@DESC_A: + Y: 13 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Failed to connect to the OpenRA forum. + Label@DESC_B: + Y: 29 + Width: PARENT_RIGHT + Height: 25 + Font: Small + Align: Center + Text: Please check your internet connection. + Button@CONNECTION_ERROR_RETRY: + X: 185 + Y: 70 + Width: 70 + Height: 20 + BaseLine: 1 + Font: TinyBold + Text: Retry + +Container@PLAYER_PROFILE_BADGES_INSERT: + Logic: PlayerProfileBadgesLogic + Width: PARENT_RIGHT + Height: 110 + Children: + Container@BADGE_TEMPLATE: + Width: PARENT_RIGHT + Height: 25 + Children: + Sprite@ICON: + X: 6 + Y: 1 + Width: 24 + Height: 24 + Label@LABEL: + X: 36 + Width: PARENT_RIGHT - 60 + Height: 24 + Font: Bold diff --git a/mods/common/chrome/tooltips.yaml b/mods/common/chrome/tooltips.yaml index f36ee7456142..aeff05d37985 100644 --- a/mods/common/chrome/tooltips.yaml +++ b/mods/common/chrome/tooltips.yaml @@ -113,63 +113,119 @@ Background@SPAWN_TOOLTIP: Font: TinyBold Align: center -Background@CLIENT_TOOLTIP: - Logic: ClientTooltipLogic +Background@LATENCY_TOOLTIP: + Logic: LatencyTooltipLogic Background: dialog4 - Height: 51 + Height: 29 Width: 7 Children: - Label@ADMIN: - Y: 4 - Height: 18 - Font: Bold - Text: Game Admin - Align: Center - Label@IP: - Y: 7 - Width: 5 - Height: 10 - Font: TinyBold - Align: Center - Label@LOCATION: - Y: 19 - Height: 10 - Font: TinyBold - Align: Center Label@LATENCY_PREFIX: - X: 10 - Y: 31 - Height: 10 - Font: TinyBold + X: 7 + Y: 2 + Height: 23 + Font: Bold Text: Latency: Label@LATENCY: - Y: 31 - Height: 10 - Font: TinyBold + Y: 2 + Height: 23 + Font: Bold -Background@INGAME_CLIENT_TOOLTIP: - Logic: ClientTooltipLogic +Background@ANONYMOUS_PLAYER_TOOLTIP: + Logic: AnonymousProfileTooltipLogic Background: dialog4 - Height: 41 - Width: 7 + Height: 56 + Width: 200 Children: - Label@ADMIN: - Y: 4 - Height: 18 - Font: Bold - Text: Game Admin - Align: Center - Label@IP: - Y: 7 - Width: 5 - Height: 10 - Font: TinyBold - Align: Center + Label@NAME: + X: 7 + Text: Anonymous Player + Height: 24 + Font: MediumBold Label@LOCATION: - Y: 19 - Height: 10 + X: 7 + Y: 23 + Height: 12 + Font: TinyBold + Label@IP: + X: 7 + Y: 36 + Height: 12 Font: TinyBold - Align: Center + Container@GAME_ADMIN: + X: 7 + Y: 49 + Height: 12 + Visible: False + Children: + Image@ICON: + Y: 5 + Width: 7 + Height: 5 + ImageCollection: lobby-bits + ImageName: admin + Label@LABEL: + X: 9 + Height: 12 + Text: Game Admin + Font: TinyBold + +Background@REGISTERED_PLAYER_TOOLTIP: + Logic: RegisteredProfileTooltipLogic + Width: 270 + Background: dialog4 + Children: + Container@HEADER: + Width: PARENT_RIGHT + Children: + Container@PROFILE_HEADER: + Height: 43 + Children: + Label@PROFILE_NAME: + X: 7 + Width: PARENT_RIGHT - 20 + Height: 24 + Font: MediumBold + Label@PROFILE_RANK: + X: 7 + Y: 23 + Width: PARENT_RIGHT - 20 + Height: 12 + Font: TinyBold + Container@GAME_ADMIN: + X: 7 + Y: 36 + Width: PARENT_RIGHT - 20 + Height: 12 + Visible: False + Children: + Image@ICON: + Y: 5 + Width: 7 + Height: 5 + ImageCollection: lobby-bits + ImageName: admin + Label@LABEL: + X: 10 + Height: 12 + Text: Game Admin + Font: TinyBold + Container@MESSAGE_HEADER: + Height: 26 + Width: PARENT_RIGHT + Children: + Label@MESSAGE: + X: 7 + Width: PARENT_RIGHT - 14 + Height: 23 + Font: Bold + Container@BADGES_CONTAINER: + Width: PARENT_RIGHT + Visible: false + Children: + Background@SEPARATOR: + X: 10 + Height: 1 + Background: tooltip-separator Background@PRODUCTION_TOOLTIP: Logic: ProductionTooltipLogic diff --git a/mods/d2k/chrome.yaml b/mods/d2k/chrome.yaml index ba4144081332..d9d01368f199 100644 --- a/mods/d2k/chrome.yaml +++ b/mods/d2k/chrome.yaml @@ -192,6 +192,9 @@ dialog3: dialog.png corner-bl: 640,127,1,1 corner-br: 767,127,1,1 +tooltip-separator: dialog.png + border-t: 641,0,126,1 + # Same as the half transparent frame used in the Asset Browser dialog4: dialog.png background: 517,392,54,54 @@ -212,6 +215,10 @@ lobby-bits: buttons.png huepicker: 193,0,7,15 protected: 200,0,12,13 protected-disabled: 211,0,12,13 + admin-registered: 224,0,16,16 + admin-anonymous: 240,0,16,16 + player-registered: 224,16,16,16 + player-anonymous: 240,16,16,16 reload-icon: chrome.png enabled: 416,312,16,16 diff --git a/mods/d2k/chrome/ingame-infostats.yaml b/mods/d2k/chrome/ingame-infostats.yaml index 6b5f803885cf..152153b13f6d 100644 --- a/mods/d2k/chrome/ingame-infostats.yaml +++ b/mods/d2k/chrome/ingame-infostats.yaml @@ -84,17 +84,24 @@ Container@SKIRMISH_STATS: Height: 25 X: 2 Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 8 + Y: 4 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 8 + Y: 4 + Width: 16 + Height: 16 + Visible: false + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 10 - Width: 210 + X: 29 + Width: 191 Height: 25 Shadow: True - ClientTooltipRegion@CLIENT_REGION: - TooltipContainer: TOOLTIP_CONTAINER - Template: INGAME_CLIENT_TOOLTIP - X: 10 - Width: 210 - Height: 25 Image@FACTIONFLAG: X: 230 Y: 6 diff --git a/mods/d2k/chrome/lobby-players.yaml b/mods/d2k/chrome/lobby-players.yaml index ae973d75aea7..b672a126a5af 100644 --- a/mods/d2k/chrome/lobby-players.yaml +++ b/mods/d2k/chrome/lobby-players.yaml @@ -61,11 +61,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -77,17 +72,32 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP TextField@NAME: X: 15 Width: 165 Height: 25 Text: Name + LeftMargin: 24 MaxLength: 16 + Visible: false + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP DropDownButton@SLOT_OPTIONS: X: 15 Width: 165 @@ -154,11 +164,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -170,24 +175,50 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP + Image@PROFILE: + ImageCollection: lobby-bits + X: 18 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 18 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 20 + X: 39 Y: 0 - 1 - Width: 165 + Width: 146 Height: 25 Text: Name DropDownButton@PLAYER_ACTION: X: 15 Width: 165 - Height: 23 + Height: 25 Font: Regular Visible: false Align: Left + LeftMargin: 24 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP ColorBlock@COLORBLOCK: X: 195 Y: 6 @@ -266,11 +297,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -282,17 +308,31 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP TextField@NAME: X: 15 Width: 165 Height: 25 Text: Name + LeftMargin: 24 MaxLength: 16 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@SPECTATOR: X: 190 Width: 326 @@ -320,11 +360,6 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Visible: false Children: - Image@ADMIN_INDICATOR: - X: 2 - ImageCollection: lobby-bits - ImageName: admin - Visible: false Container@LATENCY: Y: 6 Width: 11 @@ -336,13 +371,25 @@ Container@LOBBY_PLAYER_BIN: Y: 2 Width: PARENT_RIGHT - 4 Height: PARENT_BOTTOM - 4 - ClientTooltipRegion@CLIENT_REGION: + ClientTooltipRegion@LATENCY_REGION: Width: 11 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - Template: CLIENT_TOOLTIP + Template: LATENCY_TOOLTIP + Image@PROFILE: + ImageCollection: lobby-bits + X: 18 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 18 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@NAME: - X: 20 + X: 39 Y: 0 - 1 Width: 160 Height: 25 @@ -350,10 +397,24 @@ Container@LOBBY_PLAYER_BIN: DropDownButton@PLAYER_ACTION: X: 15 Width: 165 - Height: 23 + Height: 25 Font: Regular Visible: false Align: Left + LeftMargin: 24 + Children: + Image@PROFILE: + ImageCollection: lobby-bits + X: 3 + Y: 3 + Visible: false + ClientTooltipRegion@PROFILE_TOOLTIP: + X: 3 + Y: 3 + Width: 16 + Height: 16 + TooltipContainer: TOOLTIP_CONTAINER + Template: ANONYMOUS_PLAYER_TOOLTIP Label@SPECTATOR: X: 190 Width: 326 diff --git a/mods/d2k/chrome/mainmenu.yaml b/mods/d2k/chrome/mainmenu.yaml index fac7b7b1d9f1..dd517591fe7a 100644 --- a/mods/d2k/chrome/mainmenu.yaml +++ b/mods/d2k/chrome/mainmenu.yaml @@ -306,3 +306,6 @@ Container@MAINMENU: Y: 5 Width: 200 Height: 200 + Container@PLAYER_PROFILE_CONTAINER: + X: 5 + Y: 5 diff --git a/mods/d2k/chrome/tooltips.yaml b/mods/d2k/chrome/tooltips.yaml index b3bc525509c6..91ec038be5e2 100644 --- a/mods/d2k/chrome/tooltips.yaml +++ b/mods/d2k/chrome/tooltips.yaml @@ -113,63 +113,122 @@ Background@SPAWN_TOOLTIP: Font: TinyBold Align: center -Background@CLIENT_TOOLTIP: - Logic: ClientTooltipLogic +Background@LATENCY_TOOLTIP: + Logic: LatencyTooltipLogic Background: dialog3 - Height: 51 + Height: 31 Width: 7 Children: - Label@ADMIN: - Y: 4 - Height: 18 - Font: Bold - Text: Game Admin - Align: Center - Label@IP: - Y: 7 - Width: 5 - Height: 10 - Font: TinyBold - Align: Center - Label@LOCATION: - Y: 19 - Height: 10 - Font: TinyBold - Align: Center Label@LATENCY_PREFIX: - X: 10 - Y: 31 - Height: 10 - Font: TinyBold + X: 7 + Y: 3 + Height: 23 + Font: Bold Text: Latency: Label@LATENCY: - Y: 31 - Height: 10 - Font: TinyBold + Y: 3 + Height: 23 + Font: Bold -Background@INGAME_CLIENT_TOOLTIP: - Logic: ClientTooltipLogic +Background@ANONYMOUS_PLAYER_TOOLTIP: + Logic: AnonymousProfileTooltipLogic Background: dialog3 - Height: 41 - Width: 7 + Height: 57 + Width: 200 Children: - Label@ADMIN: - Y: 4 - Height: 18 - Font: Bold - Text: Game Admin - Align: Center - Label@IP: - Y: 7 - Width: 5 - Height: 10 - Font: TinyBold - Align: Center + Label@NAME: + X: 7 + Y: 1 + Text: Anonymous Player + Height: 24 + Font: MediumBold Label@LOCATION: - Y: 19 - Height: 10 + X: 7 + Y: 24 + Height: 12 Font: TinyBold - Align: Center + Label@IP: + X: 7 + Y: 37 + Height: 12 + Font: TinyBold + Container@GAME_ADMIN: + X: 7 + Y: 50 + Height: 12 + Visible: False + Children: + Image@ICON: + Y: 5 + Width: 7 + Height: 5 + ImageCollection: lobby-bits + ImageName: admin + Label@LABEL: + X: 10 + Height: 12 + Text: Game Admin + Font: TinyBold + +Background@REGISTERED_PLAYER_TOOLTIP: + Logic: RegisteredProfileTooltipLogic + Background: dialog3 + Width: 270 + Children: + Container@HEADER: + Width: PARENT_RIGHT + Children: + Container@PROFILE_HEADER: + Height: 45 + Children: + Label@PROFILE_NAME: + X: 7 + Y: 1 + Width: PARENT_RIGHT - 20 + Height: 24 + Font: MediumBold + Label@PROFILE_RANK: + X: 7 + Y: 24 + Width: PARENT_RIGHT - 20 + Height: 12 + Font: TinyBold + Container@GAME_ADMIN: + X: 7 + Y: 37 + Width: PARENT_RIGHT - 20 + Height: 12 + Visible: False + Children: + Image@ICON: + Y: 5 + Width: 7 + Height: 5 + ImageCollection: lobby-bits + ImageName: admin + Label@LABEL: + X: 10 + Height: 12 + Text: Game Admin + Font: TinyBold + Container@MESSAGE_HEADER: + Height: 31 + Width: PARENT_RIGHT + Children: + Label@MESSAGE: + X: 7 + Y: 3 + Width: PARENT_RIGHT - 14 + Height: 23 + Font: Bold + Container@BADGES_CONTAINER: + Width: PARENT_RIGHT + Visible: false + Children: + Background@SEPARATOR: + X: 10 + Height: 1 + Background: tooltip-separator Background@PRODUCTION_TOOLTIP: Logic: ProductionTooltipLogic diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index c9c0d894197d..e7a60b485149 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -86,6 +86,7 @@ ChromeLayout: common|chrome/credits.yaml common|chrome/lobby.yaml common|chrome/lobby-mappreview.yaml + common|chrome/playerprofile.yaml d2k|chrome/lobby-players.yaml common|chrome/lobby-options.yaml common|chrome/lobby-music.yaml diff --git a/mods/d2k/uibits/buttons.png b/mods/d2k/uibits/buttons.png index 308ba9633b4d..3cf1862b9b37 100644 Binary files a/mods/d2k/uibits/buttons.png and b/mods/d2k/uibits/buttons.png differ diff --git a/mods/ra/chrome.yaml b/mods/ra/chrome.yaml index 988579f583da..db66fbb00015 100644 --- a/mods/ra/chrome.yaml +++ b/mods/ra/chrome.yaml @@ -510,6 +510,9 @@ dialog4: dialog.png corner-bl: 512,446,6,6 corner-br: 571,446,6,6 +tooltip-separator: dialog.png + border-t: 517,387,54,1 + # completely black tile dialog5: dialog.png background: 579,387,64,64 @@ -530,6 +533,10 @@ lobby-bits: buttons.png huepicker: 194,0,7,15 protected: 202,0,10,13 protected-disabled: 213,0,10,13 + admin-registered: 224,0,16,16 + admin-anonymous: 240,0,16,16 + player-registered: 224,16,16,16 + player-anonymous: 240,16,16,16 reload-icon: chrome.png enabled: 512,80,16,16 diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 4cdd6a3fe843..3642abdd69e4 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -121,6 +121,7 @@ ChromeLayout: common|chrome/missionbrowser.yaml common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml + common|chrome/playerprofile.yaml Weapons: ra|weapons/explosions.yaml diff --git a/mods/ra/uibits/buttons.png b/mods/ra/uibits/buttons.png index a73f2a291c93..edcc6e3c71a6 100644 Binary files a/mods/ra/uibits/buttons.png and b/mods/ra/uibits/buttons.png differ diff --git a/mods/ts/chrome.yaml b/mods/ts/chrome.yaml index 0d3780e27ab3..79f53e431fbf 100644 --- a/mods/ts/chrome.yaml +++ b/mods/ts/chrome.yaml @@ -377,6 +377,10 @@ lobby-bits: buttons.png huepicker: 194,0,7,15 protected: 202,0,10,13 protected-disabled: 213,0,10,13 + admin-registered: 224,0,16,16 + admin-anonymous: 240,0,16,16 + player-registered: 224,16,16,16 + player-anonymous: 240,16,16,16 reload-icon: dialog.png enabled: 160,480,16,16 @@ -460,6 +464,9 @@ dialog4: dialog.png corner-bl: 512,446,6,6 corner-br: 571,446,6,6 +tooltip-separator: dialog.png + border-t: 517,387,54,1 + # A copy of dialog3 (pressed button) progressbar-bg: dialog.png background: 641,1,126,126 diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 71bc08da632d..83387298bea5 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -154,6 +154,7 @@ ChromeLayout: common|chrome/lobby-music.yaml common|chrome/lobby-servers.yaml common|chrome/lobby-kickdialogs.yaml + common|chrome/playerprofile.yaml ts|chrome/color-picker.yaml common|chrome/map-chooser.yaml common|chrome/multiplayer-browser.yaml diff --git a/mods/ts/uibits/buttons.png b/mods/ts/uibits/buttons.png index 0773410ddbc3..b233ad478529 100644 Binary files a/mods/ts/uibits/buttons.png and b/mods/ts/uibits/buttons.png differ