diff --git a/OpenRA.Game/Network/Order.cs b/OpenRA.Game/Network/Order.cs index 015851a406..271202841b 100755 --- a/OpenRA.Game/Network/Order.cs +++ b/OpenRA.Game/Network/Order.cs @@ -199,7 +199,12 @@ namespace OpenRA { return new Order("HandshakeResponse", null, false) { IsImmediate = true, TargetString = text }; } - + + public static Order Pong(string pingTime) + { + return new Order("Pong", null, false) { IsImmediate = true, TargetString = pingTime }; + } + public static Order PauseGame(bool paused) { return new Order("PauseGame", null, false) { TargetString = paused ? "Pause" : "UnPause" }; diff --git a/OpenRA.Game/Network/Session.cs b/OpenRA.Game/Network/Session.cs index a57135b012..98301611b3 100644 --- a/OpenRA.Game/Network/Session.cs +++ b/OpenRA.Game/Network/Session.cs @@ -62,6 +62,8 @@ namespace OpenRA.Network public bool IsReady { get { return State == ClientState.Ready; } } public bool IsObserver { get { return Slot == null; } } public int Ping = -1; + public int PingJitter = -1; + public int[] PingHistory = {}; } public class Slot diff --git a/OpenRA.Game/Network/UnitOrders.cs b/OpenRA.Game/Network/UnitOrders.cs index c16408fe1d..ed65a20529 100755 --- a/OpenRA.Game/Network/UnitOrders.cs +++ b/OpenRA.Game/Network/UnitOrders.cs @@ -190,6 +190,11 @@ namespace OpenRA.Network Game.Debug("{0} has reciprocated",targetPlayer.PlayerName); } + break; + } + case "Ping": + { + orderManager.IssueOrder(Order.Pong(order.TargetString)); break; } default: diff --git a/OpenRA.Game/Server/Connection.cs b/OpenRA.Game/Server/Connection.cs index 8ee7061b0e..0551e23571 100644 --- a/OpenRA.Game/Server/Connection.cs +++ b/OpenRA.Game/Server/Connection.cs @@ -25,8 +25,6 @@ namespace OpenRA.Server public int ExpectLength = 8; public int Frame = 0; public int MostRecentFrame = 0; - public string RemoteAddress; - public int Latency = -1; /* client data */ public int PlayerIndex; @@ -101,34 +99,6 @@ namespace OpenRA.Server } } } - - bool hasBeenPinged; - public void Ping() - { - if (!hasBeenPinged) - { - hasBeenPinged = true; - var pingSender = new Ping(); - pingSender.PingCompleted += new PingCompletedEventHandler(pongRecieved); - AutoResetEvent waiter = new AutoResetEvent(false); - pingSender.SendAsync(RemoteAddress, waiter); - } - } - - void pongRecieved(object sender, PingCompletedEventArgs e) - { - if (e.Cancelled || e.Error != null) - Latency = -1; - else - { - PingReply pong = e.Reply; - if (pong != null && pong.Status == IPStatus.Success) - Latency = (int)pong.RoundtripTime; - else - Latency = -1; - } - } - } public enum ReceiveState { Header, Data }; diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 020950c9b0..b962608669 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -59,8 +59,6 @@ namespace OpenRA.Server public Map Map; XTimer gameTimeout; - int highestLatency; - protected volatile ServerState pState = new ServerState(); public ServerState State { @@ -217,9 +215,6 @@ namespace OpenRA.Server DispatchOrdersToClient(newConn, 0, 0, new ServerOrder("HandshakeRequest", request.Serialize()).Serialize()); } catch (Exception) { DropClient(newConn); } - - newConn.RemoteAddress = ((IPEndPoint)newConn.socket.RemoteEndPoint).Address.ToString(); - newConn.Ping(); } void ValidateClient(Connection newConn, string data) @@ -271,7 +266,8 @@ namespace OpenRA.Server // Check if IP is banned if (lobbyInfo.GlobalSettings.Ban != null) { - if (lobbyInfo.GlobalSettings.Ban.Contains(newConn.RemoteAddress)) + var remote_addr = ((IPEndPoint)newConn.socket.RemoteEndPoint).Address.ToString(); + if (lobbyInfo.GlobalSettings.Ban.Contains(remote_addr)) { Console.WriteLine("Rejected connection from "+client.Name+"("+newConn.socket.RemoteEndPoint+"); Banned."); Log.Write("server", "Rejected connection from {0}; Banned.", @@ -286,8 +282,6 @@ namespace OpenRA.Server preConns.Remove(newConn); conns.Add(newConn); - client.Ping = newConn.Latency; - // Enforce correct PlayerIndex and Slot client.Index = newConn.PlayerIndex; client.Slot = lobbyInfo.FirstEmptySlot(); @@ -296,22 +290,24 @@ namespace OpenRA.Server SyncClientToPlayerReference(client, Map.Players[client.Slot]); lobbyInfo.Clients.Add(client); - //Assume that first validated client is server admin - if(lobbyInfo.Clients.Where(c1 => c1.Bot == null).Count()==1) + + // Assume that first validated client is server admin + if (lobbyInfo.Clients.Where(c1 => c1.Bot == null).Count() == 1) client.IsAdmin=true; OpenRA.Network.Session.Client clientAdmin = lobbyInfo.Clients.Where(c1 => c1.IsAdmin).Single(); - Log.Write("server", "Client {0}: Accepted connection from {1} with {2} ms ping latency.", - newConn.PlayerIndex, newConn.socket.RemoteEndPoint, newConn.Latency); + Log.Write("server", "Client {0}: Accepted connection from {1}.", + newConn.PlayerIndex, newConn.socket.RemoteEndPoint); foreach (var t in ServerTraits.WithInterface()) t.ClientJoined(this, newConn); + SyncLobbyInfo(); SendChat(newConn, "has joined the game."); - SetDynamicOrderLag(); - SyncLobbyInfo(); + // Send initial ping + SendOrderTo(newConn, "Ping", Environment.TickCount.ToString()); if (File.Exists("{0}motd_{1}.txt".F(Platform.SupportDir, lobbyInfo.GlobalSettings.Mods[0]))) { @@ -464,6 +460,31 @@ namespace OpenRA.Server foreach (var c in conns.Except(conn).ToArray()) DispatchOrdersToClient(c, fromIndex, 0, so.Serialize()); break; + case "Pong": + { + int pingSent; + if (!int.TryParse(so.Data, out pingSent)) + { + Log.Write("server", "Invalid order pong payload: {0}", so.Data); + break; + } + + var history = fromClient.PingHistory.ToList(); + history.Add(Environment.TickCount - pingSent); + + // Cap ping history at 5 values (25 seconds) + if (history.Count > 5) + history.RemoveRange(0, history.Count - 5); + + fromClient.Ping = history.Sum() / history.Count; + fromClient.PingJitter = (history.Max() - history.Min())/2; + fromClient.PingHistory = history.ToArray(); + + if (State == ServerState.WaitingPlayers) + SyncLobbyInfo(); + + break; + } } } @@ -503,9 +524,6 @@ namespace OpenRA.Server } } - if (highestLatency == toDrop.Latency) - SetDynamicOrderLag(); - DispatchOrders( toDrop, toDrop.MostRecentFrame, new byte[] { 0xbf } ); if (conns.Count != 0 || lobbyInfo.GlobalSettings.Dedicated) @@ -521,23 +539,6 @@ namespace OpenRA.Server catch { } } - public void SetDynamicOrderLag() - { - foreach (var conn in conns) - { - if (conn.Latency > highestLatency) - highestLatency = conn.Latency; - } - - Log.Write("server", "Measured {0} ms as the highest connection round trip time.".F(highestLatency)); - - lobbyInfo.GlobalSettings.OrderLatency = highestLatency / 120; - if (lobbyInfo.GlobalSettings.OrderLatency < 1) // should never be 0 - lobbyInfo.GlobalSettings.OrderLatency = 1; - - Log.Write("server", "Order lag has been adjusted to {0} frames.".F(lobbyInfo.GlobalSettings.OrderLatency)); - } - public void SyncLobbyInfo() { if (State != ServerState.GameStarted) /* don't do this while the game is running, it breaks things. */ diff --git a/OpenRA.Game/Widgets/MapPreviewWidget.cs b/OpenRA.Game/Widgets/MapPreviewWidget.cs index 667343798c..18ce33f525 100644 --- a/OpenRA.Game/Widgets/MapPreviewWidget.cs +++ b/OpenRA.Game/Widgets/MapPreviewWidget.cs @@ -138,7 +138,7 @@ namespace OpenRA.Widgets { var owned = colors.ContainsKey(p); var pos = ConvertToPreview(p); - var sprite = ChromeProvider.GetImage("spawnpoints", owned ? "owned" : "unowned"); + var sprite = ChromeProvider.GetImage("lobby-bits", owned ? "spawn-claimed" : "spawn-unclaimed"); var offset = new int2(-sprite.bounds.Width/2, -sprite.bounds.Height/2); if (owned) diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 9af3413a88..df0d79d224 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -429,6 +429,7 @@ + diff --git a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs index 0fbb96e0fd..1d2b742c82 100644 --- a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs @@ -100,22 +100,6 @@ namespace OpenRA.Mods.RA.Server server.StartGame(); return true; }}, - { "lag", - s => - { - int lag; - if (!int.TryParse(s, out lag)) - { - Log.Write("server", "Invalid order lag: {0}", s); - return false; - } - - Log.Write("server", "Order lag is now {0} frames.", lag); - - server.lobbyInfo.GlobalSettings.OrderLatency = lag; - server.SyncLobbyInfo(); - return true; - }}, { "slot", s => { diff --git a/OpenRA.Mods.RA/ServerTraits/PlayerPinger.cs b/OpenRA.Mods.RA/ServerTraits/PlayerPinger.cs new file mode 100644 index 0000000000..cf94efda72 --- /dev/null +++ b/OpenRA.Mods.RA/ServerTraits/PlayerPinger.cs @@ -0,0 +1,41 @@ +#region Copyright & License Information +/* + * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using OpenRA.Server; +using OpenRA.Network; +using S = OpenRA.Server.Server; + +namespace OpenRA.Mods.RA.Server +{ + public class PlayerPinger : ServerTrait, ITick + { + int PingInterval = 5000; // Ping every 5 seconds + + // TickTimeout is in microseconds + public int TickTimeout { get { return PingInterval * 100; } } + + int lastPing = 0; + bool isInitialPing = true; + public void Tick(S server) + { + if ((Environment.TickCount - lastPing > PingInterval) || isInitialPing) + { + isInitialPing = false; + lastPing = Environment.TickCount; + foreach (var p in server.conns) + server.SendOrderTo(p, "Ping", Environment.TickCount.ToString()); + } + } + } +} diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs index c7be1963f0..e1604531f8 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -162,20 +162,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic }; mapButton.IsVisible = () => mapButton.Visible && Game.IsHost; - var randomMapButton = lobby.GetOrNull("RANDOMMAP_BUTTON"); - var maps = Game.modData.AvailableMaps.Where(m => m.Value.Selectable).ToArray(); - if (randomMapButton != null && maps.Any()) - { - randomMapButton.OnClick = () => - { - var mapUid = maps.Random(Game.CosmeticRandom).Key; - orderManager.IssueOrder(Order.Command("map " + mapUid)); - Game.Settings.Server.Map = mapUid; - Game.Settings.Save(); - }; - randomMapButton.IsVisible = () => mapButton.Visible && Game.IsHost; - } - var assignTeams = lobby.GetOrNull("ASSIGNTEAMS_DROPDOWNBUTTON"); if (assignTeams != null) { @@ -377,7 +363,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic // get template for possible reuse if (idx < Players.Children.Count) - template = Players.Children [idx]; + template = Players.Children[idx]; // Empty slot if (client == null) @@ -385,29 +371,17 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (template == null || template.Id != EmptySlotTemplate.Id) template = EmptySlotTemplate.Clone(); - Func getText = () => slot.Closed ? "Closed" : "Open"; - var ready = orderManager.LocalClient.IsReady; - if (Game.IsHost) - { - var name = template.Get("NAME_HOST"); - name.IsVisible = () => true; - name.IsDisabled = () => ready; - name.GetText = getText; - name.OnMouseDown = _ => LobbyUtils.ShowSlotDropDown(name, slot, client, orderManager); - } + LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager); else - { - var name = template.Get("NAME"); - name.IsVisible = () => true; - name.GetText = getText; - } + LobbyUtils.SetupSlotWidget(template, slot, client); var join = template.Get("JOIN"); join.IsVisible = () => !slot.Closed; - join.IsDisabled = () => ready; + join.IsDisabled = () => orderManager.LocalClient.IsReady; join.OnClick = () => orderManager.IssueOrder(Order.Command("slot " + key)); } + // Editable player in slot else if ((client.Index == orderManager.LocalClient.Index) || (client.Bot != null && Game.IsHost)) @@ -415,92 +389,33 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (template == null || template.Id != EditablePlayerTemplate.Id) template = EditablePlayerTemplate.Clone(); - var botReady = client.Bot != null && Game.IsHost && orderManager.LocalClient.IsReady; - var ready = botReady || client.IsReady; + LobbyUtils.SetupAdminPingWidget(template, slot, client, orderManager, client.Bot == null); if (client.Bot != null) - { - var name = template.Get("BOT_DROPDOWN"); - name.IsVisible = () => true; - name.IsDisabled = () => ready; - name.GetText = () => client.Name; - name.OnMouseDown = _ => LobbyUtils.ShowSlotDropDown(name, slot, client, orderManager); - } + LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager); else - { - var name = template.Get("NAME"); - name.IsVisible = () => true; - name.IsDisabled = () => ready; - LobbyUtils.SetupNameWidget(orderManager, client, name); - } + LobbyUtils.SetupEditableNameWidget(template, slot, client, orderManager); - var color = template.Get("COLOR"); - color.IsDisabled = () => slot.LockColor || ready; - color.OnMouseDown = _ => LobbyUtils.ShowColorDropDown(color, client, orderManager, colorPreview); - - var colorBlock = color.Get("COLORBLOCK"); - colorBlock.GetColor = () => client.ColorRamp.GetColor(0); - - var faction = template.Get("FACTION"); - faction.IsDisabled = () => slot.LockRace || ready; - faction.OnMouseDown = _ => LobbyUtils.ShowRaceDropDown(faction, client, orderManager, CountryNames); - - var factionname = faction.Get("FACTIONNAME"); - factionname.GetText = () => CountryNames[client.Country]; - var factionflag = faction.Get("FACTIONFLAG"); - factionflag.GetImageName = () => client.Country; - factionflag.GetImageCollection = () => "flags"; - - var team = template.Get("TEAM"); - team.IsDisabled = () => slot.LockTeam || ready; - team.OnMouseDown = _ => LobbyUtils.ShowTeamDropDown(team, client, orderManager, Map); - team.GetText = () => (client.Team == 0) ? "-" : client.Team.ToString(); + LobbyUtils.SetupEditableColorWidget(template, slot, client, orderManager, colorPreview); + LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, CountryNames); + LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, Map.GetSpawnPoints().Length); + LobbyUtils.SetupEditableReadyWidget(template, slot, client, orderManager); if (slot.LockTeam || client.Team > 0) TeamGame = true; - - if (client.Bot == null) - { - // local player - var status = template.Get("STATUS_CHECKBOX"); - status.IsChecked = () => ready; - status.IsVisible = () => true; - status.OnClick = CycleReady; - } - else // Bot - template.Get("STATUS_IMAGE").IsVisible = () => true; } else { // Non-editable player in slot if (template == null || template.Id != NonEditablePlayerTemplate.Id) template = NonEditablePlayerTemplate.Clone(); - template.Get("NAME").GetText = () => client.Name; - if (client.IsAdmin) - template.Get("NAME").Font = "Bold"; - if (client.Ping > -1) - template.Get("NAME").GetColor = () => LobbyUtils.GetPingColor(client.Ping); - - var color = template.Get("COLOR"); - color.GetColor = () => client.ColorRamp.GetColor(0); - - var faction = template.Get("FACTION"); - var factionname = faction.Get("FACTIONNAME"); - factionname.GetText = () => CountryNames[client.Country]; - var factionflag = faction.Get("FACTIONFLAG"); - factionflag.GetImageName = () => client.Country; - factionflag.GetImageCollection = () => "flags"; - - var team = template.Get("TEAM"); - team.GetText = () => (client.Team == 0) ? "-" : client.Team.ToString(); - - template.Get("STATUS_IMAGE").IsVisible = () => - client.Bot != null || client.IsReady; - - var kickButton = template.Get("KICK"); - kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient.Index; - kickButton.IsDisabled = () => orderManager.LocalClient.IsReady; - kickButton.OnClick = () => orderManager.IssueOrder(Order.Command("kick " + client.Index)); + LobbyUtils.SetupAdminPingWidget(template, slot, client, orderManager, client.Bot == null); + LobbyUtils.SetupNameWidget(template, slot, client); + LobbyUtils.SetupKickWidget(template, slot, client, orderManager); + LobbyUtils.SetupColorWidget(template, slot, client); + LobbyUtils.SetupFactionWidget(template, slot, client, CountryNames); + LobbyUtils.SetupTeamWidget(template, slot, client); + LobbyUtils.SetupReadyWidget(template, slot, client); } template.IsVisible = () => true; @@ -518,7 +433,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic { Widget template = null; var c = client; - var ready = c.IsReady; // get template for possible reuse if (idx < Players.Children.Count) @@ -530,20 +444,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (template == null || template.Id != EditableSpectatorTemplate.Id) template = EditableSpectatorTemplate.Clone(); - var name = template.Get("NAME"); - name.IsDisabled = () => ready; - LobbyUtils.SetupNameWidget(orderManager, c, name); - - var color = template.Get("COLOR"); - color.IsDisabled = () => ready; - color.OnMouseDown = _ => LobbyUtils.ShowColorDropDown(color, c, orderManager, colorPreview); - - var colorBlock = color.Get("COLORBLOCK"); - colorBlock.GetColor = () => c.ColorRamp.GetColor(0); - - var status = template.Get("STATUS_CHECKBOX"); - status.IsChecked = () => ready; - status.OnClick = CycleReady; + LobbyUtils.SetupEditableNameWidget(template, null, c, orderManager); + LobbyUtils.SetupEditableColorWidget(template, null, c, orderManager, colorPreview); + LobbyUtils.SetupEditableReadyWidget(template, null, client, orderManager); } // Non-editable spectator else @@ -551,20 +454,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (template == null || template.Id != NonEditableSpectatorTemplate.Id) template = NonEditableSpectatorTemplate.Clone(); - template.Get("NAME").GetText = () => c.Name; - if (client.IsAdmin) - template.Get("NAME").Font = "Bold"; - var color = template.Get("COLOR"); - color.GetColor = () => c.ColorRamp.GetColor(0); - - template.Get("STATUS_IMAGE").IsVisible = () => c.Bot != null || c.IsReady; - - var kickButton = template.Get("KICK"); - kickButton.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index; - kickButton.IsDisabled = () => orderManager.LocalClient.IsReady; - kickButton.OnClick = () => orderManager.IssueOrder(Order.Command("kick " + c.Index)); + LobbyUtils.SetupNameWidget(template, null, client); + LobbyUtils.SetupKickWidget(template, null, client, orderManager); + LobbyUtils.SetupColorWidget(template, null, client); + LobbyUtils.SetupReadyWidget(template, null, client); } + LobbyUtils.SetupAdminPingWidget(template, null, c, orderManager, true); template.IsVisible = () => true; if (idx >= Players.Children.Count) @@ -575,7 +471,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic idx++; } - // Spectate button if (orderManager.LocalClient.Slot != null) { @@ -602,11 +497,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic Players.RemoveChild(Players.Children[idx]); } - void CycleReady() - { - orderManager.IssueOrder(Order.Command("ready")); - } - class DropDownOption { public string Title; diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs index 452b522e6e..8cbf698add 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs @@ -21,31 +21,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic { public static class LobbyUtils { - public static void SetupNameWidget(OrderManager orderManager, Session.Client c, TextFieldWidget name) - { - if (c.IsAdmin) - name.Font = "Bold"; - name.Text = c.Name; - if (c.Ping > -1) - name.TextColor = GetPingColor(c.Ping); - name.OnEnterKey = () => - { - name.Text = name.Text.Trim(); - if (name.Text.Length == 0) - name.Text = c.Name; - - name.LoseFocus(); - if (name.Text == c.Name) - return true; - - orderManager.IssueOrder(Order.Command("name " + name.Text)); - Game.Settings.Player.Name = name.Text; - Game.Settings.Save(); - return true; - }; - name.OnLoseFocus = () => name.OnEnterKey(); - } - class SlotDropDownOption { public string Title; @@ -92,7 +67,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic } public static void ShowTeamDropDown(DropDownButtonWidget dropdown, Session.Client client, - OrderManager orderManager, Map map) + OrderManager orderManager, int teamCount) { Func setupItem = (ii, itemTemplate) => { @@ -103,7 +78,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic return item; }; - var options = Exts.MakeArray(map.GetSpawnPoints().Length + 1, i => i).ToList(); + var options = Exts.MakeArray(teamCount + 1, i => i).ToList(); dropdown.ShowDropDown("TEAM_DROPDOWN_TEMPLATE", 150, options, setupItem); } @@ -192,13 +167,151 @@ namespace OpenRA.Mods.RA.Widgets.Logic } } - public static Color GetPingColor(int ping) + static Color GetPingColor(Session.Client c) { - if (ping > 720) // OrderLag > 6 + if (c.Ping < 0) // Ping unknown + return Color.Gray; + if (c.Ping > 720) // OrderLag > 6 return Color.Red; - if (ping > 360) // OrderLag > 3 + if (c.Ping > 360) // OrderLag > 3 return Color.Orange; + return Color.LimeGreen; } + + public static void SetupAdminPingWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, bool visible) + { + parent.Get("ADMIN_INDICATOR").IsVisible = () => c.IsAdmin; + var block = parent.Get("PING_BLOCK"); + block.IsVisible = () => visible; + + if (visible) + block.Get("PING_COLOR").GetColor = () => GetPingColor(c); + } + + public static void SetupEditableNameWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager) + { + var name = parent.Get("NAME"); + name.IsVisible = () => true; + name.IsDisabled = () => orderManager.LocalClient.IsReady; + + name.Text = c.Name; + name.OnEnterKey = () => + { + name.Text = name.Text.Trim(); + if (name.Text.Length == 0) + name.Text = c.Name; + + name.LoseFocus(); + if (name.Text == c.Name) + return true; + + orderManager.IssueOrder(Order.Command("name " + name.Text)); + Game.Settings.Player.Name = name.Text; + Game.Settings.Save(); + return true; + }; + + name.OnLoseFocus = () => name.OnEnterKey(); + } + + public static void SetupNameWidget(Widget parent, Session.Slot s, Session.Client c) + { + var name = parent.Get("NAME"); + name.GetText = () => c.Name; + } + + public static void SetupEditableSlotWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager) + { + var slot = parent.Get("SLOT_OPTIONS"); + slot.IsVisible = () => true; + slot.IsDisabled = () => orderManager.LocalClient.IsReady; + slot.GetText = () => c != null ? c.Name : s.Closed ? "Closed" : "Open"; + slot.OnMouseDown = _ => LobbyUtils.ShowSlotDropDown(slot, s, c, orderManager); + + // Ensure Name selector (if present) is hidden + var name = parent.GetOrNull("NAME"); + if (name != null) + name.IsVisible = () => false; + } + + public static void SetupSlotWidget(Widget parent, Session.Slot s, Session.Client c) + { + var name = parent.Get("NAME"); + name.IsVisible = () => true; + name.GetText = () => c != null ? c.Name : s.Closed ? "Closed" : "Open"; + + // Ensure Slot selector (if present) is hidden + var slot = parent.GetOrNull("SLOT_OPTIONS"); + if (slot != null) + slot.IsVisible = () => false; + } + + public static void SetupKickWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager) + { + var button = parent.Get("KICK"); + button.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index; + button.IsDisabled = () => orderManager.LocalClient.IsReady; + button.OnClick = () => orderManager.IssueOrder(Order.Command("kick " + c.Index)); + } + + public static void SetupEditableColorWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, ColorPreviewManagerWidget colorPreview) + { + var color = parent.Get("COLOR"); + color.IsDisabled = () => (s != null && s.LockColor) || orderManager.LocalClient.IsReady; + color.OnMouseDown = _ => LobbyUtils.ShowColorDropDown(color, c, orderManager, colorPreview); + + SetupColorWidget(color, s, c); + } + + public static void SetupColorWidget(Widget parent, Session.Slot s, Session.Client c) + { + var color = parent.Get("COLORBLOCK"); + color.GetColor = () => c.ColorRamp.GetColor(0); + } + + public static void SetupEditableFactionWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, Dictionary countryNames) + { + var dropdown = parent.Get("FACTION"); + dropdown.IsDisabled = () => s.LockRace || orderManager.LocalClient.IsReady; + dropdown.OnMouseDown = _ => LobbyUtils.ShowRaceDropDown(dropdown, c, orderManager, countryNames); + SetupFactionWidget(dropdown, s, c, countryNames); + } + + public static void SetupFactionWidget(Widget parent, Session.Slot s, Session.Client c, Dictionary countryNames) + { + var factionname = parent.Get("FACTIONNAME"); + factionname.GetText = () => countryNames[c.Country]; + var factionflag = parent.Get("FACTIONFLAG"); + factionflag.GetImageName = () => c.Country; + factionflag.GetImageCollection = () => "flags"; + } + + public static void SetupEditableTeamWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, int teamCount) + { + var dropdown = parent.Get("TEAM"); + dropdown.IsDisabled = () => s.LockTeam || orderManager.LocalClient.IsReady; + dropdown.OnMouseDown = _ => LobbyUtils.ShowTeamDropDown(dropdown, c, orderManager, teamCount); + dropdown.GetText = () => (c.Team == 0) ? "-" : c.Team.ToString(); + } + + public static void SetupTeamWidget(Widget parent, Session.Slot s, Session.Client c) + { + parent.Get("TEAM").GetText = () => (c.Team == 0) ? "-" : c.Team.ToString(); + } + + public static void SetupEditableReadyWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager) + { + var status = parent.Get("STATUS_CHECKBOX"); + status.IsChecked = () => orderManager.LocalClient.IsReady || c.Bot != null; + status.IsVisible = () => true; + status.IsDisabled = () => c.Bot != null; + status.OnClick = () => orderManager.IssueOrder(Order.Command("ready")); + } + + public static void SetupReadyWidget(Widget parent, Session.Slot s, Session.Client c) + { + parent.Get("STATUS_IMAGE").IsVisible = () => c.IsReady || c.Bot != null; + } } } diff --git a/artsrc/cnc/chrome.svg b/artsrc/cnc/chrome.svg index a78a6a1ae2..6ee015901a 100644 --- a/artsrc/cnc/chrome.svg +++ b/artsrc/cnc/chrome.svg @@ -13,7 +13,7 @@ height="512" id="svg2" version="1.1" - inkscape:version="0.48.1 r9760" + inkscape:version="0.48.2 r9819" sodipodi:docname="chrome.svg" inkscape:export-filename="/Users/paul/src/OpenRA/mods/cnc/uibits/chrome.png" inkscape:export-xdpi="90" @@ -61,9 +61,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="2" - inkscape:cx="159.47924" - inkscape:cy="358.66173" + inkscape:zoom="11.313708" + inkscape:cx="352.22285" + inkscape:cy="474.91804" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" @@ -663,7 +663,7 @@ sodipodi:end="6.2831853" sodipodi:start="0.00014815468" transform="matrix(0.90203431,0,0,0.77317229,6.750015,557.2837)" - d="m 290.17741,50.543926 a 4.9887238,5.8201776 0 1 1 0,-8.62e-4" + d="m 290.17741,50.543926 c -4e-4,3.214396 -2.23426,5.819792 -4.98946,5.819316 -2.75519,-4.77e-4 -4.98839,-2.606645 -4.98798,-5.82104 4e-4,-3.214395 2.23426,-5.819792 4.98946,-5.819315 2.75491,4.76e-4 4.98798,2.606119 4.98798,5.820177" sodipodi:ry="5.8201776" sodipodi:rx="4.9887238" sodipodi:cy="50.543064" @@ -733,7 +733,7 @@ sodipodi:cy="50.543064" sodipodi:rx="4.9887238" sodipodi:ry="5.8201776" - d="m 290.17741,50.543926 a 4.9887238,5.8201776 0 1 1 0,-8.62e-4" + d="m 290.17741,50.543926 c -4e-4,3.214396 -2.23426,5.819792 -4.98946,5.819316 -2.75519,-4.77e-4 -4.98839,-2.606645 -4.98798,-5.82104 4e-4,-3.214395 2.23426,-5.819792 4.98946,-5.819315 2.75491,4.76e-4 4.98798,2.606119 4.98798,5.820177" transform="matrix(0.90203431,0,0,0.77317229,38.750015,525.2837)" sodipodi:start="0.00014815468" sodipodi:end="6.2831853" @@ -1274,5 +1274,13 @@ inkscape:export-filename="/Users/paul/src/OpenRA/mods/cnc/uibits/rect3776.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" /> + diff --git a/mods/cnc/chrome.yaml b/mods/cnc/chrome.yaml index 0e0821f783..32f97902ac 100644 --- a/mods/cnc/chrome.yaml +++ b/mods/cnc/chrome.yaml @@ -386,9 +386,10 @@ music: chrome.png next: 256,16,16,16 prev: 272,16,16,16 -spawnpoints: chrome.png - owned: 256,32,16,16 - unowned: 256,48,16,16 +lobby-bits: chrome.png + spawn-claimed: 256,32,16,16 + spawn-unclaimed: 256,48,16,16 + admin: 340,39,7,5 checkbox-bits: chrome.png checked: 272,32,16,16 diff --git a/mods/cnc/chrome/lobby.yaml b/mods/cnc/chrome/lobby.yaml index f37737d71c..b5f325c6e0 100644 --- a/mods/cnc/chrome/lobby.yaml +++ b/mods/cnc/chrome/lobby.yaml @@ -68,15 +68,35 @@ Container@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Background@PING_BLOCK: + Background:button + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 TextField@NAME: Text:Name - Width:205 + X:15 + Width:190 Height:25 MaxLength:16 Visible:false - DropDownButton@BOT_DROPDOWN: + DropDownButton@SLOT_OPTIONS: Text:Name - Width:205 + X:15 + Width:190 Height:25 Font:Regular Visible:false @@ -136,11 +156,29 @@ Container@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Background@PING_BLOCK: + Background:button + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 Label@NAME: Text:Name - Width:200 + Width:185 Height:25 - X:5 + X:15 Y:0-1 Button@KICK: Text:X @@ -149,12 +187,12 @@ Container@SERVER_LOBBY: X:180 Y:2 Font:Bold - ColorBlock@COLOR: + ColorBlock@COLORBLOCK: X:215 Y:6 Width:35 Height:13 - Label@FACTION: + Container@FACTION: Width:100 Height:25 X:285 @@ -192,9 +230,10 @@ Container@SERVER_LOBBY: Height:25 Visible:false Children: - DropDownButton@NAME_HOST: + DropDownButton@SLOT_OPTIONS: Text:Name - Width:205 + X:15 + Width:190 Height:25 Font:Regular Visible:false @@ -202,7 +241,7 @@ Container@SERVER_LOBBY: Text:Name Width:200 Height:25 - X:5 + X:15 Y:0-1 Visible:false Button@JOIN: @@ -219,9 +258,28 @@ Container@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Background@PING_BLOCK: + Background:button + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 TextField@NAME: Text:Name - Width:205 + X:15 + Width:190 Height:25 MaxLength:16 DropDownButton@COLOR: @@ -256,11 +314,29 @@ Container@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Background@PING_BLOCK: + Background:button + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 Label@NAME: Text:Name - Width:200 + Width:185 Height:25 - X:5 + X:15 Y:0-1 Button@KICK: Text:X @@ -269,7 +345,7 @@ Container@SERVER_LOBBY: X:180 Y:2 Font:Bold - ColorBlock@COLOR: + ColorBlock@COLORBLOCK: X:215 Y:6 Width:35 @@ -300,9 +376,9 @@ Container@SERVER_LOBBY: Button@SPECTATE: Text:Spectate Font:Regular - Width:470 + Width:455 Height:25 - X:0 + X:15 Y:0 Container@LABEL_CONTAINER: X:20 @@ -355,7 +431,7 @@ Container@SERVER_LOBBY: Height:20 Text: Crates DropDownButton@ASSIGNTEAMS_DROPDOWNBUTTON: - X:399 + X:398 Y:255 Width:120 Height:25 diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index d45403031f..a306881e58 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -112,6 +112,7 @@ LoadScreen: CncLoadScreen ServerTraits: LobbyCommands + PlayerPinger MasterServerPinger ChromeMetrics: diff --git a/mods/cnc/uibits/chrome.png b/mods/cnc/uibits/chrome.png index 998f9a4aa3..5a0df4224e 100644 Binary files a/mods/cnc/uibits/chrome.png and b/mods/cnc/uibits/chrome.png differ diff --git a/mods/d2k/chrome.yaml b/mods/d2k/chrome.yaml index 2ce1edabc5..fbee0faecc 100644 --- a/mods/d2k/chrome.yaml +++ b/mods/d2k/chrome.yaml @@ -236,9 +236,10 @@ dialog: dialog.png corner-bl: 191,489,9,9 corner-br: 200,489,9,9 -spawnpoints: spawnpoints.png - unowned: 528,128,16,16 - owned: 512,128,16,16 +lobby-bits: spawnpoints.png + spawn-unclaimed: 528,128,16,16 + spawn-claimed: 512,128,16,16 + admin: 37,5,7,5 strategic: strategic.png unowned: 0,0,32,32 diff --git a/mods/d2k/chrome/lobby.yaml b/mods/d2k/chrome/lobby.yaml index e1969e1cf1..e000aadfec 100644 --- a/mods/d2k/chrome/lobby.yaml +++ b/mods/d2k/chrome/lobby.yaml @@ -48,16 +48,33 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 TextField@NAME: Text:Name - Width:150 + X:15 + Width:135 Height:25 - X:0 - Y:0 MaxLength:16 - DropDownButton@BOT_DROPDOWN: + DropDownButton@SLOT_OPTIONS: Text:Name - Width:150 + X:15 + Width:135 Height:25 Font:Regular Visible:false @@ -117,11 +134,28 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 Label@NAME: Text:Name - Width:145 + Width:130 Height:25 - X:5 + X:20 Y:0-1 Button@KICK: Text:X @@ -130,12 +164,12 @@ Background@SERVER_LOBBY: X:125 Y:2 Font:Bold - ColorBlock@COLOR: + ColorBlock@COLORBLOCK: X:165 Y:6 Width:45 Height:13 - Label@FACTION: + Container@FACTION: Width:130 Height:25 X:250 @@ -167,7 +201,6 @@ Background@SERVER_LOBBY: Height:20 ImageCollection:checkbox-bits ImageName:checked - Container@TEMPLATE_EMPTY: X:5 Y:0 @@ -177,15 +210,15 @@ Background@SERVER_LOBBY: Children: Label@NAME: Text:Name - Width:145 + Width:130 Height:25 - X:5 + X:20 Y:0-1 - DropDownButton@NAME_HOST: + DropDownButton@SLOT_OPTIONS: Text:Name - Width:150 + Width:135 Height:25 - X:0 + X:15 Y:0 Visible:false Button@JOIN: @@ -201,9 +234,27 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 TextField@NAME: Text:Name - Width:150 + X:15 + Width:135 Height:25 MaxLength:16 DropDownButton@COLOR: @@ -238,7 +289,6 @@ Background@SERVER_LOBBY: Height:20 ImageCollection:checkbox-bits ImageName:checked - Container@TEMPLATE_NONEDITABLE_SPECTATOR: X:5 Y:0 @@ -246,11 +296,28 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 Label@NAME: Text:Name - Width:145 + Width:130 Height:25 - X:5 + X:20 Y:0-1 Button@KICK: Text:X @@ -259,7 +326,7 @@ Background@SERVER_LOBBY: X:125 Y:2 Font:Bold - ColorBlock@COLOR: + ColorBlock@COLORBLOCK: X:165 Y:6 Width:45 @@ -386,13 +453,6 @@ Background@SERVER_LOBBY: Height:25 Text:Change Map Font:Bold - Button@RANDOMMAP_BUTTON: - X:PARENT_RIGHT-154 - Y:PARENT_BOTTOM-259 - Width:120 - Height:25 - Text:Random Map - Font:Bold DropDownButton@ASSIGNTEAMS_DROPDOWNBUTTON: X:PARENT_RIGHT-154 Y:PARENT_BOTTOM-229 diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 1af3dc4afa..477dd592b6 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -89,6 +89,7 @@ LoadScreen: D2kLoadScreen ServerTraits: LobbyCommands + PlayerPinger MasterServerPinger ChromeMetrics: diff --git a/mods/ra/chrome.yaml b/mods/ra/chrome.yaml index 56dc9f0ea0..fdb3ed3762 100644 --- a/mods/ra/chrome.yaml +++ b/mods/ra/chrome.yaml @@ -169,9 +169,10 @@ dialog: dialog.png corner-bl: 191,489,9,9 corner-br: 200,489,9,9 -spawnpoints: spawnpoints.png - unowned: 528,128,16,16 - owned: 512,128,16,16 +lobby-bits: spawnpoints.png + spawn-unclaimed: 528,128,16,16 + spawn-claimed: 512,128,16,16 + admin: 37,5,7,5 strategic: strategic.png unowned: 0,0,32,32 diff --git a/mods/ra/chrome/lobby.yaml b/mods/ra/chrome/lobby.yaml index 3b85458a1a..39de30bbac 100644 --- a/mods/ra/chrome/lobby.yaml +++ b/mods/ra/chrome/lobby.yaml @@ -48,16 +48,33 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 TextField@NAME: Text:Name - Width:150 + X:15 + Width:135 Height:25 - X:0 - Y:0 MaxLength:16 - DropDownButton@BOT_DROPDOWN: + DropDownButton@SLOT_OPTIONS: Text:Name - Width:150 + X:15 + Width:135 Height:25 Font:Regular Visible:false @@ -117,11 +134,28 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 Label@NAME: Text:Name - Width:145 + Width:130 Height:25 - X:5 + X:20 Y:0-1 Button@KICK: Text:X @@ -130,12 +164,12 @@ Background@SERVER_LOBBY: X:125 Y:2 Font:Bold - ColorBlock@COLOR: + ColorBlock@COLORBLOCK: X:165 Y:6 Width:45 Height:13 - Label@FACTION: + Container@FACTION: Width:130 Height:25 X:250 @@ -167,7 +201,6 @@ Background@SERVER_LOBBY: Height:20 ImageCollection:checkbox-bits ImageName:checked - Container@TEMPLATE_EMPTY: X:5 Y:0 @@ -177,15 +210,15 @@ Background@SERVER_LOBBY: Children: Label@NAME: Text:Name - Width:145 + Width:130 Height:25 - X:5 + X:20 Y:0-1 - DropDownButton@NAME_HOST: + DropDownButton@SLOT_OPTIONS: Text:Name - Width:150 + Width:135 Height:25 - X:0 + X:15 Y:0 Visible:false Button@JOIN: @@ -201,9 +234,27 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 TextField@NAME: Text:Name - Width:150 + X:15 + Width:135 Height:25 MaxLength:16 DropDownButton@COLOR: @@ -238,7 +289,6 @@ Background@SERVER_LOBBY: Height:20 ImageCollection:checkbox-bits ImageName:checked - Container@TEMPLATE_NONEDITABLE_SPECTATOR: X:5 Y:0 @@ -246,11 +296,28 @@ Background@SERVER_LOBBY: Height:25 Visible:false Children: + Image@ADMIN_INDICATOR: + ImageCollection:lobby-bits + ImageName:admin + X:2 + Visible:false + Container@PING_BLOCK: + X:0 + Y:6 + Width:11 + Height:14 + Visible:false + Children: + ColorBlock@PING_COLOR: + X:2 + Y:2 + Width:PARENT_RIGHT-4 + Height:PARENT_BOTTOM-4 Label@NAME: Text:Name - Width:145 + Width:130 Height:25 - X:5 + X:20 Y:0-1 Button@KICK: Text:X @@ -259,7 +326,7 @@ Background@SERVER_LOBBY: X:125 Y:2 Font:Bold - ColorBlock@COLOR: + ColorBlock@COLORBLOCK: X:165 Y:6 Width:45 diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index bbe96b670d..ae392c4e35 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -103,6 +103,7 @@ LoadScreen: RALoadScreen ServerTraits: LobbyCommands + PlayerPinger MasterServerPinger ChromeMetrics: diff --git a/mods/ra/uibits/spawnpoints.png b/mods/ra/uibits/spawnpoints.png index d889ecdcb7..1e34011111 100644 Binary files a/mods/ra/uibits/spawnpoints.png and b/mods/ra/uibits/spawnpoints.png differ