diff --git a/OpenRA.Game/Network/FrameData.cs b/OpenRA.Game/Network/FrameData.cs deleted file mode 100644 index d540ab78fe..0000000000 --- a/OpenRA.Game/Network/FrameData.cs +++ /dev/null @@ -1,81 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2020 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.Collections.Generic; -using System.Linq; - -namespace OpenRA.Network -{ - class FrameData - { - public struct ClientOrder - { - public int Client; - public Order Order; - - public override string ToString() - { - return $"ClientId: {Client} {Order}"; - } - } - - readonly Dictionary clientQuitTimes = new Dictionary(); - readonly Dictionary> framePackets = new Dictionary>(); - - public IEnumerable ClientsPlayingInFrame(int frame) - { - return clientQuitTimes - .Where(x => frame <= x.Value) - .Select(x => x.Key) - .OrderBy(x => x); - } - - public void ClientQuit(int clientId, int lastClientFrame) - { - if (lastClientFrame == -1) - lastClientFrame = framePackets - .Where(x => x.Value.ContainsKey(clientId)) - .Select(x => x.Key).MaxByOrDefault(x => x); - - clientQuitTimes[clientId] = lastClientFrame; - } - - public void AddFrameOrders(int clientId, int frame, byte[] orders) - { - var frameData = framePackets.GetOrAdd(frame); - frameData.Add(clientId, orders); - } - - public bool IsReadyForFrame(int frame) - { - return !ClientsNotReadyForFrame(frame).Any(); - } - - public IEnumerable ClientsNotReadyForFrame(int frame) - { - var frameData = framePackets.GetOrAdd(frame); - return ClientsPlayingInFrame(frame) - .Where(client => !frameData.ContainsKey(client)); - } - - public IEnumerable OrdersForFrame(World world, int frame) - { - var frameData = framePackets[frame]; - var clientData = ClientsPlayingInFrame(frame) - .ToDictionary(k => k, v => frameData[v]); - - return clientData - .SelectMany(x => x.Value - .ToOrderList(world) - .Select(o => new ClientOrder { Client = x.Key, Order = o })); - } - } -} diff --git a/OpenRA.Game/Network/OrderManager.cs b/OpenRA.Game/Network/OrderManager.cs index 78a858bf49..895b8c4b74 100644 --- a/OpenRA.Game/Network/OrderManager.cs +++ b/OpenRA.Game/Network/OrderManager.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using OpenRA.Primitives; using OpenRA.Support; @@ -19,10 +20,12 @@ namespace OpenRA.Network { public sealed class OrderManager : IDisposable { - static readonly IEnumerable NoClients = new Session.Client[] { }; - readonly SyncReport syncReport; - readonly FrameData frameData = new FrameData(); + + // These are the clients who we expect to receive orders / sync from before we can simulate the next tick + readonly HashSet activeClients = new HashSet(); + + readonly Dictionary> pendingPackets = new Dictionary>(); public Session LobbyInfo = new Session(); public Session.Client LocalClient => LobbyInfo.ClientWithIndex(Connection.LocalClientId); @@ -49,6 +52,7 @@ namespace OpenRA.Network readonly List localOrders = new List(); readonly List localImmediateOrders = new List(); + readonly List<(int ClientId, byte[] Packet)> immediatePackets = new List<(int ClientId, byte[] Packet)>(); readonly List chatCache = new List(); @@ -57,9 +61,20 @@ namespace OpenRA.Network bool disposed; bool generateSyncReport = false; + public struct ClientOrder + { + public int Client; + public Order Order; + + public override string ToString() + { + return "ClientId: {0} {1}".F(Client, Order); + } + } + void OutOfSync(int frame) { - syncReport.DumpSyncReport(frame, frameData.OrdersForFrame(World, frame)); + syncReport.DumpSyncReport(frame); throw new InvalidOperationException($"Out of sync in frame {frame}.\n Compare syncreport.log with other players."); } @@ -114,14 +129,12 @@ namespace OpenRA.Network Connection.SendImmediate(localImmediateOrders.Select(o => o.Serialize())); localImmediateOrders.Clear(); - var immediatePackets = new List<(int ClientId, byte[] Packet)>(); - Connection.Receive( (clientId, packet) => { var frame = BitConverter.ToInt32(packet, 0); if (packet.Length == 5 && packet[4] == (byte)OrderType.Disconnect) - frameData.ClientQuit(clientId, frame); + activeClients.Remove(clientId); else if (packet.Length > 4 && packet[4] == (byte)OrderType.SyncHash) { if (packet.Length != 4 + Order.SyncHashOrderLength) @@ -135,7 +148,10 @@ namespace OpenRA.Network else if (frame == 0) immediatePackets.Add((clientId, packet)); else - frameData.AddFrameOrders(clientId, frame, packet); + { + activeClients.Add(clientId); + pendingPackets.GetOrAdd(clientId).Enqueue(packet); + } }); foreach (var p in immediatePackets) @@ -149,6 +165,8 @@ namespace OpenRA.Network return; } } + + immediatePackets.Clear(); } Dictionary syncForFrame = new Dictionary(); @@ -169,18 +187,7 @@ namespace OpenRA.Network syncForFrame.Add(frame, packet); } - public bool IsReadyForNextFrame => GameStarted && frameData.IsReadyForFrame(NetFrameNumber); - - public IEnumerable GetClientsNotReadyForNextFrame - { - get - { - return GameStarted - ? frameData.ClientsNotReadyForFrame(NetFrameNumber) - .Select(a => LobbyInfo.ClientWithIndex(a)) - : NoClients; - } - } + public bool IsReadyForNextFrame => GameStarted && activeClients.All(client => pendingPackets[client].Count > 0); public void Tick() { @@ -192,8 +199,26 @@ namespace OpenRA.Network localOrders.Clear(); - foreach (var order in frameData.OrdersForFrame(World, NetFrameNumber)) - UnitOrders.ProcessOrder(this, World, order.Client, order.Order); + var clientOrders = new List(); + + foreach (var clientId in activeClients) + { + // The IsReadyForNextFrame check above guarantees that all clients have sent a packet + var frameData = pendingPackets[clientId].Dequeue(); + + // Orders are synchronised by sending an initial FramesAhead set of empty packets + // and then making sure that we enqueue and process exactly one packet for each player each tick. + // This may change in the future, so sanity check that the orders are for the frame we expect + // and crash early instead of risking desyncs. + var frameNumber = BitConverter.ToInt32(frameData, 0); + if (frameNumber != NetFrameNumber) + throw new InvalidDataException($"Attempted to process orders from client {clientId} for frame {frameNumber} on frame {NetFrameNumber}"); + foreach (var order in frameData.ToOrderList(World)) + { + UnitOrders.ProcessOrder(this, World, clientId, order); + clientOrders.Add(new ClientOrder { Client = clientId, Order = order }); + } + } if (NetFrameNumber + FramesAhead >= GameSaveLastSyncFrame) { @@ -209,7 +234,7 @@ namespace OpenRA.Network if (generateSyncReport) using (new PerfSample("sync_report")) - syncReport.UpdateSyncReport(); + syncReport.UpdateSyncReport(clientOrders); ++NetFrameNumber; } diff --git a/OpenRA.Game/Network/SyncReport.cs b/OpenRA.Game/Network/SyncReport.cs index 2f435b0da5..b34e9595c9 100644 --- a/OpenRA.Game/Network/SyncReport.cs +++ b/OpenRA.Game/Network/SyncReport.cs @@ -51,19 +51,20 @@ namespace OpenRA.Network syncReports[i] = new Report(); } - internal void UpdateSyncReport() + internal void UpdateSyncReport(List orders) { - GenerateSyncReport(syncReports[curIndex]); + GenerateSyncReport(syncReports[curIndex], orders); curIndex = ++curIndex % NumSyncReports; } - void GenerateSyncReport(Report report) + void GenerateSyncReport(Report report, List orders) { report.Frame = orderManager.NetFrameNumber; report.SyncedRandom = orderManager.World.SharedRandom.Last; report.TotalCount = orderManager.World.SharedRandom.TotalCount; report.Traits.Clear(); report.Effects.Clear(); + report.Orders = orders; foreach (var actor in orderManager.World.ActorsHavingTrait()) { @@ -100,7 +101,7 @@ namespace OpenRA.Network } } - internal void DumpSyncReport(int frame, IEnumerable orders) + internal void DumpSyncReport(int frame) { var reportName = "syncreport-" + DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture) + ".log"; Log.AddChannel("sync", reportName); @@ -137,7 +138,7 @@ namespace OpenRA.Network } Log.Write("sync", "Orders Issued:"); - foreach (var o in orders) + foreach (var o in r.Orders) Log.Write("sync", "\t {0}", o.ToString()); return; @@ -154,6 +155,7 @@ namespace OpenRA.Network public int TotalCount; public List Traits = new List(); public List Effects = new List(); + public List Orders = new List(); } struct TraitReport diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index c7c50cf6c3..79f6b2bc45 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -1252,16 +1252,6 @@ namespace OpenRA.Server SyncLobbyInfo(); State = ServerState.GameStarted; - var disconnectData = new[] { (byte)OrderType.Disconnect }; - foreach (var c in Conns) - { - foreach (var d in Conns) - DispatchOrdersToClient(c, d.PlayerIndex, int.MaxValue, disconnectData); - - if (recorder != null) - recorder.ReceiveFrame(c.PlayerIndex, int.MaxValue, disconnectData); - } - if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled) GameSave = new GameSave();