Lock Server.LobbyInfo to prevent races with callback threads.
This commit is contained in:
@@ -431,69 +431,72 @@ namespace OpenRA.Server
|
|||||||
|
|
||||||
Action completeConnection = () =>
|
Action completeConnection = () =>
|
||||||
{
|
{
|
||||||
client.Slot = LobbyInfo.FirstEmptySlot();
|
lock (LobbyInfo)
|
||||||
client.IsAdmin = !LobbyInfo.Clients.Any(c1 => c1.IsAdmin);
|
|
||||||
|
|
||||||
if (client.IsObserver && !LobbyInfo.GlobalSettings.AllowSpectators)
|
|
||||||
{
|
{
|
||||||
SendOrderTo(newConn, "ServerError", "The game is full");
|
client.Slot = LobbyInfo.FirstEmptySlot();
|
||||||
DropClient(newConn);
|
client.IsAdmin = !LobbyInfo.Clients.Any(c1 => c1.IsAdmin);
|
||||||
return;
|
|
||||||
|
if (client.IsObserver && !LobbyInfo.GlobalSettings.AllowSpectators)
|
||||||
|
{
|
||||||
|
SendOrderTo(newConn, "ServerError", "The game is full");
|
||||||
|
DropClient(newConn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.Slot != null)
|
||||||
|
SyncClientToPlayerReference(client, Map.Players.Players[client.Slot]);
|
||||||
|
else
|
||||||
|
client.Color = Color.White;
|
||||||
|
|
||||||
|
// Promote connection to a valid client
|
||||||
|
PreConns.Remove(newConn);
|
||||||
|
Conns.Add(newConn);
|
||||||
|
LobbyInfo.Clients.Add(client);
|
||||||
|
newConn.Validated = true;
|
||||||
|
|
||||||
|
var clientPing = new Session.ClientPing { Index = client.Index };
|
||||||
|
LobbyInfo.ClientPings.Add(clientPing);
|
||||||
|
|
||||||
|
Log.Write("server", "Client {0}: Accepted connection from {1}.",
|
||||||
|
newConn.PlayerIndex, newConn.Socket.RemoteEndPoint);
|
||||||
|
|
||||||
|
if (client.Fingerprint != null)
|
||||||
|
Log.Write("server", "Client {0}: Player fingerprint is {1}.",
|
||||||
|
newConn.PlayerIndex, client.Fingerprint);
|
||||||
|
|
||||||
|
foreach (var t in serverTraits.WithInterface<IClientJoined>())
|
||||||
|
t.ClientJoined(this, newConn);
|
||||||
|
|
||||||
|
SyncLobbyInfo();
|
||||||
|
|
||||||
|
Log.Write("server", "{0} ({1}) has joined the game.",
|
||||||
|
client.Name, newConn.Socket.RemoteEndPoint);
|
||||||
|
|
||||||
|
// Report to all other players
|
||||||
|
SendMessage("{0} has joined the game.".F(client.Name), newConn);
|
||||||
|
|
||||||
|
// Send initial ping
|
||||||
|
SendOrderTo(newConn, "Ping", Game.RunTime.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
if (Type == ServerType.Dedicated)
|
||||||
|
{
|
||||||
|
var motdFile = Platform.ResolvePath(Platform.SupportDirPrefix, "motd.txt");
|
||||||
|
if (!File.Exists(motdFile))
|
||||||
|
File.WriteAllText(motdFile, "Welcome, have fun and good luck!");
|
||||||
|
|
||||||
|
var motd = File.ReadAllText(motdFile);
|
||||||
|
if (!string.IsNullOrEmpty(motd))
|
||||||
|
SendOrderTo(newConn, "Message", motd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Map.DefinesUnsafeCustomRules)
|
||||||
|
SendOrderTo(newConn, "Message", "This map contains custom rules. Game experience may change.");
|
||||||
|
|
||||||
|
if (!LobbyInfo.GlobalSettings.EnableSingleplayer)
|
||||||
|
SendOrderTo(newConn, "Message", TwoHumansRequiredText);
|
||||||
|
else if (Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
|
||||||
|
SendOrderTo(newConn, "Message", "Bots have been disabled on this map.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.Slot != null)
|
|
||||||
SyncClientToPlayerReference(client, Map.Players.Players[client.Slot]);
|
|
||||||
else
|
|
||||||
client.Color = Color.White;
|
|
||||||
|
|
||||||
// Promote connection to a valid client
|
|
||||||
PreConns.Remove(newConn);
|
|
||||||
Conns.Add(newConn);
|
|
||||||
LobbyInfo.Clients.Add(client);
|
|
||||||
newConn.Validated = true;
|
|
||||||
|
|
||||||
var clientPing = new Session.ClientPing { Index = client.Index };
|
|
||||||
LobbyInfo.ClientPings.Add(clientPing);
|
|
||||||
|
|
||||||
Log.Write("server", "Client {0}: Accepted connection from {1}.",
|
|
||||||
newConn.PlayerIndex, newConn.Socket.RemoteEndPoint);
|
|
||||||
|
|
||||||
if (client.Fingerprint != null)
|
|
||||||
Log.Write("server", "Client {0}: Player fingerprint is {1}.",
|
|
||||||
newConn.PlayerIndex, client.Fingerprint);
|
|
||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<IClientJoined>())
|
|
||||||
t.ClientJoined(this, newConn);
|
|
||||||
|
|
||||||
SyncLobbyInfo();
|
|
||||||
|
|
||||||
Log.Write("server", "{0} ({1}) has joined the game.",
|
|
||||||
client.Name, newConn.Socket.RemoteEndPoint);
|
|
||||||
|
|
||||||
// Report to all other players
|
|
||||||
SendMessage("{0} has joined the game.".F(client.Name), newConn);
|
|
||||||
|
|
||||||
// Send initial ping
|
|
||||||
SendOrderTo(newConn, "Ping", Game.RunTime.ToString(CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
if (Type == ServerType.Dedicated)
|
|
||||||
{
|
|
||||||
var motdFile = Platform.ResolvePath(Platform.SupportDirPrefix, "motd.txt");
|
|
||||||
if (!File.Exists(motdFile))
|
|
||||||
File.WriteAllText(motdFile, "Welcome, have fun and good luck!");
|
|
||||||
|
|
||||||
var motd = File.ReadAllText(motdFile);
|
|
||||||
if (!string.IsNullOrEmpty(motd))
|
|
||||||
SendOrderTo(newConn, "Message", motd);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Map.DefinesUnsafeCustomRules)
|
|
||||||
SendOrderTo(newConn, "Message", "This map contains custom rules. Game experience may change.");
|
|
||||||
|
|
||||||
if (!LobbyInfo.GlobalSettings.EnableSingleplayer)
|
|
||||||
SendOrderTo(newConn, "Message", TwoHumansRequiredText);
|
|
||||||
else if (Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
|
|
||||||
SendOrderTo(newConn, "Message", "Bots have been disabled on this map.");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Type == ServerType.Local)
|
if (Type == ServerType.Local)
|
||||||
@@ -676,89 +679,119 @@ namespace OpenRA.Server
|
|||||||
|
|
||||||
void InterpretServerOrder(Connection conn, Order o)
|
void InterpretServerOrder(Connection conn, Order o)
|
||||||
{
|
{
|
||||||
// Only accept handshake responses from unvalidated clients
|
lock (LobbyInfo)
|
||||||
// Anything else may be an attempt to exploit the server
|
|
||||||
if (!conn.Validated)
|
|
||||||
{
|
{
|
||||||
if (o.OrderString == "HandshakeResponse")
|
// Only accept handshake responses from unvalidated clients
|
||||||
ValidateClient(conn, o.TargetString);
|
// Anything else may be an attempt to exploit the server
|
||||||
else
|
if (!conn.Validated)
|
||||||
{
|
{
|
||||||
Log.Write("server", "Rejected connection from {0}; Order `{1}` is not a `HandshakeResponse`.",
|
if (o.OrderString == "HandshakeResponse")
|
||||||
conn.Socket.RemoteEndPoint, o.OrderString);
|
ValidateClient(conn, o.TargetString);
|
||||||
|
else
|
||||||
DropClient(conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (o.OrderString)
|
|
||||||
{
|
|
||||||
case "Command":
|
|
||||||
{
|
{
|
||||||
var handledBy = serverTraits.WithInterface<IInterpretCommand>()
|
Log.Write("server", "Rejected connection from {0}; Order `{1}` is not a `HandshakeResponse`.",
|
||||||
.FirstOrDefault(t => t.InterpretCommand(this, conn, GetClient(conn), o.TargetString));
|
conn.Socket.RemoteEndPoint, o.OrderString);
|
||||||
|
|
||||||
if (handledBy == null)
|
DropClient(conn);
|
||||||
{
|
|
||||||
Log.Write("server", "Unknown server command: {0}", o.TargetString);
|
|
||||||
SendOrderTo(conn, "Message", "Unknown server command: {0}".F(o.TargetString));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "Chat":
|
return;
|
||||||
DispatchOrdersToClients(conn, 0, o.Serialize());
|
}
|
||||||
break;
|
|
||||||
case "Pong":
|
switch (o.OrderString)
|
||||||
{
|
{
|
||||||
if (!OpenRA.Exts.TryParseInt64Invariant(o.TargetString, out var pingSent))
|
case "Command":
|
||||||
{
|
{
|
||||||
Log.Write("server", "Invalid order pong payload: {0}", o.TargetString);
|
var handledBy = serverTraits.WithInterface<IInterpretCommand>()
|
||||||
|
.FirstOrDefault(t => t.InterpretCommand(this, conn, GetClient(conn), o.TargetString));
|
||||||
|
|
||||||
|
if (handledBy == null)
|
||||||
|
{
|
||||||
|
Log.Write("server", "Unknown server command: {0}", o.TargetString);
|
||||||
|
SendOrderTo(conn, "Message", "Unknown server command: {0}".F(o.TargetString));
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = GetClient(conn);
|
case "Chat":
|
||||||
if (client == null)
|
DispatchOrdersToClients(conn, 0, o.Serialize());
|
||||||
return;
|
|
||||||
|
|
||||||
var pingFromClient = LobbyInfo.PingFromClient(client);
|
|
||||||
if (pingFromClient == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var history = pingFromClient.LatencyHistory.ToList();
|
|
||||||
history.Add(Game.RunTime - pingSent);
|
|
||||||
|
|
||||||
// Cap ping history at 5 values (25 seconds)
|
|
||||||
if (history.Count > 5)
|
|
||||||
history.RemoveRange(0, history.Count - 5);
|
|
||||||
|
|
||||||
pingFromClient.Latency = history.Sum() / history.Count;
|
|
||||||
pingFromClient.LatencyJitter = (history.Max() - history.Min()) / 2;
|
|
||||||
pingFromClient.LatencyHistory = history.ToArray();
|
|
||||||
|
|
||||||
SyncClientPing();
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
case "Pong":
|
||||||
|
|
||||||
case "GameSaveTraitData":
|
|
||||||
{
|
|
||||||
if (GameSave != null)
|
|
||||||
{
|
{
|
||||||
var data = MiniYaml.FromString(o.TargetString)[0];
|
if (!OpenRA.Exts.TryParseInt64Invariant(o.TargetString, out var pingSent))
|
||||||
GameSave.AddTraitData(int.Parse(data.Key), data.Value);
|
{
|
||||||
|
Log.Write("server", "Invalid order pong payload: {0}", o.TargetString);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = GetClient(conn);
|
||||||
|
if (client == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pingFromClient = LobbyInfo.PingFromClient(client);
|
||||||
|
if (pingFromClient == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var history = pingFromClient.LatencyHistory.ToList();
|
||||||
|
history.Add(Game.RunTime - pingSent);
|
||||||
|
|
||||||
|
// Cap ping history at 5 values (25 seconds)
|
||||||
|
if (history.Count > 5)
|
||||||
|
history.RemoveRange(0, history.Count - 5);
|
||||||
|
|
||||||
|
pingFromClient.Latency = history.Sum() / history.Count;
|
||||||
|
pingFromClient.LatencyJitter = (history.Max() - history.Min()) / 2;
|
||||||
|
pingFromClient.LatencyHistory = history.ToArray();
|
||||||
|
|
||||||
|
SyncClientPing();
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
case "GameSaveTraitData":
|
||||||
}
|
|
||||||
|
|
||||||
case "CreateGameSave":
|
|
||||||
{
|
|
||||||
if (GameSave != null)
|
|
||||||
{
|
{
|
||||||
|
if (GameSave != null)
|
||||||
|
{
|
||||||
|
var data = MiniYaml.FromString(o.TargetString)[0];
|
||||||
|
GameSave.AddTraitData(int.Parse(data.Key), data.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "CreateGameSave":
|
||||||
|
{
|
||||||
|
if (GameSave != null)
|
||||||
|
{
|
||||||
|
// Sanitize potentially malicious input
|
||||||
|
var filename = o.TargetString;
|
||||||
|
var invalidIndex = -1;
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
||||||
|
filename = filename.Remove(invalidIndex, 1);
|
||||||
|
|
||||||
|
var baseSavePath = Platform.ResolvePath(
|
||||||
|
Platform.SupportDirPrefix,
|
||||||
|
"Saves",
|
||||||
|
ModData.Manifest.Id,
|
||||||
|
ModData.Manifest.Metadata.Version);
|
||||||
|
|
||||||
|
if (!Directory.Exists(baseSavePath))
|
||||||
|
Directory.CreateDirectory(baseSavePath);
|
||||||
|
|
||||||
|
GameSave.Save(Path.Combine(baseSavePath, filename));
|
||||||
|
DispatchOrdersToClients(null, 0, Order.FromTargetString("GameSaved", filename, true).Serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "LoadGameSave":
|
||||||
|
{
|
||||||
|
if (Type == ServerType.Dedicated || State >= ServerState.GameStarted)
|
||||||
|
break;
|
||||||
|
|
||||||
// Sanitize potentially malicious input
|
// Sanitize potentially malicious input
|
||||||
var filename = o.TargetString;
|
var filename = o.TargetString;
|
||||||
var invalidIndex = -1;
|
var invalidIndex = -1;
|
||||||
@@ -766,96 +799,69 @@ namespace OpenRA.Server
|
|||||||
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
||||||
filename = filename.Remove(invalidIndex, 1);
|
filename = filename.Remove(invalidIndex, 1);
|
||||||
|
|
||||||
var baseSavePath = Platform.ResolvePath(
|
var savePath = Platform.ResolvePath(
|
||||||
Platform.SupportDirPrefix,
|
Platform.SupportDirPrefix,
|
||||||
"Saves",
|
"Saves",
|
||||||
ModData.Manifest.Id,
|
ModData.Manifest.Id,
|
||||||
ModData.Manifest.Metadata.Version);
|
ModData.Manifest.Metadata.Version,
|
||||||
|
filename);
|
||||||
|
|
||||||
if (!Directory.Exists(baseSavePath))
|
GameSave = new GameSave(savePath);
|
||||||
Directory.CreateDirectory(baseSavePath);
|
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
|
||||||
|
LobbyInfo.Slots = GameSave.Slots;
|
||||||
|
|
||||||
GameSave.Save(Path.Combine(baseSavePath, filename));
|
// Reassign clients to slots
|
||||||
DispatchOrdersToClients(null, 0, Order.FromTargetString("GameSaved", filename, true).Serialize());
|
// - Bot ordering is preserved
|
||||||
}
|
// - Humans are assigned on a first-come-first-serve basis
|
||||||
|
// - Leftover humans become spectators
|
||||||
|
|
||||||
break;
|
// Start by removing all bots and assigning all players as spectators
|
||||||
}
|
foreach (var c in LobbyInfo.Clients)
|
||||||
|
|
||||||
case "LoadGameSave":
|
|
||||||
{
|
|
||||||
if (Type == ServerType.Dedicated || State >= ServerState.GameStarted)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Sanitize potentially malicious input
|
|
||||||
var filename = o.TargetString;
|
|
||||||
var invalidIndex = -1;
|
|
||||||
var invalidChars = Path.GetInvalidFileNameChars();
|
|
||||||
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
|
||||||
filename = filename.Remove(invalidIndex, 1);
|
|
||||||
|
|
||||||
var savePath = Platform.ResolvePath(
|
|
||||||
Platform.SupportDirPrefix,
|
|
||||||
"Saves",
|
|
||||||
ModData.Manifest.Id,
|
|
||||||
ModData.Manifest.Metadata.Version,
|
|
||||||
filename);
|
|
||||||
|
|
||||||
GameSave = new GameSave(savePath);
|
|
||||||
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
|
|
||||||
LobbyInfo.Slots = GameSave.Slots;
|
|
||||||
|
|
||||||
// Reassign clients to slots
|
|
||||||
// - Bot ordering is preserved
|
|
||||||
// - Humans are assigned on a first-come-first-serve basis
|
|
||||||
// - Leftover humans become spectators
|
|
||||||
|
|
||||||
// Start by removing all bots and assigning all players as spectators
|
|
||||||
foreach (var c in LobbyInfo.Clients)
|
|
||||||
{
|
|
||||||
if (c.Bot != null)
|
|
||||||
{
|
{
|
||||||
LobbyInfo.Clients.Remove(c);
|
if (c.Bot != null)
|
||||||
var ping = LobbyInfo.PingFromClient(c);
|
|
||||||
if (ping != null)
|
|
||||||
LobbyInfo.ClientPings.Remove(ping);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
c.Slot = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild/remap the saved client state
|
|
||||||
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
|
|
||||||
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
|
|
||||||
foreach (var kv in GameSave.SlotClients)
|
|
||||||
{
|
|
||||||
if (kv.Value.Bot != null)
|
|
||||||
{
|
|
||||||
var bot = new Session.Client()
|
|
||||||
{
|
{
|
||||||
Index = ChooseFreePlayerIndex(),
|
LobbyInfo.Clients.Remove(c);
|
||||||
State = Session.ClientState.NotReady,
|
var ping = LobbyInfo.PingFromClient(c);
|
||||||
BotControllerClientIndex = adminClientIndex
|
if (ping != null)
|
||||||
};
|
LobbyInfo.ClientPings.Remove(ping);
|
||||||
|
}
|
||||||
kv.Value.ApplyTo(bot);
|
else
|
||||||
LobbyInfo.Clients.Add(bot);
|
c.Slot = null;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// Rebuild/remap the saved client state
|
||||||
|
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
|
||||||
|
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
|
||||||
|
foreach (var kv in GameSave.SlotClients)
|
||||||
{
|
{
|
||||||
// This will throw if the server doesn't have enough human clients to fill all player slots
|
if (kv.Value.Bot != null)
|
||||||
// See TODO above - this isn't a problem in practice because MP saves won't use this
|
{
|
||||||
var client = LobbyInfo.Clients.First(c => c.Slot == null);
|
var bot = new Session.Client()
|
||||||
kv.Value.ApplyTo(client);
|
{
|
||||||
|
Index = ChooseFreePlayerIndex(),
|
||||||
|
State = Session.ClientState.NotReady,
|
||||||
|
BotControllerClientIndex = adminClientIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
kv.Value.ApplyTo(bot);
|
||||||
|
LobbyInfo.Clients.Add(bot);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This will throw if the server doesn't have enough human clients to fill all player slots
|
||||||
|
// See TODO above - this isn't a problem in practice because MP saves won't use this
|
||||||
|
var client = LobbyInfo.Clients.First(c => c.Slot == null);
|
||||||
|
kv.Value.ApplyTo(client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncLobbyInfo();
|
||||||
|
SyncLobbyClients();
|
||||||
|
SyncClientPing();
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SyncLobbyInfo();
|
|
||||||
SyncLobbyClients();
|
|
||||||
SyncClientPing();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,54 +872,57 @@ namespace OpenRA.Server
|
|||||||
|
|
||||||
public void DropClient(Connection toDrop)
|
public void DropClient(Connection toDrop)
|
||||||
{
|
{
|
||||||
if (!PreConns.Remove(toDrop))
|
lock (LobbyInfo)
|
||||||
{
|
{
|
||||||
Conns.Remove(toDrop);
|
if (!PreConns.Remove(toDrop))
|
||||||
|
|
||||||
var dropClient = LobbyInfo.Clients.FirstOrDefault(c1 => c1.Index == toDrop.PlayerIndex);
|
|
||||||
if (dropClient == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var suffix = "";
|
|
||||||
if (State == ServerState.GameStarted)
|
|
||||||
suffix = dropClient.IsObserver ? " (Spectator)" : dropClient.Team != 0 ? " (Team {0})".F(dropClient.Team) : "";
|
|
||||||
SendMessage("{0}{1} has disconnected.".F(dropClient.Name, suffix));
|
|
||||||
|
|
||||||
// Send disconnected order, even if still in the lobby
|
|
||||||
DispatchOrdersToClients(toDrop, 0, Order.FromTargetString("Disconnected", "", true).Serialize());
|
|
||||||
|
|
||||||
LobbyInfo.Clients.RemoveAll(c => c.Index == toDrop.PlayerIndex);
|
|
||||||
LobbyInfo.ClientPings.RemoveAll(p => p.Index == toDrop.PlayerIndex);
|
|
||||||
|
|
||||||
// Client was the server admin
|
|
||||||
// TODO: Reassign admin for game in progress via an order
|
|
||||||
if (Type == ServerType.Dedicated && dropClient.IsAdmin && State == ServerState.WaitingPlayers)
|
|
||||||
{
|
{
|
||||||
// Remove any bots controlled by the admin
|
Conns.Remove(toDrop);
|
||||||
LobbyInfo.Clients.RemoveAll(c => c.Bot != null && c.BotControllerClientIndex == toDrop.PlayerIndex);
|
|
||||||
|
|
||||||
var nextAdmin = LobbyInfo.Clients.Where(c1 => c1.Bot == null)
|
var dropClient = LobbyInfo.Clients.FirstOrDefault(c1 => c1.Index == toDrop.PlayerIndex);
|
||||||
.MinByOrDefault(c => c.Index);
|
if (dropClient == null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (nextAdmin != null)
|
var suffix = "";
|
||||||
|
if (State == ServerState.GameStarted)
|
||||||
|
suffix = dropClient.IsObserver ? " (Spectator)" : dropClient.Team != 0 ? " (Team {0})".F(dropClient.Team) : "";
|
||||||
|
SendMessage("{0}{1} has disconnected.".F(dropClient.Name, suffix));
|
||||||
|
|
||||||
|
// Send disconnected order, even if still in the lobby
|
||||||
|
DispatchOrdersToClients(toDrop, 0, Order.FromTargetString("Disconnected", "", true).Serialize());
|
||||||
|
|
||||||
|
LobbyInfo.Clients.RemoveAll(c => c.Index == toDrop.PlayerIndex);
|
||||||
|
LobbyInfo.ClientPings.RemoveAll(p => p.Index == toDrop.PlayerIndex);
|
||||||
|
|
||||||
|
// Client was the server admin
|
||||||
|
// TODO: Reassign admin for game in progress via an order
|
||||||
|
if (Type == ServerType.Dedicated && dropClient.IsAdmin && State == ServerState.WaitingPlayers)
|
||||||
{
|
{
|
||||||
nextAdmin.IsAdmin = true;
|
// Remove any bots controlled by the admin
|
||||||
SendMessage("{0} is now the admin.".F(nextAdmin.Name));
|
LobbyInfo.Clients.RemoveAll(c => c.Bot != null && c.BotControllerClientIndex == toDrop.PlayerIndex);
|
||||||
|
|
||||||
|
var nextAdmin = LobbyInfo.Clients.Where(c1 => c1.Bot == null)
|
||||||
|
.MinByOrDefault(c => c.Index);
|
||||||
|
|
||||||
|
if (nextAdmin != null)
|
||||||
|
{
|
||||||
|
nextAdmin.IsAdmin = true;
|
||||||
|
SendMessage("{0} is now the admin.".F(nextAdmin.Name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DispatchOrders(toDrop, toDrop.MostRecentFrame, new[] { (byte)OrderType.Disconnect });
|
||||||
|
|
||||||
|
// All clients have left: clean up
|
||||||
|
if (!Conns.Any())
|
||||||
|
foreach (var t in serverTraits.WithInterface<INotifyServerEmpty>())
|
||||||
|
t.ServerEmpty(this);
|
||||||
|
|
||||||
|
if (Conns.Any() || Type == ServerType.Dedicated)
|
||||||
|
SyncLobbyClients();
|
||||||
|
|
||||||
|
if (Type != ServerType.Dedicated && dropClient.IsAdmin)
|
||||||
|
Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchOrders(toDrop, toDrop.MostRecentFrame, new[] { (byte)OrderType.Disconnect });
|
|
||||||
|
|
||||||
// All clients have left: clean up
|
|
||||||
if (!Conns.Any())
|
|
||||||
foreach (var t in serverTraits.WithInterface<INotifyServerEmpty>())
|
|
||||||
t.ServerEmpty(this);
|
|
||||||
|
|
||||||
if (Conns.Any() || Type == ServerType.Dedicated)
|
|
||||||
SyncLobbyClients();
|
|
||||||
|
|
||||||
if (Type != ServerType.Dedicated && dropClient.IsAdmin)
|
|
||||||
Shutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -925,11 +934,14 @@ namespace OpenRA.Server
|
|||||||
|
|
||||||
public void SyncLobbyInfo()
|
public void SyncLobbyInfo()
|
||||||
{
|
{
|
||||||
if (State == ServerState.WaitingPlayers) // Don't do this while the game is running, it breaks things!
|
lock (LobbyInfo)
|
||||||
DispatchOrders(null, 0, Order.FromTargetString("SyncInfo", LobbyInfo.Serialize(), true).Serialize());
|
{
|
||||||
|
if (State == ServerState.WaitingPlayers) // Don't do this while the game is running, it breaks things!
|
||||||
|
DispatchOrders(null, 0, Order.FromTargetString("SyncInfo", LobbyInfo.Serialize(), true).Serialize());
|
||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
||||||
t.LobbyInfoSynced(this);
|
t.LobbyInfoSynced(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncLobbyClients()
|
public void SyncLobbyClients()
|
||||||
@@ -937,13 +949,16 @@ namespace OpenRA.Server
|
|||||||
if (State != ServerState.WaitingPlayers)
|
if (State != ServerState.WaitingPlayers)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// TODO: Only need to sync the specific client that has changed to avoid conflicts!
|
lock (LobbyInfo)
|
||||||
var clientData = LobbyInfo.Clients.Select(client => client.Serialize()).ToList();
|
{
|
||||||
|
// TODO: Only need to sync the specific client that has changed to avoid conflicts!
|
||||||
|
var clientData = LobbyInfo.Clients.Select(client => client.Serialize()).ToList();
|
||||||
|
|
||||||
DispatchOrders(null, 0, Order.FromTargetString("SyncLobbyClients", clientData.WriteToString(), true).Serialize());
|
DispatchOrders(null, 0, Order.FromTargetString("SyncLobbyClients", clientData.WriteToString(), true).Serialize());
|
||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
||||||
t.LobbyInfoSynced(this);
|
t.LobbyInfoSynced(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncLobbySlots()
|
public void SyncLobbySlots()
|
||||||
@@ -951,13 +966,16 @@ namespace OpenRA.Server
|
|||||||
if (State != ServerState.WaitingPlayers)
|
if (State != ServerState.WaitingPlayers)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// TODO: Don't sync all the slots if just one changed!
|
lock (LobbyInfo)
|
||||||
var slotData = LobbyInfo.Slots.Select(slot => slot.Value.Serialize()).ToList();
|
{
|
||||||
|
// TODO: Don't sync all the slots if just one changed!
|
||||||
|
var slotData = LobbyInfo.Slots.Select(slot => slot.Value.Serialize()).ToList();
|
||||||
|
|
||||||
DispatchOrders(null, 0, Order.FromTargetString("SyncLobbySlots", slotData.WriteToString(), true).Serialize());
|
DispatchOrders(null, 0, Order.FromTargetString("SyncLobbySlots", slotData.WriteToString(), true).Serialize());
|
||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
||||||
t.LobbyInfoSynced(this);
|
t.LobbyInfoSynced(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncLobbyGlobalSettings()
|
public void SyncLobbyGlobalSettings()
|
||||||
@@ -965,86 +983,95 @@ namespace OpenRA.Server
|
|||||||
if (State != ServerState.WaitingPlayers)
|
if (State != ServerState.WaitingPlayers)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var sessionData = new List<MiniYamlNode> { LobbyInfo.GlobalSettings.Serialize() };
|
lock (LobbyInfo)
|
||||||
|
{
|
||||||
|
var sessionData = new List<MiniYamlNode> { LobbyInfo.GlobalSettings.Serialize() };
|
||||||
|
|
||||||
DispatchOrders(null, 0, Order.FromTargetString("SyncLobbyGlobalSettings", sessionData.WriteToString(), true).Serialize());
|
DispatchOrders(null, 0, Order.FromTargetString("SyncLobbyGlobalSettings", sessionData.WriteToString(), true).Serialize());
|
||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
foreach (var t in serverTraits.WithInterface<INotifySyncLobbyInfo>())
|
||||||
t.LobbyInfoSynced(this);
|
t.LobbyInfoSynced(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncClientPing()
|
public void SyncClientPing()
|
||||||
{
|
{
|
||||||
// TODO: Split this further into per client ping orders
|
lock (LobbyInfo)
|
||||||
var clientPings = LobbyInfo.ClientPings.Select(ping => ping.Serialize()).ToList();
|
{
|
||||||
|
// TODO: Split this further into per client ping orders
|
||||||
|
var clientPings = LobbyInfo.ClientPings.Select(ping => ping.Serialize()).ToList();
|
||||||
|
|
||||||
// Note that syncing pings doesn't trigger INotifySyncLobbyInfo
|
// Note that syncing pings doesn't trigger INotifySyncLobbyInfo
|
||||||
DispatchOrders(null, 0, Order.FromTargetString("SyncClientPings", clientPings.WriteToString(), true).Serialize());
|
DispatchOrders(null, 0, Order.FromTargetString("SyncClientPings", clientPings.WriteToString(), true).Serialize());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartGame()
|
public void StartGame()
|
||||||
{
|
{
|
||||||
foreach (var listener in listeners)
|
lock (LobbyInfo)
|
||||||
listener.Stop();
|
|
||||||
|
|
||||||
Console.WriteLine("[{0}] Game started", DateTime.Now.ToString(Settings.TimestampFormat));
|
|
||||||
|
|
||||||
// Drop any unvalidated clients
|
|
||||||
foreach (var c in PreConns.ToArray())
|
|
||||||
DropClient(c);
|
|
||||||
|
|
||||||
// Drop any players who are not ready
|
|
||||||
foreach (var c in Conns.Where(c => GetClient(c).IsInvalid).ToArray())
|
|
||||||
{
|
{
|
||||||
SendOrderTo(c, "ServerError", "You have been kicked from the server!");
|
foreach (var listener in listeners)
|
||||||
DropClient(c);
|
listener.Stop();
|
||||||
}
|
|
||||||
|
|
||||||
// HACK: Turn down the latency if there is only one real player
|
Console.WriteLine("[{0}] Game started", DateTime.Now.ToString(Settings.TimestampFormat));
|
||||||
if (LobbyInfo.NonBotClients.Count() == 1)
|
|
||||||
LobbyInfo.GlobalSettings.OrderLatency = 1;
|
|
||||||
|
|
||||||
// Enable game saves for singleplayer missions only
|
// Drop any unvalidated clients
|
||||||
// TODO: Enable for multiplayer (non-dedicated servers only) once the lobby UI has been created
|
foreach (var c in PreConns.ToArray())
|
||||||
LobbyInfo.GlobalSettings.GameSavesEnabled = Type != ServerType.Dedicated && LobbyInfo.NonBotClients.Count() == 1;
|
DropClient(c);
|
||||||
|
|
||||||
SyncLobbyInfo();
|
// Drop any players who are not ready
|
||||||
State = ServerState.GameStarted;
|
foreach (var c in Conns.Where(c => GetClient(c).IsInvalid).ToArray())
|
||||||
|
|
||||||
foreach (var c in Conns)
|
|
||||||
foreach (var d in Conns)
|
|
||||||
DispatchOrdersToClient(c, d.PlayerIndex, int.MaxValue, new[] { (byte)OrderType.Disconnect });
|
|
||||||
|
|
||||||
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
|
|
||||||
GameSave = new GameSave();
|
|
||||||
|
|
||||||
var startGameData = "";
|
|
||||||
if (GameSave != null)
|
|
||||||
{
|
|
||||||
GameSave.StartGame(LobbyInfo, Map);
|
|
||||||
if (GameSave.LastOrdersFrame >= 0)
|
|
||||||
{
|
{
|
||||||
startGameData = new List<MiniYamlNode>()
|
SendOrderTo(c, "ServerError", "You have been kicked from the server!");
|
||||||
{
|
DropClient(c);
|
||||||
new MiniYamlNode("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToString()),
|
|
||||||
new MiniYamlNode("SaveSyncFrame", GameSave.LastSyncFrame.ToString())
|
|
||||||
}.WriteToString();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DispatchOrders(null, 0,
|
// HACK: Turn down the latency if there is only one real player
|
||||||
Order.FromTargetString("StartGame", startGameData, true).Serialize());
|
if (LobbyInfo.NonBotClients.Count() == 1)
|
||||||
|
LobbyInfo.GlobalSettings.OrderLatency = 1;
|
||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<IStartGame>())
|
// Enable game saves for singleplayer missions only
|
||||||
t.GameStarted(this);
|
// TODO: Enable for multiplayer (non-dedicated servers only) once the lobby UI has been created
|
||||||
|
LobbyInfo.GlobalSettings.GameSavesEnabled = Type != ServerType.Dedicated && LobbyInfo.NonBotClients.Count() == 1;
|
||||||
|
|
||||||
if (GameSave != null && GameSave.LastOrdersFrame >= 0)
|
SyncLobbyInfo();
|
||||||
{
|
State = ServerState.GameStarted;
|
||||||
GameSave.ParseOrders(LobbyInfo, (frame, client, data) =>
|
|
||||||
|
foreach (var c in Conns)
|
||||||
|
foreach (var d in Conns)
|
||||||
|
DispatchOrdersToClient(c, d.PlayerIndex, int.MaxValue, new[] { (byte)OrderType.Disconnect });
|
||||||
|
|
||||||
|
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
|
||||||
|
GameSave = new GameSave();
|
||||||
|
|
||||||
|
var startGameData = "";
|
||||||
|
if (GameSave != null)
|
||||||
{
|
{
|
||||||
foreach (var c in Conns)
|
GameSave.StartGame(LobbyInfo, Map);
|
||||||
DispatchOrdersToClient(c, client, frame, data);
|
if (GameSave.LastOrdersFrame >= 0)
|
||||||
});
|
{
|
||||||
|
startGameData = new List<MiniYamlNode>()
|
||||||
|
{
|
||||||
|
new MiniYamlNode("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToString()),
|
||||||
|
new MiniYamlNode("SaveSyncFrame", GameSave.LastSyncFrame.ToString())
|
||||||
|
}.WriteToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchOrders(null, 0,
|
||||||
|
Order.FromTargetString("StartGame", startGameData, true).Serialize());
|
||||||
|
|
||||||
|
foreach (var t in serverTraits.WithInterface<IStartGame>())
|
||||||
|
t.GameStarted(this);
|
||||||
|
|
||||||
|
if (GameSave != null && GameSave.LastOrdersFrame >= 0)
|
||||||
|
{
|
||||||
|
GameSave.ParseOrders(LobbyInfo, (frame, client, data) =>
|
||||||
|
{
|
||||||
|
foreach (var c in Conns)
|
||||||
|
DispatchOrdersToClient(c, client, frame, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,25 +21,28 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
{
|
{
|
||||||
public void ClientJoined(OpenRA.Server.Server server, Connection conn)
|
public void ClientJoined(OpenRA.Server.Server server, Connection conn)
|
||||||
{
|
{
|
||||||
if (server.LobbyInfo.ClientWithIndex(conn.PlayerIndex).IsAdmin)
|
lock (server.LobbyInfo)
|
||||||
return;
|
|
||||||
|
|
||||||
var defaults = new Session.Global();
|
|
||||||
LobbyCommands.LoadMapSettings(server, defaults, server.Map.Rules);
|
|
||||||
|
|
||||||
var options = server.Map.Rules.Actors["player"].TraitInfos<ILobbyOptions>()
|
|
||||||
.Concat(server.Map.Rules.Actors["world"].TraitInfos<ILobbyOptions>())
|
|
||||||
.SelectMany(t => t.LobbyOptions(server.Map.Rules));
|
|
||||||
|
|
||||||
var optionNames = new Dictionary<string, string>();
|
|
||||||
foreach (var o in options)
|
|
||||||
optionNames[o.Id] = o.Name;
|
|
||||||
|
|
||||||
foreach (var kv in server.LobbyInfo.GlobalSettings.LobbyOptions)
|
|
||||||
{
|
{
|
||||||
if (!defaults.LobbyOptions.TryGetValue(kv.Key, out var def) || kv.Value.Value != def.Value)
|
if (server.LobbyInfo.ClientWithIndex(conn.PlayerIndex).IsAdmin)
|
||||||
if (optionNames.TryGetValue(kv.Key, out var optionName))
|
return;
|
||||||
server.SendOrderTo(conn, "Message", optionName + ": " + kv.Value.Value);
|
|
||||||
|
var defaults = new Session.Global();
|
||||||
|
LobbyCommands.LoadMapSettings(server, defaults, server.Map.Rules);
|
||||||
|
|
||||||
|
var options = server.Map.Rules.Actors["player"].TraitInfos<ILobbyOptions>()
|
||||||
|
.Concat(server.Map.Rules.Actors["world"].TraitInfos<ILobbyOptions>())
|
||||||
|
.SelectMany(t => t.LobbyOptions(server.Map.Rules));
|
||||||
|
|
||||||
|
var optionNames = new Dictionary<string, string>();
|
||||||
|
foreach (var o in options)
|
||||||
|
optionNames[o.Id] = o.Name;
|
||||||
|
|
||||||
|
foreach (var kv in server.LobbyInfo.GlobalSettings.LobbyOptions)
|
||||||
|
{
|
||||||
|
if (!defaults.LobbyOptions.TryGetValue(kv.Key, out var def) || kv.Value.Value != def.Value)
|
||||||
|
if (optionNames.TryGetValue(kv.Key, out var optionName))
|
||||||
|
server.SendOrderTo(conn, "Message", optionName + ": " + kv.Value.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
lastPing = Game.RunTime;
|
lastPing = Game.RunTime;
|
||||||
|
|
||||||
// Ignore client timeout in singleplayer games to make debugging easier
|
// Ignore client timeout in singleplayer games to make debugging easier
|
||||||
if (server.LobbyInfo.NonBotClients.Count() < 2 && server.Type != ServerType.Dedicated)
|
var nonBotClientCount = 0;
|
||||||
|
lock (server.LobbyInfo)
|
||||||
|
nonBotClientCount = server.LobbyInfo.NonBotClients.Count();
|
||||||
|
|
||||||
|
if (nonBotClientCount < 2 && server.Type != ServerType.Dedicated)
|
||||||
foreach (var c in server.Conns.ToList())
|
foreach (var c in server.Conns.ToList())
|
||||||
server.SendOrderTo(c, "Ping", Game.RunTime.ToString());
|
server.SendOrderTo(c, "Ping", Game.RunTime.ToString());
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user