diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 933a24b273..d3291d3e3d 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -836,6 +836,7 @@
+
diff --git a/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs
index fb410b43f9..084e84bd8d 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs
@@ -10,15 +10,7 @@
#endregion
using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.Linq;
-using System.Net;
-using System.Text;
-using BeaconLib;
using OpenRA.Network;
-using OpenRA.Server;
-using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
@@ -27,246 +19,20 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
static readonly Action DoNothing = () => { };
- readonly Color incompatibleVersionColor;
- readonly Color incompatibleProtectedGameColor;
- readonly Color protectedGameColor;
- readonly Color incompatibleWaitingGameColor;
- readonly Color waitingGameColor;
- readonly Color incompatibleGameStartedColor;
- readonly Color gameStartedColor;
- readonly Color incompatibleGameColor;
- readonly ModData modData;
- readonly WebServices services;
- readonly Probe lanGameProbe;
-
- readonly Widget serverList;
- readonly ScrollItemWidget serverTemplate;
- readonly ScrollItemWidget headerTemplate;
- readonly Widget noticeContainer;
- readonly Widget clientContainer;
- readonly ScrollPanelWidget clientList;
- readonly ScrollItemWidget clientTemplate, clientHeader;
- readonly MapPreviewWidget mapPreview;
- readonly ButtonWidget joinButton;
- readonly int joinButtonY;
-
- GameServer currentServer;
- MapPreview currentMap;
- bool showNotices;
-
- Action onStart;
- Action onExit;
-
- enum SearchStatus { Fetching, Failed, NoGames, Hidden }
-
- SearchStatus searchStatus = SearchStatus.Fetching;
-
- Download currentQuery;
- IEnumerable lanGameLocations;
-
- public string ProgressLabelText()
- {
- switch (searchStatus)
- {
- case SearchStatus.Failed: return "Failed to query server list.";
- case SearchStatus.NoGames: return "No games found. Try changing filters.";
- default: return "";
- }
- }
+ readonly Action onStart;
+ readonly Action onExit;
+ readonly ServerListLogic serverListLogic;
[ObjectCreator.UseCtor]
public MultiplayerLogic(Widget widget, ModData modData, Action onStart, Action onExit, string directConnectHost, int directConnectPort)
{
- this.modData = modData;
+ // MultiplayerLogic is a superset of the ServerListLogic
+ // but cannot be a direct subclass because it needs to pass object-level state to the constructor
+ serverListLogic = new ServerListLogic(widget, modData, Join);
+
this.onStart = onStart;
this.onExit = onExit;
- services = modData.Manifest.Get();
-
- incompatibleVersionColor = ChromeMetrics.Get("IncompatibleVersionColor");
- incompatibleGameColor = ChromeMetrics.Get("IncompatibleGameColor");
- incompatibleProtectedGameColor = ChromeMetrics.Get("IncompatibleProtectedGameColor");
- protectedGameColor = ChromeMetrics.Get("ProtectedGameColor");
- waitingGameColor = ChromeMetrics.Get("WaitingGameColor");
- incompatibleWaitingGameColor = ChromeMetrics.Get("IncompatibleWaitingGameColor");
- gameStartedColor = ChromeMetrics.Get("GameStartedColor");
- incompatibleGameStartedColor = ChromeMetrics.Get("IncompatibleGameStartedColor");
-
- serverList = widget.Get("SERVER_LIST");
- headerTemplate = serverList.Get("HEADER_TEMPLATE");
- serverTemplate = serverList.Get("SERVER_TEMPLATE");
-
- noticeContainer = widget.GetOrNull("NOTICE_CONTAINER");
- if (noticeContainer != null)
- {
- noticeContainer.IsVisible = () => showNotices;
- noticeContainer.Get("OUTDATED_VERSION_LABEL").IsVisible = () => services.ModVersionStatus == ModVersionStatus.Outdated;
- noticeContainer.Get("UNKNOWN_VERSION_LABEL").IsVisible = () => services.ModVersionStatus == ModVersionStatus.Unknown;
- noticeContainer.Get("PLAYTEST_AVAILABLE_LABEL").IsVisible = () => services.ModVersionStatus == ModVersionStatus.PlaytestAvailable;
- }
-
- var noticeWatcher = widget.Get("NOTICE_WATCHER");
- if (noticeWatcher != null && noticeContainer != null)
- {
- var containerHeight = noticeContainer.Bounds.Height;
- noticeWatcher.OnTick = () =>
- {
- var show = services.ModVersionStatus != ModVersionStatus.NotChecked && services.ModVersionStatus != ModVersionStatus.Latest;
- if (show != showNotices)
- {
- var dir = show ? 1 : -1;
- serverList.Bounds.Y += dir * containerHeight;
- serverList.Bounds.Height -= dir * containerHeight;
- showNotices = show;
- }
- };
- }
-
- joinButton = widget.Get("JOIN_BUTTON");
- joinButton.IsVisible = () => currentServer != null;
- joinButton.IsDisabled = () => !currentServer.IsJoinable;
- joinButton.OnClick = () => Join(currentServer);
- joinButtonY = joinButton.Bounds.Y;
-
- // Display the progress label over the server list
- // The text is only visible when the list is empty
- var progressText = widget.Get("PROGRESS_LABEL");
- progressText.IsVisible = () => searchStatus != SearchStatus.Hidden;
- progressText.GetText = ProgressLabelText;
-
- var gs = Game.Settings.Game;
- Action toggleFilterFlag = f =>
- {
- gs.MPGameFilters ^= f;
- Game.Settings.Save();
- RefreshServerList();
- };
-
- var filtersPanel = Ui.LoadWidget("MULTIPLAYER_FILTER_PANEL", null, new WidgetArgs());
- var showWaitingCheckbox = filtersPanel.GetOrNull("WAITING_FOR_PLAYERS");
- if (showWaitingCheckbox != null)
- {
- showWaitingCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Waiting);
- showWaitingCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Waiting);
- }
-
- var showEmptyCheckbox = filtersPanel.GetOrNull("EMPTY");
- if (showEmptyCheckbox != null)
- {
- showEmptyCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Empty);
- showEmptyCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Empty);
- }
-
- var showAlreadyStartedCheckbox = filtersPanel.GetOrNull("ALREADY_STARTED");
- if (showAlreadyStartedCheckbox != null)
- {
- showAlreadyStartedCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Started);
- showAlreadyStartedCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Started);
- }
-
- var showProtectedCheckbox = filtersPanel.GetOrNull("PASSWORD_PROTECTED");
- if (showProtectedCheckbox != null)
- {
- showProtectedCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Protected);
- showProtectedCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Protected);
- }
-
- var showIncompatibleCheckbox = filtersPanel.GetOrNull("INCOMPATIBLE_VERSION");
- if (showIncompatibleCheckbox != null)
- {
- showIncompatibleCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Incompatible);
- showIncompatibleCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Incompatible);
- }
-
- var filtersButton = widget.GetOrNull("FILTERS_DROPDOWNBUTTON");
- if (filtersButton != null)
- {
- filtersButton.IsDisabled = () => searchStatus == SearchStatus.Fetching;
- filtersButton.OnMouseDown = _ =>
- {
- filtersButton.RemovePanel();
- filtersButton.AttachPanel(filtersPanel);
- };
- }
-
- var reloadButton = widget.GetOrNull("RELOAD_BUTTON");
- if (reloadButton != null)
- {
- reloadButton.IsDisabled = () => searchStatus == SearchStatus.Fetching;
- reloadButton.OnClick = RefreshServerList;
-
- var reloadIcon = reloadButton.GetOrNull("IMAGE_RELOAD");
- if (reloadIcon != null)
- {
- var disabledFrame = 0;
- var disabledImage = "disabled-" + disabledFrame.ToString();
- reloadIcon.GetImageName = () => searchStatus == SearchStatus.Fetching ? disabledImage : reloadIcon.ImageName;
-
- var reloadTicker = reloadIcon.Get("ANIMATION");
- if (reloadTicker != null)
- {
- reloadTicker.OnTick = () =>
- {
- disabledFrame = searchStatus == SearchStatus.Fetching ? (disabledFrame + 1) % 12 : 0;
- disabledImage = "disabled-" + disabledFrame.ToString();
- };
- }
- }
- }
-
- mapPreview = widget.GetOrNull("SELECTED_MAP_PREVIEW");
- if (mapPreview != null)
- mapPreview.Preview = () => currentMap;
-
- var mapTitle = widget.GetOrNull("SELECTED_MAP");
- if (mapTitle != null)
- {
- var font = Game.Renderer.Fonts[mapTitle.Font];
- var title = new CachedTransform(m => m == null ? "No Server Selected" :
- WidgetUtils.TruncateText(m.Title, mapTitle.Bounds.Width, font));
- mapTitle.GetText = () => title.Update(currentMap);
- }
-
- var ip = widget.GetOrNull("SELECTED_IP");
- if (ip != null)
- {
- ip.IsVisible = () => currentServer != null;
- ip.GetText = () => currentServer.Address;
- }
-
- var status = widget.GetOrNull("SELECTED_STATUS");
- if (status != null)
- {
- status.IsVisible = () => currentServer != null;
- status.GetText = () => GetStateLabel(currentServer);
- status.GetColor = () => GetStateColor(currentServer, status);
- }
-
- var modVersion = widget.GetOrNull("SELECTED_MOD_VERSION");
- if (modVersion != null)
- {
- modVersion.IsVisible = () => currentServer != null;
- modVersion.GetColor = () => currentServer.IsCompatible ? modVersion.TextColor : incompatibleVersionColor;
-
- var font = Game.Renderer.Fonts[modVersion.Font];
- var version = new CachedTransform(s => WidgetUtils.TruncateText(s.ModLabel, mapTitle.Bounds.Width, font));
- modVersion.GetText = () => version.Update(currentServer);
- }
-
- var players = widget.GetOrNull("SELECTED_PLAYERS");
- if (players != null)
- {
- players.IsVisible = () => currentServer != null && !currentServer.Clients.Any();
- players.GetText = () => PlayersLabel(currentServer);
- }
-
- clientContainer = widget.Get("CLIENT_LIST_CONTAINER");
- clientList = Ui.LoadWidget("MULTIPLAYER_CLIENT_LIST", clientContainer, new WidgetArgs()) as ScrollPanelWidget;
- clientList.IsVisible = () => currentServer != null && currentServer.Clients.Any();
- clientHeader = clientList.Get("HEADER");
- clientTemplate = clientList.Get("TEMPLATE");
- clientList.RemoveChildren();
-
var directConnectButton = widget.Get("DIRECTCONNECT_BUTTON");
directConnectButton.OnClick = () =>
{
@@ -291,20 +57,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
widget.Get("BACK_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); };
- lanGameLocations = new List();
- try
- {
- lanGameProbe = new Probe("OpenRALANGame");
- lanGameProbe.BeaconsUpdated += locations => lanGameLocations = locations;
- lanGameProbe.Start();
- }
- catch (Exception ex)
- {
- Log.Write("debug", "BeaconLib.Probe: " + ex.Message);
- }
-
- RefreshServerList();
-
if (directConnectHost != null)
{
// The connection window must be opened at the end of the tick for the widget hierarchy to
@@ -325,346 +77,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
}
}
- string PlayersLabel(GameServer game)
- {
- return "{0}{1}{2}".F(
- "{0} Player{1}".F(game.Players > 0 ? game.Players.ToString() : "No", game.Players != 1 ? "s" : ""),
- game.Bots > 0 ? ", {0} Bot{1}".F(game.Bots, game.Bots != 1 ? "s" : "") : "",
- game.Spectators > 0 ? ", {0} Spectator{1}".F(game.Spectators, game.Spectators != 1 ? "s" : "") : "");
- }
-
- void RefreshServerList()
- {
- // Query in progress
- if (currentQuery != null)
- return;
-
- searchStatus = SearchStatus.Fetching;
-
- Action onComplete = i =>
- {
- currentQuery = null;
-
- List games = null;
- if (i.Error == null)
- {
- try
- {
- var data = Encoding.UTF8.GetString(i.Result);
- var yaml = MiniYaml.FromString(data);
-
- games = yaml.Select(a => new GameServer(a.Value))
- .Where(gs => gs.Address != null)
- .ToList();
- }
- catch
- {
- searchStatus = SearchStatus.Failed;
- }
- }
-
- var lanGames = new List();
- foreach (var bl in lanGameLocations)
- {
- var game = MiniYaml.FromString(bl.Data)[0].Value;
- var idNode = game.Nodes.FirstOrDefault(n => n.Key == "Id");
-
- // Skip beacons created by this instance and replace Id by expected int value
- if (idNode != null && idNode.Value.Value != Platform.SessionGUID.ToString())
- {
- idNode.Value.Value = "-1";
-
- // Rewrite the server address with the correct IP
- var addressNode = game.Nodes.FirstOrDefault(n => n.Key == "Address");
- if (addressNode != null)
- addressNode.Value.Value = bl.Address.ToString().Split(':')[0] + ":" + addressNode.Value.Value.Split(':')[1];
-
- try
- {
- lanGames.Add(new GameServer(game));
- }
- catch { }
- }
- }
-
- var groupedLanGames = lanGames.GroupBy(gs => gs.Address).Select(g => g.Last());
- if (games != null)
- games.AddRange(groupedLanGames);
- else if (groupedLanGames.Any())
- games = groupedLanGames.ToList();
-
- Game.RunAfterTick(() => RefreshServerListInner(games));
- };
-
- var queryURL = services.ServerList + "?protocol={0}&engine={1}&mod={2}&version={3}".F(
- GameServer.ProtocolVersion,
- Uri.EscapeUriString(Game.EngineVersion),
- Uri.EscapeUriString(Game.ModData.Manifest.Id),
- Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version));
-
- currentQuery = new Download(queryURL, _ => { }, onComplete);
- }
-
- int GroupSortOrder(GameServer testEntry)
- {
- // Games that we can't join are sorted last
- if (!testEntry.IsCompatible)
- return testEntry.Mod == modData.Manifest.Id ? 1 : 0;
-
- // Games for the current mod+version are sorted first
- if (testEntry.Mod == modData.Manifest.Id)
- return testEntry.Version == modData.Manifest.Metadata.Version ? 4 : 3;
-
- // Followed by games for different mods that are joinable
- return 2;
- }
-
- void SelectServer(GameServer server)
- {
- currentServer = server;
- currentMap = server != null ? modData.MapCache[server.Map] : null;
-
- clientList.RemoveChildren();
- if (server == null || !server.Clients.Any())
- {
- joinButton.Bounds.Y = joinButtonY;
- return;
- }
-
- joinButton.Bounds.Y = clientContainer.Bounds.Bottom;
-
- var players = server.Clients
- .Where(c => !c.IsSpectator)
- .GroupBy(p => p.Team)
- .OrderBy(g => g.Key);
-
- var teams = new Dictionary>();
- var noTeams = players.Count() == 1;
- foreach (var p in players)
- {
- var label = noTeams ? "Players" : p.Key == 0 ? "No Team" : "Team {0}".F(p.Key);
- teams.Add(label, p);
- }
-
- if (server.Clients.Any(c => c.IsSpectator))
- teams.Add("Spectators", server.Clients.Where(c => c.IsSpectator));
-
- // Can only show factions if the server is running the same mod
- var disableFactionDisplay = server.Mod != modData.Manifest.Id;
-
- if (mapPreview != null)
- {
- var spawns = currentMap.SpawnPoints;
- var occupants = server.Clients
- .Where(c => (c.SpawnPoint - 1 >= 0) && (c.SpawnPoint - 1 < spawns.Length))
- .ToDictionary(c => spawns[c.SpawnPoint - 1], c => new SpawnOccupant(c, disableFactionDisplay));
-
- mapPreview.SpawnOccupants = () => occupants;
- }
-
- var factionInfo = modData.DefaultRules.Actors["world"].TraitInfos();
- foreach (var kv in teams)
- {
- var group = kv.Key;
- if (group.Length > 0)
- {
- var header = ScrollItemWidget.Setup(clientHeader, () => true, () => { });
- header.Get("LABEL").GetText = () => group;
- clientList.AddChild(header);
- }
-
- foreach (var option in kv.Value)
- {
- var o = option;
-
- var item = ScrollItemWidget.Setup(clientTemplate, () => false, () => { });
- if (!o.IsSpectator && !disableFactionDisplay)
- {
- var label = item.Get("LABEL");
- var font = Game.Renderer.Fonts[label.Font];
- var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font);
- label.GetText = () => name;
- label.GetColor = () => o.Color.RGB;
-
- var flag = item.Get("FLAG");
- flag.IsVisible = () => true;
- flag.GetImageCollection = () => "flags";
- flag.GetImageName = () => (factionInfo != null && factionInfo.Any(f => f.InternalName == o.Faction)) ? o.Faction : "Random";
- }
- else
- {
- var label = item.Get("NOFLAG_LABEL");
- var font = Game.Renderer.Fonts[label.Font];
- var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font);
- label.GetText = () => name;
- label.GetColor = () => o.Color.RGB;
- }
-
- clientList.AddChild(item);
- }
- }
- }
-
- void RefreshServerListInner(List games)
- {
- ScrollItemWidget nextServerRow = null;
- List rows = null;
-
- if (games != null)
- rows = LoadGameRows(games, out nextServerRow);
-
- Game.RunAfterTick(() =>
- {
- serverList.RemoveChildren();
- SelectServer(null);
-
- if (games == null)
- {
- searchStatus = SearchStatus.Failed;
- return;
- }
-
- if (!rows.Any())
- {
- searchStatus = SearchStatus.NoGames;
- return;
- }
-
- searchStatus = SearchStatus.Hidden;
-
- // Search for any unknown maps
- if (Game.Settings.Game.AllowDownloading)
- modData.MapCache.QueryRemoteMapDetails(services.MapRepository, games.Where(g => !Filtered(g)).Select(g => g.Map));
-
- foreach (var row in rows)
- serverList.AddChild(row);
-
- if (nextServerRow != null)
- nextServerRow.OnClick();
- });
- }
-
- List LoadGameRows(List games, out ScrollItemWidget nextServerRow)
- {
- nextServerRow = null;
- var rows = new List();
- var mods = games.GroupBy(g => g.ModLabel)
- .OrderByDescending(g => GroupSortOrder(g.First()))
- .ThenByDescending(g => g.Count());
-
- foreach (var modGames in mods)
- {
- if (modGames.All(Filtered))
- continue;
-
- var header = ScrollItemWidget.Setup(headerTemplate, () => true, () => { });
-
- var headerTitle = modGames.First().ModLabel;
- header.Get("LABEL").GetText = () => headerTitle;
- rows.Add(header);
-
- Func listOrder = g =>
- {
- // Servers waiting for players are always first
- if (g.State == (int)ServerState.WaitingPlayers && g.Players > 0)
- return 0;
-
- // Then servers with spectators
- if (g.State == (int)ServerState.WaitingPlayers && g.Spectators > 0)
- return 1;
-
- // Then active games
- if (g.State >= (int)ServerState.GameStarted)
- return 2;
-
- // Empty servers are shown at the end because a flood of empty servers
- // at the top of the game list make the community look dead
- return 3;
- };
-
- foreach (var modGamesByState in modGames.GroupBy(listOrder).OrderBy(g => g.Key))
- {
- // Sort 'Playing' games by Started, others by number of players
- foreach (var game in modGamesByState.Key == 2 ? modGamesByState.OrderByDescending(g => g.Started) : modGamesByState.OrderByDescending(g => g.Players))
- {
- if (Filtered(game))
- continue;
-
- var canJoin = game.IsJoinable;
- var item = ScrollItemWidget.Setup(serverTemplate, () => currentServer == game, () => SelectServer(game), () => Join(game));
- var title = item.GetOrNull("TITLE");
- if (title != null)
- {
- var font = Game.Renderer.Fonts[title.Font];
- var label = WidgetUtils.TruncateText(game.Name, title.Bounds.Width, font);
- title.GetText = () => label;
- title.GetColor = () => canJoin ? title.TextColor : incompatibleGameColor;
- }
-
- var password = item.GetOrNull("PASSWORD_PROTECTED");
- if (password != null)
- {
- password.IsVisible = () => game.Protected;
- password.GetImageName = () => canJoin ? "protected" : "protected-disabled";
- }
-
- var players = item.GetOrNull("PLAYERS");
- if (players != null)
- {
- var label = "{0} / {1}".F(game.Players + game.Bots, game.MaxPlayers + game.Bots)
- + (game.Spectators > 0 ? " + {0}".F(game.Spectators) : "");
-
- var color = canJoin ? players.TextColor : incompatibleGameColor;
- players.GetText = () => label;
- players.GetColor = () => color;
-
- if (game.Clients.Any())
- {
- var displayClients = game.Clients.Select(c => c.Name);
- if (game.Clients.Length > 10)
- displayClients = displayClients
- .Take(9)
- .Append("+ {0} other players".F(game.Clients.Length - 9));
-
- var tooltip = displayClients.JoinWith("\n");
- players.GetTooltipText = () => tooltip;
- }
- else
- players.GetTooltipText = null;
- }
-
- var state = item.GetOrNull("STATUS");
- if (state != null)
- {
- var label = game.State >= (int)ServerState.GameStarted ?
- "Playing" : "Waiting";
- state.GetText = () => label;
-
- var color = GetStateColor(game, state, !canJoin);
- state.GetColor = () => color;
- }
-
- var location = item.GetOrNull("LOCATION");
- if (location != null)
- {
- var font = Game.Renderer.Fonts[location.Font];
- var cachedServerLocation = game.Id != -1 ? GeoIP.LookupCountry(game.Address.Split(':')[0]) : "Local Network";
- var label = WidgetUtils.TruncateText(cachedServerLocation, location.Bounds.Width, font);
- location.GetText = () => label;
- location.GetColor = () => canJoin ? location.TextColor : incompatibleGameColor;
- }
-
- if (currentServer != null && game.Address == currentServer.Address)
- nextServerRow = item;
-
- rows.Add(item);
- }
- }
- }
-
- return rows;
- }
-
void OpenLobby()
{
// Close the multiplayer browser
@@ -703,76 +115,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic
ConnectionLogic.Connect(host, port, "", OpenLobby, DoNothing);
}
- static string GetStateLabel(GameServer game)
- {
- if (game == null)
- return "";
-
- if (game.State == (int)ServerState.GameStarted)
- {
- var label = "In progress";
-
- if (game.PlayTime > 0)
- {
- var totalMinutes = Math.Ceiling(game.PlayTime / 60.0);
- label += " for {0} minute{1}".F(totalMinutes, totalMinutes > 1 ? "s" : "");
- }
-
- return label;
- }
-
- if (game.State == (int)ServerState.WaitingPlayers)
- return game.Protected ? "Password protected" : "Waiting for players";
-
- if (game.State == (int)ServerState.ShuttingDown)
- return "Server shutting down";
-
- return "Unknown server state";
- }
-
- Color GetStateColor(GameServer game, LabelWidget label, bool darkened = false)
- {
- if (!game.Protected && game.State == (int)ServerState.WaitingPlayers)
- return darkened ? incompatibleWaitingGameColor : waitingGameColor;
-
- if (game.Protected && game.State == (int)ServerState.WaitingPlayers)
- return darkened ? incompatibleProtectedGameColor : protectedGameColor;
-
- if (game.State == (int)ServerState.GameStarted)
- return darkened ? incompatibleGameStartedColor : gameStartedColor;
-
- return label.TextColor;
- }
-
- bool Filtered(GameServer game)
- {
- var filters = Game.Settings.Game.MPGameFilters;
- if (game.State == (int)ServerState.GameStarted && !filters.HasFlag(MPGameFilters.Started))
- return true;
-
- if (game.State == (int)ServerState.WaitingPlayers && !filters.HasFlag(MPGameFilters.Waiting) && game.Players != 0)
- return true;
-
- if ((game.Players + game.Spectators) == 0 && !filters.HasFlag(MPGameFilters.Empty))
- return true;
-
- if (!game.IsCompatible && !filters.HasFlag(MPGameFilters.Incompatible))
- return true;
-
- if (game.Protected && !filters.HasFlag(MPGameFilters.Protected))
- return true;
-
- return false;
- }
-
bool disposed;
protected override void Dispose(bool disposing)
{
if (disposing && !disposed)
{
disposed = true;
- if (lanGameProbe != null)
- lanGameProbe.Dispose();
+ serverListLogic.Dispose();
}
base.Dispose(disposing);
diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs
new file mode 100644
index 0000000000..d04940495c
--- /dev/null
+++ b/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs
@@ -0,0 +1,714 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 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.Linq;
+using System.Net;
+using System.Text;
+using BeaconLib;
+using OpenRA.Network;
+using OpenRA.Server;
+using OpenRA.Traits;
+using OpenRA.Widgets;
+
+namespace OpenRA.Mods.Common.Widgets.Logic
+{
+ public class ServerListLogic : ChromeLogic
+ {
+ readonly Color incompatibleVersionColor;
+ readonly Color incompatibleProtectedGameColor;
+ readonly Color protectedGameColor;
+ readonly Color incompatibleWaitingGameColor;
+ readonly Color waitingGameColor;
+ readonly Color incompatibleGameStartedColor;
+ readonly Color gameStartedColor;
+ readonly Color incompatibleGameColor;
+ readonly ModData modData;
+ readonly WebServices services;
+ readonly Probe lanGameProbe;
+
+ readonly Widget serverList;
+ readonly ScrollItemWidget serverTemplate;
+ readonly ScrollItemWidget headerTemplate;
+ readonly Widget noticeContainer;
+ readonly Widget clientContainer;
+ readonly ScrollPanelWidget clientList;
+ readonly ScrollItemWidget clientTemplate, clientHeader;
+ readonly MapPreviewWidget mapPreview;
+ readonly ButtonWidget joinButton;
+ readonly int joinButtonY;
+
+ readonly Action onJoin;
+
+ GameServer currentServer;
+ MapPreview currentMap;
+ bool showNotices;
+
+ enum SearchStatus { Fetching, Failed, NoGames, Hidden }
+
+ SearchStatus searchStatus = SearchStatus.Fetching;
+
+ Download currentQuery;
+ IEnumerable lanGameLocations;
+
+ public string ProgressLabelText()
+ {
+ switch (searchStatus)
+ {
+ case SearchStatus.Failed: return "Failed to query server list.";
+ case SearchStatus.NoGames: return "No games found. Try changing filters.";
+ default: return "";
+ }
+ }
+
+ [ObjectCreator.UseCtor]
+ public ServerListLogic(Widget widget, ModData modData, Action onJoin)
+ {
+ this.modData = modData;
+ this.onJoin = onJoin;
+
+ services = modData.Manifest.Get();
+
+ incompatibleVersionColor = ChromeMetrics.Get("IncompatibleVersionColor");
+ incompatibleGameColor = ChromeMetrics.Get("IncompatibleGameColor");
+ incompatibleProtectedGameColor = ChromeMetrics.Get("IncompatibleProtectedGameColor");
+ protectedGameColor = ChromeMetrics.Get("ProtectedGameColor");
+ waitingGameColor = ChromeMetrics.Get("WaitingGameColor");
+ incompatibleWaitingGameColor = ChromeMetrics.Get("IncompatibleWaitingGameColor");
+ gameStartedColor = ChromeMetrics.Get("GameStartedColor");
+ incompatibleGameStartedColor = ChromeMetrics.Get("IncompatibleGameStartedColor");
+
+ serverList = widget.Get("SERVER_LIST");
+ headerTemplate = serverList.Get("HEADER_TEMPLATE");
+ serverTemplate = serverList.Get("SERVER_TEMPLATE");
+
+ noticeContainer = widget.GetOrNull("NOTICE_CONTAINER");
+ if (noticeContainer != null)
+ {
+ noticeContainer.IsVisible = () => showNotices;
+ noticeContainer.Get("OUTDATED_VERSION_LABEL").IsVisible = () => services.ModVersionStatus == ModVersionStatus.Outdated;
+ noticeContainer.Get("UNKNOWN_VERSION_LABEL").IsVisible = () => services.ModVersionStatus == ModVersionStatus.Unknown;
+ noticeContainer.Get("PLAYTEST_AVAILABLE_LABEL").IsVisible = () => services.ModVersionStatus == ModVersionStatus.PlaytestAvailable;
+ }
+
+ var noticeWatcher = widget.Get("NOTICE_WATCHER");
+ if (noticeWatcher != null && noticeContainer != null)
+ {
+ var containerHeight = noticeContainer.Bounds.Height;
+ noticeWatcher.OnTick = () =>
+ {
+ var show = services.ModVersionStatus != ModVersionStatus.NotChecked && services.ModVersionStatus != ModVersionStatus.Latest;
+ if (show != showNotices)
+ {
+ var dir = show ? 1 : -1;
+ serverList.Bounds.Y += dir * containerHeight;
+ serverList.Bounds.Height -= dir * containerHeight;
+ showNotices = show;
+ }
+ };
+ }
+
+ joinButton = widget.GetOrNull("JOIN_BUTTON");
+ if (joinButton != null)
+ {
+ joinButton.IsVisible = () => currentServer != null;
+ joinButton.IsDisabled = () => !currentServer.IsJoinable;
+ joinButton.OnClick = () => onJoin(currentServer);
+ joinButtonY = joinButton.Bounds.Y;
+ }
+
+ // Display the progress label over the server list
+ // The text is only visible when the list is empty
+ var progressText = widget.Get("PROGRESS_LABEL");
+ progressText.IsVisible = () => searchStatus != SearchStatus.Hidden;
+ progressText.GetText = ProgressLabelText;
+
+ var gs = Game.Settings.Game;
+ Action toggleFilterFlag = f =>
+ {
+ gs.MPGameFilters ^= f;
+ Game.Settings.Save();
+ RefreshServerList();
+ };
+
+ var filtersButton = widget.GetOrNull("FILTERS_DROPDOWNBUTTON");
+ if (filtersButton != null)
+ {
+ // HACK: MULTIPLAYER_FILTER_PANEL doesn't follow our normal procedure for dropdown creation
+ // but we still need to be able to set the dropdown width based on the parent
+ // The yaml should use PARENT_RIGHT instead of DROPDOWN_WIDTH
+ var filtersPanel = Ui.LoadWidget("MULTIPLAYER_FILTER_PANEL", filtersButton, new WidgetArgs());
+ filtersButton.Children.Remove(filtersPanel);
+
+ var showWaitingCheckbox = filtersPanel.GetOrNull("WAITING_FOR_PLAYERS");
+ if (showWaitingCheckbox != null)
+ {
+ showWaitingCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Waiting);
+ showWaitingCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Waiting);
+ }
+
+ var showEmptyCheckbox = filtersPanel.GetOrNull("EMPTY");
+ if (showEmptyCheckbox != null)
+ {
+ showEmptyCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Empty);
+ showEmptyCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Empty);
+ }
+
+ var showAlreadyStartedCheckbox = filtersPanel.GetOrNull("ALREADY_STARTED");
+ if (showAlreadyStartedCheckbox != null)
+ {
+ showAlreadyStartedCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Started);
+ showAlreadyStartedCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Started);
+ }
+
+ var showProtectedCheckbox = filtersPanel.GetOrNull("PASSWORD_PROTECTED");
+ if (showProtectedCheckbox != null)
+ {
+ showProtectedCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Protected);
+ showProtectedCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Protected);
+ }
+
+ var showIncompatibleCheckbox = filtersPanel.GetOrNull("INCOMPATIBLE_VERSION");
+ if (showIncompatibleCheckbox != null)
+ {
+ showIncompatibleCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Incompatible);
+ showIncompatibleCheckbox.OnClick = () => toggleFilterFlag(MPGameFilters.Incompatible);
+ }
+
+ filtersButton.IsDisabled = () => searchStatus == SearchStatus.Fetching;
+ filtersButton.OnMouseDown = _ =>
+ {
+ filtersButton.RemovePanel();
+ filtersButton.AttachPanel(filtersPanel);
+ };
+ }
+
+ var reloadButton = widget.GetOrNull("RELOAD_BUTTON");
+ if (reloadButton != null)
+ {
+ reloadButton.IsDisabled = () => searchStatus == SearchStatus.Fetching;
+ reloadButton.OnClick = RefreshServerList;
+
+ var reloadIcon = reloadButton.GetOrNull("IMAGE_RELOAD");
+ if (reloadIcon != null)
+ {
+ var disabledFrame = 0;
+ var disabledImage = "disabled-" + disabledFrame.ToString();
+ reloadIcon.GetImageName = () => searchStatus == SearchStatus.Fetching ? disabledImage : reloadIcon.ImageName;
+
+ var reloadTicker = reloadIcon.Get("ANIMATION");
+ if (reloadTicker != null)
+ {
+ reloadTicker.OnTick = () =>
+ {
+ disabledFrame = searchStatus == SearchStatus.Fetching ? (disabledFrame + 1) % 12 : 0;
+ disabledImage = "disabled-" + disabledFrame.ToString();
+ };
+ }
+ }
+ }
+
+ mapPreview = widget.GetOrNull("SELECTED_MAP_PREVIEW");
+ if (mapPreview != null)
+ mapPreview.Preview = () => currentMap;
+
+ var mapTitle = widget.GetOrNull("SELECTED_MAP");
+ if (mapTitle != null)
+ {
+ var font = Game.Renderer.Fonts[mapTitle.Font];
+ var title = new CachedTransform(m => m == null ? "No Server Selected" :
+ WidgetUtils.TruncateText(m.Title, mapTitle.Bounds.Width, font));
+ mapTitle.GetText = () => title.Update(currentMap);
+ }
+
+ var ip = widget.GetOrNull("SELECTED_IP");
+ if (ip != null)
+ {
+ ip.IsVisible = () => currentServer != null;
+ ip.GetText = () => currentServer.Address;
+ }
+
+ var status = widget.GetOrNull("SELECTED_STATUS");
+ if (status != null)
+ {
+ status.IsVisible = () => currentServer != null;
+ status.GetText = () => GetStateLabel(currentServer);
+ status.GetColor = () => GetStateColor(currentServer, status);
+ }
+
+ var modVersion = widget.GetOrNull("SELECTED_MOD_VERSION");
+ if (modVersion != null)
+ {
+ modVersion.IsVisible = () => currentServer != null;
+ modVersion.GetColor = () => currentServer.IsCompatible ? modVersion.TextColor : incompatibleVersionColor;
+
+ var font = Game.Renderer.Fonts[modVersion.Font];
+ var version = new CachedTransform(s => WidgetUtils.TruncateText(s.ModLabel, mapTitle.Bounds.Width, font));
+ modVersion.GetText = () => version.Update(currentServer);
+ }
+
+ var players = widget.GetOrNull("SELECTED_PLAYERS");
+ if (players != null)
+ {
+ players.IsVisible = () => currentServer != null && (clientContainer == null || !currentServer.Clients.Any());
+ players.GetText = () => PlayersLabel(currentServer);
+ }
+
+ clientContainer = widget.GetOrNull("CLIENT_LIST_CONTAINER");
+ if (clientContainer != null)
+ {
+ clientList = Ui.LoadWidget("MULTIPLAYER_CLIENT_LIST", clientContainer, new WidgetArgs()) as ScrollPanelWidget;
+ clientList.IsVisible = () => currentServer != null && currentServer.Clients.Any();
+ clientHeader = clientList.Get("HEADER");
+ clientTemplate = clientList.Get("TEMPLATE");
+ clientList.RemoveChildren();
+ }
+
+ lanGameLocations = new List();
+ try
+ {
+ lanGameProbe = new Probe("OpenRALANGame");
+ lanGameProbe.BeaconsUpdated += locations => lanGameLocations = locations;
+ lanGameProbe.Start();
+ }
+ catch (Exception ex)
+ {
+ Log.Write("debug", "BeaconLib.Probe: " + ex.Message);
+ }
+
+ RefreshServerList();
+ }
+
+ string PlayersLabel(GameServer game)
+ {
+ return "{0}{1}{2}".F(
+ "{0} Player{1}".F(game.Players > 0 ? game.Players.ToString() : "No", game.Players != 1 ? "s" : ""),
+ game.Bots > 0 ? ", {0} Bot{1}".F(game.Bots, game.Bots != 1 ? "s" : "") : "",
+ game.Spectators > 0 ? ", {0} Spectator{1}".F(game.Spectators, game.Spectators != 1 ? "s" : "") : "");
+ }
+
+ void RefreshServerList()
+ {
+ // Query in progress
+ if (currentQuery != null)
+ return;
+
+ searchStatus = SearchStatus.Fetching;
+
+ Action onComplete = i =>
+ {
+ currentQuery = null;
+
+ List games = null;
+ if (i.Error == null)
+ {
+ try
+ {
+ var data = Encoding.UTF8.GetString(i.Result);
+ var yaml = MiniYaml.FromString(data);
+
+ games = yaml.Select(a => new GameServer(a.Value))
+ .Where(gs => gs.Address != null)
+ .ToList();
+ }
+ catch
+ {
+ searchStatus = SearchStatus.Failed;
+ }
+ }
+
+ var lanGames = new List();
+ foreach (var bl in lanGameLocations)
+ {
+ var game = MiniYaml.FromString(bl.Data)[0].Value;
+ var idNode = game.Nodes.FirstOrDefault(n => n.Key == "Id");
+
+ // Skip beacons created by this instance and replace Id by expected int value
+ if (idNode != null && idNode.Value.Value != Platform.SessionGUID.ToString())
+ {
+ idNode.Value.Value = "-1";
+
+ // Rewrite the server address with the correct IP
+ var addressNode = game.Nodes.FirstOrDefault(n => n.Key == "Address");
+ if (addressNode != null)
+ addressNode.Value.Value = bl.Address.ToString().Split(':')[0] + ":" + addressNode.Value.Value.Split(':')[1];
+
+ try
+ {
+ lanGames.Add(new GameServer(game));
+ }
+ catch { }
+ }
+ }
+
+ var groupedLanGames = lanGames.GroupBy(gs => gs.Address).Select(g => g.Last());
+ if (games != null)
+ games.AddRange(groupedLanGames);
+ else if (groupedLanGames.Any())
+ games = groupedLanGames.ToList();
+
+ Game.RunAfterTick(() => RefreshServerListInner(games));
+ };
+
+ var queryURL = services.ServerList + "?protocol={0}&engine={1}&mod={2}&version={3}".F(
+ GameServer.ProtocolVersion,
+ Uri.EscapeUriString(Game.EngineVersion),
+ Uri.EscapeUriString(Game.ModData.Manifest.Id),
+ Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version));
+
+ currentQuery = new Download(queryURL, _ => { }, onComplete);
+ }
+
+ int GroupSortOrder(GameServer testEntry)
+ {
+ // Games that we can't join are sorted last
+ if (!testEntry.IsCompatible)
+ return testEntry.Mod == modData.Manifest.Id ? 1 : 0;
+
+ // Games for the current mod+version are sorted first
+ if (testEntry.Mod == modData.Manifest.Id)
+ return testEntry.Version == modData.Manifest.Metadata.Version ? 4 : 3;
+
+ // Followed by games for different mods that are joinable
+ return 2;
+ }
+
+ void SelectServer(GameServer server)
+ {
+ currentServer = server;
+ currentMap = server != null ? modData.MapCache[server.Map] : null;
+
+ if (server == null || !server.Clients.Any())
+ {
+ if (joinButton != null)
+ joinButton.Bounds.Y = joinButtonY;
+
+ return;
+ }
+
+ if (joinButton != null)
+ joinButton.Bounds.Y = clientContainer.Bounds.Bottom;
+
+ // Can only show factions if the server is running the same mod
+ var disableFactionDisplay = server.Mod != modData.Manifest.Id;
+
+ if (server != null && mapPreview != null)
+ {
+ var spawns = currentMap.SpawnPoints;
+ var occupants = server.Clients
+ .Where(c => (c.SpawnPoint - 1 >= 0) && (c.SpawnPoint - 1 < spawns.Length))
+ .ToDictionary(c => spawns[c.SpawnPoint - 1], c => new SpawnOccupant(c, disableFactionDisplay));
+
+ mapPreview.SpawnOccupants = () => occupants;
+ }
+
+ if (clientList == null)
+ return;
+
+ clientList.RemoveChildren();
+
+ var players = server.Clients
+ .Where(c => !c.IsSpectator)
+ .GroupBy(p => p.Team)
+ .OrderBy(g => g.Key);
+
+ var teams = new Dictionary>();
+ var noTeams = players.Count() == 1;
+ foreach (var p in players)
+ {
+ var label = noTeams ? "Players" : p.Key == 0 ? "No Team" : "Team {0}".F(p.Key);
+ teams.Add(label, p);
+ }
+
+ if (server.Clients.Any(c => c.IsSpectator))
+ teams.Add("Spectators", server.Clients.Where(c => c.IsSpectator));
+
+ var factionInfo = modData.DefaultRules.Actors["world"].TraitInfos();
+ foreach (var kv in teams)
+ {
+ var group = kv.Key;
+ if (group.Length > 0)
+ {
+ var header = ScrollItemWidget.Setup(clientHeader, () => true, () => { });
+ header.Get("LABEL").GetText = () => group;
+ clientList.AddChild(header);
+ }
+
+ foreach (var option in kv.Value)
+ {
+ var o = option;
+
+ var item = ScrollItemWidget.Setup(clientTemplate, () => false, () => { });
+ if (!o.IsSpectator && !disableFactionDisplay)
+ {
+ var label = item.Get("LABEL");
+ var font = Game.Renderer.Fonts[label.Font];
+ var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font);
+ label.GetText = () => name;
+ label.GetColor = () => o.Color.RGB;
+
+ var flag = item.Get("FLAG");
+ flag.IsVisible = () => true;
+ flag.GetImageCollection = () => "flags";
+ flag.GetImageName = () => (factionInfo != null && factionInfo.Any(f => f.InternalName == o.Faction)) ? o.Faction : "Random";
+ }
+ else
+ {
+ var label = item.Get("NOFLAG_LABEL");
+ var font = Game.Renderer.Fonts[label.Font];
+ var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font);
+ label.GetText = () => name;
+ label.GetColor = () => o.Color.RGB;
+ }
+
+ clientList.AddChild(item);
+ }
+ }
+ }
+
+ void RefreshServerListInner(List games)
+ {
+ ScrollItemWidget nextServerRow = null;
+ List rows = null;
+
+ if (games != null)
+ rows = LoadGameRows(games, out nextServerRow);
+
+ Game.RunAfterTick(() =>
+ {
+ serverList.RemoveChildren();
+ SelectServer(null);
+
+ if (games == null)
+ {
+ searchStatus = SearchStatus.Failed;
+ return;
+ }
+
+ if (!rows.Any())
+ {
+ searchStatus = SearchStatus.NoGames;
+ return;
+ }
+
+ searchStatus = SearchStatus.Hidden;
+
+ // Search for any unknown maps
+ if (Game.Settings.Game.AllowDownloading)
+ modData.MapCache.QueryRemoteMapDetails(services.MapRepository, games.Where(g => !Filtered(g)).Select(g => g.Map));
+
+ foreach (var row in rows)
+ serverList.AddChild(row);
+
+ if (nextServerRow != null)
+ nextServerRow.OnClick();
+ });
+ }
+
+ List LoadGameRows(List games, out ScrollItemWidget nextServerRow)
+ {
+ nextServerRow = null;
+ var rows = new List();
+ var mods = games.GroupBy(g => g.ModLabel)
+ .OrderByDescending(g => GroupSortOrder(g.First()))
+ .ThenByDescending(g => g.Count());
+
+ foreach (var modGames in mods)
+ {
+ if (modGames.All(Filtered))
+ continue;
+
+ var header = ScrollItemWidget.Setup(headerTemplate, () => true, () => { });
+
+ var headerTitle = modGames.First().ModLabel;
+ header.Get("LABEL").GetText = () => headerTitle;
+ rows.Add(header);
+
+ Func listOrder = g =>
+ {
+ // Servers waiting for players are always first
+ if (g.State == (int)ServerState.WaitingPlayers && g.Players > 0)
+ return 0;
+
+ // Then servers with spectators
+ if (g.State == (int)ServerState.WaitingPlayers && g.Spectators > 0)
+ return 1;
+
+ // Then active games
+ if (g.State >= (int)ServerState.GameStarted)
+ return 2;
+
+ // Empty servers are shown at the end because a flood of empty servers
+ // at the top of the game list make the community look dead
+ return 3;
+ };
+
+ foreach (var modGamesByState in modGames.GroupBy(listOrder).OrderBy(g => g.Key))
+ {
+ // Sort 'Playing' games by Started, others by number of players
+ foreach (var game in modGamesByState.Key == 2 ? modGamesByState.OrderByDescending(g => g.Started) : modGamesByState.OrderByDescending(g => g.Players))
+ {
+ if (Filtered(game))
+ continue;
+
+ var canJoin = game.IsJoinable;
+ var item = ScrollItemWidget.Setup(serverTemplate, () => currentServer == game, () => SelectServer(game), () => onJoin(game));
+ var title = item.GetOrNull("TITLE");
+ if (title != null)
+ {
+ var font = Game.Renderer.Fonts[title.Font];
+ var label = WidgetUtils.TruncateText(game.Name, title.Bounds.Width, font);
+ title.GetText = () => label;
+ title.GetColor = () => canJoin ? title.TextColor : incompatibleGameColor;
+ }
+
+ var password = item.GetOrNull("PASSWORD_PROTECTED");
+ if (password != null)
+ {
+ password.IsVisible = () => game.Protected;
+ password.GetImageName = () => canJoin ? "protected" : "protected-disabled";
+ }
+
+ var players = item.GetOrNull("PLAYERS");
+ if (players != null)
+ {
+ var label = "{0} / {1}".F(game.Players + game.Bots, game.MaxPlayers + game.Bots)
+ + (game.Spectators > 0 ? " + {0}".F(game.Spectators) : "");
+
+ var color = canJoin ? players.TextColor : incompatibleGameColor;
+ players.GetText = () => label;
+ players.GetColor = () => color;
+
+ if (game.Clients.Any())
+ {
+ var displayClients = game.Clients.Select(c => c.Name);
+ if (game.Clients.Length > 10)
+ displayClients = displayClients
+ .Take(9)
+ .Append("+ {0} other players".F(game.Clients.Length - 9));
+
+ var tooltip = displayClients.JoinWith("\n");
+ players.GetTooltipText = () => tooltip;
+ }
+ else
+ players.GetTooltipText = null;
+ }
+
+ var state = item.GetOrNull("STATUS");
+ if (state != null)
+ {
+ var label = game.State >= (int)ServerState.GameStarted ?
+ "Playing" : "Waiting";
+ state.GetText = () => label;
+
+ var color = GetStateColor(game, state, !canJoin);
+ state.GetColor = () => color;
+ }
+
+ var location = item.GetOrNull("LOCATION");
+ if (location != null)
+ {
+ var font = Game.Renderer.Fonts[location.Font];
+ var cachedServerLocation = game.Id != -1 ? GeoIP.LookupCountry(game.Address.Split(':')[0]) : "Local Network";
+ var label = WidgetUtils.TruncateText(cachedServerLocation, location.Bounds.Width, font);
+ location.GetText = () => label;
+ location.GetColor = () => canJoin ? location.TextColor : incompatibleGameColor;
+ }
+
+ if (currentServer != null && game.Address == currentServer.Address)
+ nextServerRow = item;
+
+ rows.Add(item);
+ }
+ }
+ }
+
+ return rows;
+ }
+
+ static string GetStateLabel(GameServer game)
+ {
+ if (game == null)
+ return "";
+
+ if (game.State == (int)ServerState.GameStarted)
+ {
+ var label = "In progress";
+
+ if (game.PlayTime > 0)
+ {
+ var totalMinutes = Math.Ceiling(game.PlayTime / 60.0);
+ label += " for {0} minute{1}".F(totalMinutes, totalMinutes > 1 ? "s" : "");
+ }
+
+ return label;
+ }
+
+ if (game.State == (int)ServerState.WaitingPlayers)
+ return game.Protected ? "Password protected" : "Waiting for players";
+
+ if (game.State == (int)ServerState.ShuttingDown)
+ return "Server shutting down";
+
+ return "Unknown server state";
+ }
+
+ Color GetStateColor(GameServer game, LabelWidget label, bool darkened = false)
+ {
+ if (!game.Protected && game.State == (int)ServerState.WaitingPlayers)
+ return darkened ? incompatibleWaitingGameColor : waitingGameColor;
+
+ if (game.Protected && game.State == (int)ServerState.WaitingPlayers)
+ return darkened ? incompatibleProtectedGameColor : protectedGameColor;
+
+ if (game.State == (int)ServerState.GameStarted)
+ return darkened ? incompatibleGameStartedColor : gameStartedColor;
+
+ return label.TextColor;
+ }
+
+ bool Filtered(GameServer game)
+ {
+ var filters = Game.Settings.Game.MPGameFilters;
+ if (game.State == (int)ServerState.GameStarted && !filters.HasFlag(MPGameFilters.Started))
+ return true;
+
+ if (game.State == (int)ServerState.WaitingPlayers && !filters.HasFlag(MPGameFilters.Waiting) && game.Players != 0)
+ return true;
+
+ if ((game.Players + game.Spectators) == 0 && !filters.HasFlag(MPGameFilters.Empty))
+ return true;
+
+ if (!game.IsCompatible && !filters.HasFlag(MPGameFilters.Incompatible))
+ return true;
+
+ if (game.Protected && !filters.HasFlag(MPGameFilters.Protected))
+ return true;
+
+ return false;
+ }
+
+ bool disposed;
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && !disposed)
+ {
+ disposed = true;
+ if (lanGameProbe != null)
+ lanGameProbe.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/mods/cnc/chrome/multiplayer-browserpanels.yaml b/mods/cnc/chrome/multiplayer-browserpanels.yaml
index af3cccdbed..a6c3671aae 100644
--- a/mods/cnc/chrome/multiplayer-browserpanels.yaml
+++ b/mods/cnc/chrome/multiplayer-browserpanels.yaml
@@ -40,7 +40,7 @@ ScrollPanel@MULTIPLAYER_CLIENT_LIST:
Shadow: True
ScrollPanel@MULTIPLAYER_FILTER_PANEL:
- Width: 147
+ Width: PARENT_RIGHT
Height: 130
Background: panel-black
Children:
diff --git a/mods/common/chrome/multiplayer-browserpanels.yaml b/mods/common/chrome/multiplayer-browserpanels.yaml
index 93133d3c1f..2651074fcc 100644
--- a/mods/common/chrome/multiplayer-browserpanels.yaml
+++ b/mods/common/chrome/multiplayer-browserpanels.yaml
@@ -41,7 +41,7 @@ ScrollPanel@MULTIPLAYER_CLIENT_LIST:
Shadow: True
ScrollPanel@MULTIPLAYER_FILTER_PANEL:
- Width: 158
+ Width: PARENT_RIGHT
Height: 130
Children:
Checkbox@WAITING_FOR_PLAYERS: