diff --git a/OpenRA.Game/Network/GameServer.cs b/OpenRA.Game/Network/GameServer.cs
index f8e3e64024..165fc1c38f 100644
--- a/OpenRA.Game/Network/GameServer.cs
+++ b/OpenRA.Game/Network/GameServer.cs
@@ -9,62 +9,209 @@
*/
#endregion
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using OpenRA.Graphics;
+
namespace OpenRA.Network
{
+ public class GameClient
+ {
+ public readonly string Name;
+ public readonly HSLColor Color;
+ public readonly string Faction;
+ public readonly int Team;
+ public readonly int SpawnPoint;
+ public readonly bool IsAdmin;
+ public readonly bool IsSpectator;
+ public readonly bool IsBot;
+
+ public GameClient() { }
+
+ public GameClient(Session.Client c)
+ {
+ Name = c.Name;
+ Color = c.Color;
+ Faction = c.Faction;
+ Team = c.Team;
+ SpawnPoint = c.SpawnPoint;
+ IsAdmin = c.IsAdmin;
+ IsSpectator = c.Slot == null && c.Bot == null;
+ IsBot = c.Bot != null;
+ }
+ }
+
public class GameServer
{
- public readonly int Id = 0;
+ static readonly string[] SerializeFields = { "Name", "Address", "Mod", "Version", "Map", "State", "MaxPlayers", "Protected" };
+ public const int ProtocolVersion = 2;
+
+ /// Online game number or -1 for LAN games
+ public readonly int Id = -1;
+
+ /// Name of the server
public readonly string Name = null;
+
+ /// ip:port string to connect to.
public readonly string Address = null;
+
+ /// Port of the server
+ public readonly int Port = 0;
+
+ /// The current state of the server (waiting/playing/completed)
public readonly int State = 0;
- public readonly int Players = 0;
+
+ /// The number of slots available for non-bot players
public readonly int MaxPlayers = 0;
- public readonly int Bots = 0;
- public readonly int Spectators = 0;
+
+ /// UID of the map
public readonly string Map = null;
- public readonly string Mods = "";
- public readonly int TTL = 0;
+
+ /// Mod ID
+ public readonly string Mod = "";
+
+ /// Mod Version
+ public readonly string Version = "";
+
+ /// Password protected
public readonly bool Protected = false;
+
+ /// UTC datetime string when the game changed to the Playing state
public readonly string Started = null;
+ /// Number of non-spectator, non-bot players. Only defined if GameServer is parsed from yaml.
+ public readonly int Players = 0;
+
+ /// Number of bot players. Only defined if GameServer is parsed from yaml.
+ public readonly int Bots = 0;
+
+ /// Number of spectators. Only defined if GameServer is parsed from yaml.
+ public readonly int Spectators = 0;
+
+ /// Number of seconds that the game has been in the Playing state. Only defined if GameServer is parsed from yaml.
+ public readonly int PlayTime = -1;
+
+ /// Can we join this server (after switching mods if required)? Only defined if GameServer is parsed from yaml.
+ [FieldLoader.Ignore]
public readonly bool IsCompatible = false;
+
+ /// Can we join this server (after switching mods if required)? Only defined if GameServer is parsed from yaml.
+ [FieldLoader.Ignore]
public readonly bool IsJoinable = false;
+ /// Label to display in the multiplayer browser. Only defined if GameServer is parsed from yaml.
+ [FieldLoader.Ignore]
public readonly string ModLabel = "";
- public readonly string ModId = "";
- public readonly string ModVersion = "";
+
+ [FieldLoader.LoadUsing("LoadClients")]
+ public readonly GameClient[] Clients;
+
+ static object LoadClients(MiniYaml yaml)
+ {
+ var clients = new List();
+ var clientsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Clients");
+ if (clientsNode != null)
+ {
+ var regex = new Regex(@"Client@\d+");
+ foreach (var client in clientsNode.Value.Nodes)
+ if (regex.IsMatch(client.Key))
+ clients.Add(FieldLoader.Load(client.Value));
+ }
+
+ return clients.ToArray();
+ }
public GameServer(MiniYaml yaml)
{
FieldLoader.Load(this, yaml);
- Manifest mod;
- ExternalMod external;
- var modVersion = Mods.Split('@');
-
- ModLabel = "Unknown mod: {0}".F(Mods);
- if (modVersion.Length == 2)
+ // Games advertised using the old API used a single Mods field
+ if (Mod == null || Version == null)
{
- ModId = modVersion[0];
- ModVersion = modVersion[1];
-
- var externalKey = ExternalMod.MakeKey(modVersion[0], modVersion[1]);
- if (Game.ExternalMods.TryGetValue(externalKey, out external)
- && external.Version == modVersion[1])
+ var modsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Mods");
+ if (modsNode != null)
{
- ModLabel = "{0} ({1})".F(external.Title, external.Version);
- IsCompatible = true;
- }
- else if (Game.Mods.TryGetValue(modVersion[0], out mod))
- {
- // Use internal mod data to populate the section header, but
- // on-connect switching must use the external mod plumbing.
- ModLabel = "{0} ({1})".F(mod.Metadata.Title, modVersion[1]);
+ var modVersion = modsNode.Value.Value.Split('@');
+ Mod = modVersion[0];
+ Version = modVersion[1];
}
}
+ // Games advertised using the old API calculated the play time locally
+ if (State == 2 && PlayTime < 0)
+ {
+ DateTime startTime;
+ if (DateTime.TryParse(Started, out startTime))
+ PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
+ }
+
+ Manifest mod;
+ ExternalMod external;
+
+ var externalKey = ExternalMod.MakeKey(Mod, Version);
+ if (Game.ExternalMods.TryGetValue(externalKey, out external) && external.Version == Version)
+ {
+ ModLabel = "{0} ({1})".F(external.Title, external.Version);
+ IsCompatible = true;
+ }
+ else if (Game.Mods.TryGetValue(Mod, out mod))
+ {
+ // Use internal mod data to populate the section header, but
+ // on-connect switching must use the external mod plumbing.
+ ModLabel = "{0} ({1})".F(mod.Metadata.Title, Version);
+ }
+ else
+ ModLabel = "Unknown mod: {0}".F(Mod);
+
var mapAvailable = Game.Settings.Game.AllowDownloading || Game.ModData.MapCache[Map].Status == MapStatus.Available;
IsJoinable = IsCompatible && State == 1 && mapAvailable;
}
+
+ public GameServer(Server.Server server)
+ {
+ Name = server.Settings.Name;
+
+ // IP address will be replaced with a real value by the master server / receiving LAN client
+ Address = "0.0.0.0:" + server.Settings.ListenPort.ToString();
+ State = (int)server.State;
+ MaxPlayers = server.LobbyInfo.Slots.Count(s => !s.Value.Closed) - server.LobbyInfo.Clients.Count(c1 => c1.Bot != null);
+ Map = server.Map.Uid;
+ Mod = server.ModData.Manifest.Id;
+ Version = server.ModData.Manifest.Metadata.Version;
+ Protected = !string.IsNullOrEmpty(server.Settings.Password);
+ Clients = server.LobbyInfo.Clients.Select(c => new GameClient(c)).ToArray();
+ }
+
+ public string ToPOSTData(bool lanGame)
+ {
+ var root = new List() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
+ foreach (var field in SerializeFields)
+ root.Add(FieldSaver.SaveField(this, field));
+
+ if (lanGame)
+ {
+ // Add fields that are normally generated by the master server
+ // LAN games overload the Id with a GUID string (rather than an ID) to allow deduplication
+ root.Add(new MiniYamlNode("Id", Platform.SessionGUID.ToString()));
+ root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToString()));
+ root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToString()));
+ root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToString()));
+
+ // Included for backwards compatibility with older clients that don't support separated Mod/Version.
+ root.Add(new MiniYamlNode("Mods", Mod + "@" + Version));
+ }
+
+ var clientsNode = new MiniYaml("");
+ var i = 0;
+ foreach (var c in Clients)
+ clientsNode.Nodes.Add(new MiniYamlNode("Client@" + (i++).ToString(), FieldSaver.Save(c)));
+
+ root.Add(new MiniYamlNode("Clients", clientsNode));
+ return new MiniYaml("", root)
+ .ToLines("Game")
+ .JoinWith("\n");
+ }
}
}
diff --git a/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs b/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs
index 81d72c68cf..f498d11395 100644
--- a/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs
+++ b/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs
@@ -16,6 +16,7 @@ using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using BeaconLib;
+using OpenRA.Network;
using OpenRA.Server;
using S = OpenRA.Server.Server;
@@ -88,27 +89,18 @@ namespace OpenRA.Mods.Common.Server
void PublishGame(S server)
{
- var mod = server.ModData.Manifest;
+ // Cache the server info on the main thread to ensure data consistency
+ var gs = new GameServer(server);
- // important to grab these on the main server thread, not in the worker we're about to spawn -- they may be modified
- // by the main thread as clients join and leave.
- var numPlayers = server.LobbyInfo.Clients.Where(c1 => c1.Bot == null && c1.Slot != null).Count();
- var numBots = server.LobbyInfo.Clients.Where(c1 => c1.Bot != null).Count();
- var numSpectators = server.LobbyInfo.Clients.Where(c1 => c1.Bot == null && c1.Slot == null).Count();
- var numSlots = server.LobbyInfo.Slots.Where(s => !s.Value.Closed).Count() - numBots;
- var passwordProtected = !string.IsNullOrEmpty(server.Settings.Password);
- var clients = server.LobbyInfo.Clients.Where(c1 => c1.Bot == null).Select(c => Convert.ToBase64String(Encoding.UTF8.GetBytes(c.Name))).ToArray();
+ if (!isBusy && server.Settings.AdvertiseOnline)
+ UpdateMasterServer(server, gs.ToPOSTData(false));
- UpdateMasterServer(server, numPlayers, numSlots, numBots, numSpectators, mod, passwordProtected, clients);
if (LanGameBeacon != null)
- UpdateLANGameBeacon(server, numPlayers, numSlots, numBots, numSpectators, mod, passwordProtected);
+ LanGameBeacon.BeaconData = gs.ToPOSTData(true);
}
- void UpdateMasterServer(S server, int numPlayers, int numSlots, int numBots, int numSpectators, Manifest mod, bool passwordProtected, string[] clients)
+ void UpdateMasterServer(S server, string postData)
{
- if (isBusy || !server.Settings.AdvertiseOnline)
- return;
-
lastPing = Game.RunTime;
isBusy = true;
@@ -116,31 +108,15 @@ namespace OpenRA.Mods.Common.Server
{
try
{
- var url = "ping?port={0}&name={1}&state={2}&players={3}&bots={4}&mods={5}&map={6}&maxplayers={7}&spectators={8}&protected={9}&clients={10}";
- if (isInitialPing) url += "&new=1";
-
var serverList = server.ModData.Manifest.Get().ServerList;
using (var wc = new WebClient())
{
wc.Proxy = null;
- var masterResponse = wc.DownloadData(
- serverList + url.F(
- server.Settings.ExternalPort, Uri.EscapeUriString(server.Settings.Name),
- (int)server.State,
- numPlayers,
- numBots,
- "{0}@{1}".F(mod.Id, mod.Metadata.Version),
- server.LobbyInfo.GlobalSettings.Map,
- numSlots,
- numSpectators,
- passwordProtected ? 1 : 0,
- string.Join(",", clients)));
+ var masterResponseText = wc.UploadString(serverList + "ping", postData);
if (isInitialPing)
{
- var masterResponseText = Encoding.UTF8.GetString(masterResponse);
Log.Write("server", "Master server: " + masterResponseText);
-
var errorCode = 0;
var errorMessage = string.Empty;
@@ -182,28 +158,5 @@ namespace OpenRA.Mods.Common.Server
a.BeginInvoke(null, null);
}
-
- void UpdateLANGameBeacon(S server, int numPlayers, int numSlots, int numBots, int numSpectators, Manifest mod, bool passwordProtected)
- {
- var settings = server.Settings;
-
- // TODO: Serialize and send client names
- var lanGameYaml =
-@"Game:
- Id: {0}
- Name: {1}
- Address: {2}:{3}
- State: {4}
- Players: {5}
- MaxPlayers: {6}
- Bots: {7}
- Spectators: {8}
- Map: {9}
- Mods: {10}@{11}
- Protected: {12}".F(Platform.SessionGUID, settings.Name, server.Ip, settings.ListenPort, (int)server.State, numPlayers, numSlots, numBots, numSpectators,
- server.Map.Uid, mod.Id, mod.Metadata.Version, passwordProtected);
-
- LanGameBeacon.BeaconData = lanGameYaml;
- }
}
}
diff --git a/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs
index 1afe6db4d4..fd830e581b 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs
@@ -334,7 +334,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
Game.RunAfterTick(() => RefreshServerListInner(games));
};
- var queryURL = services.ServerList + "games?version={0}&mod={1}&modversion={2}".F(
+ var queryURL = services.ServerList + "games?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));
@@ -346,11 +347,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
// Games that we can't join are sorted last
if (!testEntry.IsCompatible)
- return testEntry.ModId == modData.Manifest.Id ? 1 : 0;
+ return testEntry.Mod == modData.Manifest.Id ? 1 : 0;
// Games for the current mod+version are sorted first
- if (testEntry.ModId == modData.Manifest.Id)
- return testEntry.ModVersion == modData.Manifest.Metadata.Version ? 4 : 3;
+ 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;
@@ -405,7 +406,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
nextServerRow = null;
var rows = new List();
- var mods = games.GroupBy(g => g.Mods)
+ var mods = games.GroupBy(g => g.ModLabel)
.OrderByDescending(g => GroupSortOrder(g.First()))
.ThenByDescending(g => g.Count());
@@ -553,10 +554,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
var label = "In progress";
- DateTime startTime;
- if (DateTime.TryParse(game.Started, out startTime))
+ if (game.PlayTime > 0)
{
- var totalMinutes = Math.Ceiling((DateTime.UtcNow - startTime).TotalMinutes);
+ var totalMinutes = Math.Ceiling(game.PlayTime / 60.0);
label += " for {0} minute{1}".F(totalMinutes, totalMinutes > 1 ? "s" : "");
}