Implement new master server ping protocol.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>Online game number or -1 for LAN games</summary>
|
||||
public readonly int Id = -1;
|
||||
|
||||
/// <summary>Name of the server</summary>
|
||||
public readonly string Name = null;
|
||||
|
||||
/// <summary>ip:port string to connect to.</summary>
|
||||
public readonly string Address = null;
|
||||
|
||||
/// <summary>Port of the server</summary>
|
||||
public readonly int Port = 0;
|
||||
|
||||
/// <summary>The current state of the server (waiting/playing/completed)</summary>
|
||||
public readonly int State = 0;
|
||||
public readonly int Players = 0;
|
||||
|
||||
/// <summary>The number of slots available for non-bot players</summary>
|
||||
public readonly int MaxPlayers = 0;
|
||||
public readonly int Bots = 0;
|
||||
public readonly int Spectators = 0;
|
||||
|
||||
/// <summary>UID of the map</summary>
|
||||
public readonly string Map = null;
|
||||
public readonly string Mods = "";
|
||||
public readonly int TTL = 0;
|
||||
|
||||
/// <summary>Mod ID</summary>
|
||||
public readonly string Mod = "";
|
||||
|
||||
/// <summary>Mod Version</summary>
|
||||
public readonly string Version = "";
|
||||
|
||||
/// <summary>Password protected</summary>
|
||||
public readonly bool Protected = false;
|
||||
|
||||
/// <summary>UTC datetime string when the game changed to the Playing state</summary>
|
||||
public readonly string Started = null;
|
||||
|
||||
/// <summary>Number of non-spectator, non-bot players. Only defined if GameServer is parsed from yaml.</summary>
|
||||
public readonly int Players = 0;
|
||||
|
||||
/// <summary>Number of bot players. Only defined if GameServer is parsed from yaml.</summary>
|
||||
public readonly int Bots = 0;
|
||||
|
||||
/// <summary>Number of spectators. Only defined if GameServer is parsed from yaml.</summary>
|
||||
public readonly int Spectators = 0;
|
||||
|
||||
/// <summary>Number of seconds that the game has been in the Playing state. Only defined if GameServer is parsed from yaml.</summary>
|
||||
public readonly int PlayTime = -1;
|
||||
|
||||
/// <summary>Can we join this server (after switching mods if required)? Only defined if GameServer is parsed from yaml.</summary>
|
||||
[FieldLoader.Ignore]
|
||||
public readonly bool IsCompatible = false;
|
||||
|
||||
/// <summary>Can we join this server (after switching mods if required)? Only defined if GameServer is parsed from yaml.</summary>
|
||||
[FieldLoader.Ignore]
|
||||
public readonly bool IsJoinable = false;
|
||||
|
||||
/// <summary>Label to display in the multiplayer browser. Only defined if GameServer is parsed from yaml.</summary>
|
||||
[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<GameClient>();
|
||||
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<GameClient>(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<MiniYamlNode>() { 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WebServices>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Widget>();
|
||||
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" : "");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user