Files
OpenRA/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs
RoosterDragon b7e0ed9b87 Improve lookups of nodes by key in MiniYaml.
When handling the Nodes collection in MiniYaml, individual nodes are located via one of two methods:

// Lookup a single key with linear search.
var node = yaml.Nodes.FirstOrDefault(n => n.Key == "SomeKey");

// Convert to dictionary, expecting many key lookups.
var dict = nodes.ToDictionary();

// Lookup a single key in the dictionary.
var node = dict["SomeKey"];

To simplify lookup of individual keys via linear search, provide helper methods NodeWithKeyOrDefault and NodeWithKey. These helpers do the equivalent of Single{OrDefault} searches. Whilst this requires checking the whole list, it provides a useful correctness check. Two duplicated keys in TS yaml are fixed as a result. We can also optimize the helpers to not use LINQ, avoiding allocation of the delegate to search for a key.

Adjust existing code to use either lnear searches or dictionary lookups based on whether it will be resolving many keys. Resolving few keys can be done with linear searches to avoid building a dictionary. Resolving many keys should be done with a dictionary to avoid quaradtic runtime from repeated linear searches.
2023-09-23 14:31:04 +02:00

872 lines
28 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.Linq;
using System.Threading.Tasks;
using BeaconLib;
using OpenRA.Network;
using OpenRA.Primitives;
using OpenRA.Server;
using OpenRA.Support;
using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class ServerListLogic : ChromeLogic
{
[TranslationReference]
const string SearchStatusFailed = "label-search-status-failed";
[TranslationReference]
const string SearchStatusNoGames = "label-search-status-no-games";
[TranslationReference("players")]
const string PlayersOnline = "label-players-online-count";
[TranslationReference]
const string NoServerSelected = "label-no-server-selected";
[TranslationReference]
const string MapStatusSearching = "label-map-status-searching";
[TranslationReference]
const string MapClassificationUnknown = "label-map-classification-unknown";
[TranslationReference("players")]
const string PlayersLabel = "label-players-count";
[TranslationReference("bots")]
const string BotsLabel = "label-bots-count";
[TranslationReference("spectators")]
const string SpectatorsLabel = "label-spectators-count";
[TranslationReference]
const string Players = "label-players";
[TranslationReference("team")]
const string TeamNumber = "label-team-name";
[TranslationReference]
const string NoTeam = "label-no-team";
[TranslationReference]
const string Spectators = "label-spectators";
[TranslationReference("players")]
const string OtherPlayers = "label-other-players-count";
[TranslationReference]
const string Playing = "label-playing";
[TranslationReference]
const string Waiting = "label-waiting";
[TranslationReference("minutes")]
const string InProgress = "label-in-progress-for";
[TranslationReference]
const string PasswordProtected = "label-password-protected";
[TranslationReference]
const string WaitingForPlayers = "label-waiting-for-players";
[TranslationReference]
const string ServerShuttingDown = "label-server-shutting-down";
[TranslationReference]
const string UnknownServerState = "label-unknown-server-state";
readonly string noServerSelected;
readonly string mapStatusSearching;
readonly string mapClassificationUnknown;
readonly string playing;
readonly string waiting;
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<GameServer> onJoin;
GameServer currentServer;
MapPreview currentMap;
bool showNotices;
int playerCount;
enum SearchStatus { Fetching, Failed, NoGames, Hidden }
SearchStatus searchStatus = SearchStatus.Fetching;
bool activeQuery;
IEnumerable<BeaconLocation> lanGameLocations;
readonly CachedTransform<int, string> players;
readonly CachedTransform<int, string> bots;
readonly CachedTransform<int, string> spectators;
readonly CachedTransform<double, string> minutes;
readonly string passwordProtected;
readonly string waitingForPlayers;
readonly string serverShuttingDown;
readonly string unknownServerState;
public string ProgressLabelText()
{
switch (searchStatus)
{
case SearchStatus.Failed: return TranslationProvider.GetString(SearchStatusFailed);
case SearchStatus.NoGames: return TranslationProvider.GetString(SearchStatusNoGames);
default: return "";
}
}
[ObjectCreator.UseCtor]
public ServerListLogic(Widget widget, ModData modData, Action<GameServer> onJoin)
{
this.modData = modData;
this.onJoin = onJoin;
playing = TranslationProvider.GetString(Playing);
waiting = TranslationProvider.GetString(Waiting);
noServerSelected = TranslationProvider.GetString(NoServerSelected);
mapStatusSearching = TranslationProvider.GetString(MapStatusSearching);
mapClassificationUnknown = TranslationProvider.GetString(MapClassificationUnknown);
players = new CachedTransform<int, string>(i => TranslationProvider.GetString(PlayersLabel, Translation.Arguments("players", i)));
bots = new CachedTransform<int, string>(i => TranslationProvider.GetString(BotsLabel, Translation.Arguments("bots", i)));
spectators = new CachedTransform<int, string>(i => TranslationProvider.GetString(SpectatorsLabel, Translation.Arguments("spectators", i)));
minutes = new CachedTransform<double, string>(i => TranslationProvider.GetString(InProgress, Translation.Arguments("minutes", i)));
passwordProtected = TranslationProvider.GetString(PasswordProtected);
waitingForPlayers = TranslationProvider.GetString(WaitingForPlayers);
serverShuttingDown = TranslationProvider.GetString(ServerShuttingDown);
unknownServerState = TranslationProvider.GetString(UnknownServerState);
services = modData.Manifest.Get<WebServices>();
incompatibleVersionColor = ChromeMetrics.Get<Color>("IncompatibleVersionColor");
incompatibleGameColor = ChromeMetrics.Get<Color>("IncompatibleGameColor");
incompatibleProtectedGameColor = ChromeMetrics.Get<Color>("IncompatibleProtectedGameColor");
protectedGameColor = ChromeMetrics.Get<Color>("ProtectedGameColor");
waitingGameColor = ChromeMetrics.Get<Color>("WaitingGameColor");
incompatibleWaitingGameColor = ChromeMetrics.Get<Color>("IncompatibleWaitingGameColor");
gameStartedColor = ChromeMetrics.Get<Color>("GameStartedColor");
incompatibleGameStartedColor = ChromeMetrics.Get<Color>("IncompatibleGameStartedColor");
serverList = widget.Get<ScrollPanelWidget>("SERVER_LIST");
headerTemplate = serverList.Get<ScrollItemWidget>("HEADER_TEMPLATE");
serverTemplate = serverList.Get<ScrollItemWidget>("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<LogicTickerWidget>("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<ButtonWidget>("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<LabelWidget>("PROGRESS_LABEL");
progressText.IsVisible = () => searchStatus != SearchStatus.Hidden;
progressText.GetText = ProgressLabelText;
var gs = Game.Settings.Game;
void ToggleFilterFlag(MPGameFilters f)
{
gs.MPGameFilters ^= f;
Game.Settings.Save();
RefreshServerList();
}
var filtersButton = widget.GetOrNull<DropDownButtonWidget>("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<CheckboxWidget>("WAITING_FOR_PLAYERS");
if (showWaitingCheckbox != null)
{
showWaitingCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Waiting);
showWaitingCheckbox.OnClick = () => ToggleFilterFlag(MPGameFilters.Waiting);
}
var showEmptyCheckbox = filtersPanel.GetOrNull<CheckboxWidget>("EMPTY");
if (showEmptyCheckbox != null)
{
showEmptyCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Empty);
showEmptyCheckbox.OnClick = () => ToggleFilterFlag(MPGameFilters.Empty);
}
var showAlreadyStartedCheckbox = filtersPanel.GetOrNull<CheckboxWidget>("ALREADY_STARTED");
if (showAlreadyStartedCheckbox != null)
{
showAlreadyStartedCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Started);
showAlreadyStartedCheckbox.OnClick = () => ToggleFilterFlag(MPGameFilters.Started);
}
var showProtectedCheckbox = filtersPanel.GetOrNull<CheckboxWidget>("PASSWORD_PROTECTED");
if (showProtectedCheckbox != null)
{
showProtectedCheckbox.IsChecked = () => gs.MPGameFilters.HasFlag(MPGameFilters.Protected);
showProtectedCheckbox.OnClick = () => ToggleFilterFlag(MPGameFilters.Protected);
}
var showIncompatibleCheckbox = filtersPanel.GetOrNull<CheckboxWidget>("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<ButtonWidget>("RELOAD_BUTTON");
if (reloadButton != null)
{
reloadButton.IsDisabled = () => searchStatus == SearchStatus.Fetching;
reloadButton.OnClick = RefreshServerList;
var reloadIcon = reloadButton.GetOrNull<ImageWidget>("IMAGE_RELOAD");
if (reloadIcon != null)
{
var disabledFrame = 0;
var disabledImage = "disabled-" + disabledFrame.ToStringInvariant();
reloadIcon.GetImageName = () => searchStatus == SearchStatus.Fetching ? disabledImage : reloadIcon.ImageName;
var reloadTicker = reloadIcon.Get<LogicTickerWidget>("ANIMATION");
if (reloadTicker != null)
{
reloadTicker.OnTick = () =>
{
disabledFrame = searchStatus == SearchStatus.Fetching ? (disabledFrame + 1) % 12 : 0;
disabledImage = "disabled-" + disabledFrame.ToStringInvariant();
};
}
}
}
var playersLabel = widget.GetOrNull<LabelWidget>("PLAYER_COUNT");
if (playersLabel != null)
{
var playersText = new CachedTransform<int, string>(p => TranslationProvider.GetString(PlayersOnline, Translation.Arguments("players", p)));
playersLabel.IsVisible = () => playerCount != 0;
playersLabel.GetText = () => playersText.Update(playerCount);
}
mapPreview = widget.GetOrNull<MapPreviewWidget>("SELECTED_MAP_PREVIEW");
if (mapPreview != null)
mapPreview.Preview = () => currentMap;
var mapTitle = widget.GetOrNull<LabelWithTooltipWidget>("SELECTED_MAP");
if (mapTitle != null)
{
var font = Game.Renderer.Fonts[mapTitle.Font];
var title = new CachedTransform<MapPreview, string>(m =>
{
var truncated = WidgetUtils.TruncateText(m.Title, mapTitle.Bounds.Width, font);
if (m.Title != truncated)
mapTitle.GetTooltipText = () => m.Title;
else
mapTitle.GetTooltipText = null;
return truncated;
});
mapTitle.GetText = () =>
{
if (currentMap == null)
return noServerSelected;
if (currentMap.Status == MapStatus.Searching)
return mapStatusSearching;
if (currentMap.Class == MapClassification.Unknown)
return mapClassificationUnknown;
return title.Update(currentMap);
};
}
var ip = widget.GetOrNull<LabelWidget>("SELECTED_IP");
if (ip != null)
{
ip.IsVisible = () => currentServer != null;
ip.GetText = () => currentServer.Address;
}
var status = widget.GetOrNull<LabelWidget>("SELECTED_STATUS");
if (status != null)
{
status.IsVisible = () => currentServer != null;
status.GetText = () => GetStateLabel(currentServer);
status.GetColor = () => GetStateColor(currentServer, status);
}
var modVersion = widget.GetOrNull<LabelWidget>("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<GameServer, string>(s => WidgetUtils.TruncateText(s.ModLabel, modVersion.Bounds.Width, font));
modVersion.GetText = () => version.Update(currentServer);
}
var selectedPlayers = widget.GetOrNull<LabelWidget>("SELECTED_PLAYERS");
if (selectedPlayers != null)
{
selectedPlayers.IsVisible = () => currentServer != null && (clientContainer == null || currentServer.Clients.Length == 0);
selectedPlayers.GetText = () => PlayerLabel(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.Length > 0;
clientHeader = clientList.Get<ScrollItemWidget>("HEADER");
clientTemplate = clientList.Get<ScrollItemWidget>("TEMPLATE");
clientList.RemoveChildren();
}
lanGameLocations = new List<BeaconLocation>();
try
{
lanGameProbe = new Probe("OpenRALANGame");
lanGameProbe.BeaconsUpdated += locations => lanGameLocations = locations;
lanGameProbe.Start();
}
catch (Exception ex)
{
Log.Write("debug", "BeaconLib.Probe: " + ex.Message);
}
RefreshServerList();
}
string PlayerLabel(GameServer game)
{
var label = players.Update(game.Players);
if (game.Bots > 0)
label += " " + bots.Update(game.Bots);
if (game.Spectators > 0)
label += " " + spectators.Update(game.Spectators);
return label;
}
public void RefreshServerList()
{
// Query in progress
if (activeQuery)
return;
searchStatus = SearchStatus.Fetching;
var queryURL = new HttpQueryBuilder(services.ServerList)
{
{ "protocol", GameServer.ProtocolVersion },
{ "engine", Game.EngineVersion },
{ "mod", Game.ModData.Manifest.Id },
{ "version", Game.ModData.Manifest.Metadata.Version }
}.ToString();
Task.Run(async () =>
{
List<GameServer> games = null;
activeQuery = true;
try
{
var client = HttpClientFactory.Create();
var httpResponseMessage = await client.GetAsync(queryURL);
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result);
games = new List<GameServer>();
foreach (var node in yaml)
{
try
{
var gs = new GameServer(node.Value);
if (gs.Address != null)
games.Add(gs);
}
catch
{
// Ignore any invalid games advertised.
}
}
}
catch (Exception e)
{
searchStatus = SearchStatus.Failed;
Log.Write("debug", $"Failed to query server list with exception: {e}");
}
var lanGames = new List<GameServer>();
foreach (var bl in lanGameLocations)
{
try
{
if (string.IsNullOrEmpty(bl.Data))
continue;
var game = new MiniYamlBuilder(MiniYaml.FromString(bl.Data)[0].Value);
var idNode = game.NodeWithKeyOrDefault("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.NodeWithKeyOrDefault("Address");
if (addressNode != null)
addressNode.Value.Value = bl.Address.ToString().Split(':')[0] + ":" + addressNode.Value.Value.Split(':')[1];
game.Nodes.Add(new MiniYamlNodeBuilder("Location", "Local Network"));
lanGames.Add(new GameServer(game.Build()));
}
}
catch
{
// Ignore any invalid LAN games advertised.
}
}
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));
activeQuery = false;
});
}
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;
// Can only show factions if the server is running the same mod
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 => c.SpawnPoint, c => new SpawnOccupant(c, server.Mod != modData.Manifest.Id));
mapPreview.SpawnOccupants = () => occupants;
mapPreview.DisabledSpawnPoints = () => server.DisabledSpawnPoints;
}
if (server == null || server.Clients.Length == 0)
{
if (joinButton != null)
joinButton.Bounds.Y = joinButtonY;
return;
}
if (joinButton != null)
joinButton.Bounds.Y = clientContainer.Bounds.Bottom;
if (clientList == null)
return;
clientList.RemoveChildren();
var players = server.Clients
.Where(c => !c.IsSpectator)
.GroupBy(p => p.Team)
.OrderBy(g => g.Key)
.ToList();
var teams = new Dictionary<string, IEnumerable<GameClient>>();
var noTeams = players.Count == 1;
foreach (var p in players)
{
var label = noTeams ? TranslationProvider.GetString(Players) : p.Key > 0
? TranslationProvider.GetString(TeamNumber, Translation.Arguments("team", p.Key))
: TranslationProvider.GetString(NoTeam);
teams.Add(label, p);
}
if (server.Clients.Any(c => c.IsSpectator))
teams.Add(TranslationProvider.GetString(Spectators), server.Clients.Where(c => c.IsSpectator));
var factionInfo = modData.DefaultRules.Actors[SystemActors.World].TraitInfos<FactionInfo>();
foreach (var kv in teams)
{
var group = kv.Key;
if (group.Length > 0)
{
var header = ScrollItemWidget.Setup(clientHeader, () => false, () => { });
header.Get<LabelWidget>("LABEL").GetText = () => group;
clientList.AddChild(header);
}
foreach (var option in kv.Value)
{
var o = option;
var item = ScrollItemWidget.Setup(clientTemplate, () => false, () => { });
if (!o.IsSpectator && server.Mod == modData.Manifest.Id)
{
var label = item.Get<LabelWidget>("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;
var flag = item.Get<ImageWidget>("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<LabelWidget>("NOFLAG_LABEL");
var font = Game.Renderer.Fonts[label.Font];
var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font);
// Force spectator color to prevent spoofing by the server
var color = o.IsSpectator ? Color.White : o.Color;
label.GetText = () => name;
label.GetColor = () => color;
}
clientList.AddChild(item);
}
}
}
void RefreshServerListInner(List<GameServer> games)
{
ScrollItemWidget nextServerRow = null;
List<Widget> 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.Count == 0)
{
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);
nextServerRow?.OnClick();
playerCount = games.Sum(g => g.Players);
});
}
List<Widget> LoadGameRows(List<GameServer> games, out ScrollItemWidget nextServerRow)
{
nextServerRow = null;
var rows = new List<Widget>();
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, () => false, () => { });
var headerTitle = modGames.First().ModLabel;
header.Get<LabelWidget>("LABEL").GetText = () => headerTitle;
rows.Add(header);
static int ListOrder(GameServer 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<LabelWithTooltipWidget>("TITLE");
if (title != null)
{
WidgetUtils.TruncateLabelToTooltip(title, game.Name);
title.GetColor = () => canJoin ? title.TextColor : incompatibleGameColor;
}
var password = item.GetOrNull<ImageWidget>("PASSWORD_PROTECTED");
if (password != null)
{
password.IsVisible = () => game.Protected;
password.GetImageName = () => canJoin ? "protected" : "protected-disabled";
}
var auth = item.GetOrNull<ImageWidget>("REQUIRES_AUTHENTICATION");
if (auth != null)
{
auth.IsVisible = () => game.Authentication;
auth.GetImageName = () => canJoin ? "authentication" : "authentication-disabled";
if (game.Protected && password != null)
auth.Bounds.X -= password.Bounds.Width + 5;
}
var players = item.GetOrNull<LabelWithTooltipWidget>("PLAYERS");
if (players != null)
{
var label = $"{game.Players + game.Bots} / {game.MaxPlayers + game.Bots}"
+ (game.Spectators > 0 ? $" + {game.Spectators}" : "");
var color = canJoin ? players.TextColor : incompatibleGameColor;
players.GetText = () => label;
players.GetColor = () => color;
if (game.Clients.Length > 0)
{
var displayClients = game.Clients.Select(c => c.Name);
if (game.Clients.Length > 10)
displayClients = displayClients
.Take(9)
.Append(TranslationProvider.GetString(OtherPlayers, Translation.Arguments("players", game.Clients.Length - 9)));
var tooltip = displayClients.JoinWith("\n");
players.GetTooltipText = () => tooltip;
}
else
players.GetTooltipText = null;
}
var state = item.GetOrNull<LabelWidget>("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<LabelWidget>("LOCATION");
if (location != null)
{
var font = Game.Renderer.Fonts[location.Font];
var label = WidgetUtils.TruncateText(game.Location, 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;
}
string GetStateLabel(GameServer game)
{
if (game == null)
return string.Empty;
if (game.State == (int)ServerState.GameStarted)
{
var totalMinutes = Math.Ceiling(game.PlayTime / 60.0);
return minutes.Update(totalMinutes);
}
if (game.State == (int)ServerState.WaitingPlayers)
return game.Protected ? passwordProtected : waitingForPlayers;
if (game.State == (int)ServerState.ShuttingDown)
return serverShuttingDown;
return unknownServerState;
}
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;
lanGameProbe?.Dispose();
}
base.Dispose(disposing);
}
}
}