diff --git a/OpenRA.Game/Network/Connection.cs b/OpenRA.Game/Network/Connection.cs index 3653a618ea..e05d73ec2d 100644 --- a/OpenRA.Game/Network/Connection.cs +++ b/OpenRA.Game/Network/Connection.cs @@ -31,10 +31,10 @@ namespace OpenRA.Network public interface IConnection : IDisposable { int LocalClientId { get; } - void Send(int frame, List orders); - void SendImmediate(IEnumerable orders); - void SendSync(int frame, byte[] syncData); - void Receive(Action packetFn); + void Send(int frame, IEnumerable orders); + void SendImmediate(IEnumerable orders); + void SendSync(int frame, int syncHash, ulong defeatState); + void Receive(OrderManager orderManager); } public class EchoConnection : IConnection @@ -50,32 +50,29 @@ namespace OpenRA.Network public virtual int LocalClientId => 1; - public virtual void Send(int frame, List orders) + public virtual void Send(int frame, IEnumerable orders) { var ms = new MemoryStream(); ms.WriteArray(BitConverter.GetBytes(frame)); foreach (var o in orders) - ms.WriteArray(o); + ms.WriteArray(o.Serialize()); Send(ms.ToArray()); } - public virtual void SendImmediate(IEnumerable orders) + public virtual void SendImmediate(IEnumerable orders) { foreach (var o in orders) { var ms = new MemoryStream(); ms.WriteArray(BitConverter.GetBytes(0)); - ms.WriteArray(o); + ms.WriteArray(o.Serialize()); Send(ms.ToArray()); } } - public virtual void SendSync(int frame, byte[] syncData) + public virtual void SendSync(int frame, int syncHash, ulong defeatState) { - var ms = new MemoryStream(4 + syncData.Length); - ms.WriteArray(BitConverter.GetBytes(frame)); - ms.WriteArray(syncData); - Send(ms.GetBuffer()); + Send(OrderIO.SerializeSync(frame, syncHash, defeatState)); } protected virtual void Send(byte[] packet) @@ -90,11 +87,22 @@ namespace OpenRA.Network receivedPackets.Enqueue(packet); } - public virtual void Receive(Action packetFn) + public virtual void Receive(OrderManager orderManager) { while (receivedPackets.TryDequeue(out var p)) { - packetFn(p.FromClient, p.Data); + if (OrderIO.TryParseDisconnect(p.Data, out var disconnectClient)) + orderManager.ReceiveDisconnect(disconnectClient); + else if (OrderIO.TryParseSync(p.Data, out var syncFrame, out var syncHash, out var defeatState)) + orderManager.ReceiveSync(syncFrame, syncHash, defeatState); + else if (OrderIO.TryParseOrderPacket(p.Data, out var ordersFrame, out var orders)) + { + if (ordersFrame == 0) + orderManager.ReceiveImmediateOrders(p.FromClient, orders); + else + orderManager.ReceiveOrders(p.FromClient, ordersFrame, orders); + } + Recorder?.Receive(p.FromClient, p.Data); } } @@ -247,12 +255,9 @@ namespace OpenRA.Network public override int LocalClientId => clientId; public ConnectionState ConnectionState => connectionState; - public override void SendSync(int frame, byte[] syncData) + public override void SendSync(int frame, int syncHash, ulong defeatState) { - var ms = new MemoryStream(4 + syncData.Length); - ms.WriteArray(BitConverter.GetBytes(frame)); - ms.WriteArray(syncData); - queuedSyncPackets.Add(ms.GetBuffer()); + queuedSyncPackets.Add(OrderIO.SerializeSync(frame, syncHash, defeatState)); } protected override void Send(byte[] packet) diff --git a/OpenRA.Game/Network/Order.cs b/OpenRA.Game/Network/Order.cs index bd4e205248..ef6415236d 100644 --- a/OpenRA.Game/Network/Order.cs +++ b/OpenRA.Game/Network/Order.cs @@ -372,11 +372,16 @@ namespace OpenRA case TargetType.Terrain: if (fields.HasField(OrderFields.TargetIsCell)) { - w.Write(Target.SerializableCell.Value); + w.Write(Target.SerializableCell.Value.Bits); w.Write((byte)Target.SerializableSubCell); } else - w.Write(Target.SerializablePos); + { + w.Write(Target.SerializablePos.X); + w.Write(Target.SerializablePos.Y); + w.Write(Target.SerializablePos.Z); + } + break; } } @@ -392,7 +397,7 @@ namespace OpenRA } if (fields.HasField(OrderFields.ExtraLocation)) - w.Write(ExtraLocation); + w.Write(ExtraLocation.Bits); if (fields.HasField(OrderFields.ExtraData)) w.Write(ExtraData); diff --git a/OpenRA.Game/Network/OrderIO.cs b/OpenRA.Game/Network/OrderIO.cs index 1b4ec6cd3d..2a0961299c 100644 --- a/OpenRA.Game/Network/OrderIO.cs +++ b/OpenRA.Game/Network/OrderIO.cs @@ -9,70 +9,126 @@ */ #endregion +using System; using System.Collections.Generic; using System.IO; namespace OpenRA.Network { - public static class OrderIO + public class OrderPacket { - static readonly List EmptyOrderList = new List(0); - - public static List ToOrderList(this byte[] bytes, World world) + readonly Order[] orders; + readonly MemoryStream data; + public OrderPacket(Order[] orders) { - // PERF: Skip empty order frames, often per client each frame - if (bytes.Length == 4) - return EmptyOrderList; + this.orders = orders; + data = null; + } - var ms = new MemoryStream(bytes, 4, bytes.Length - 4); - var reader = new BinaryReader(ms); - var ret = new List(); - while (ms.Position < ms.Length) + public OrderPacket(MemoryStream data) + { + orders = null; + this.data = data; + } + + public IEnumerable GetOrders(World world) + { + return orders ?? ParseData(world); + } + + IEnumerable ParseData(World world) + { + if (data == null) + yield break; + + // Order deserialization depends on the current world state, + // so must be deferred until we are ready to consume them. + var reader = new BinaryReader(data); + while (data.Position < data.Length) { var o = Order.Deserialize(world, reader); if (o != null) - ret.Add(o); + yield return o; } - - return ret; } - public static byte[] SerializeSync(int sync, ulong defeatState) + public byte[] Serialize(int frame) { - var ms = new MemoryStream(Order.SyncHashOrderLength); - using (var writer = new BinaryWriter(ms)) - { - writer.Write((byte)OrderType.SyncHash); - writer.Write(sync); - writer.Write(defeatState); - } + if (data != null) + return data.ToArray(); + var ms = new MemoryStream(); + ms.WriteArray(BitConverter.GetBytes(frame)); + foreach (var o in orders) + ms.WriteArray(o.Serialize()); + return ms.ToArray(); + } + } + + public static class OrderIO + { + static readonly OrderPacket NoOrders = new OrderPacket(Array.Empty()); + + public static byte[] SerializeSync(int frame, int syncHash, ulong defeatState) + { + var ms = new MemoryStream(4 + Order.SyncHashOrderLength); + ms.WriteArray(BitConverter.GetBytes(frame)); + ms.WriteByte((byte)OrderType.SyncHash); + ms.WriteArray(BitConverter.GetBytes(syncHash)); + ms.WriteArray(BitConverter.GetBytes(defeatState)); return ms.GetBuffer(); } - public static int2 ReadInt2(this BinaryReader r) + public static bool TryParseDisconnect(byte[] packet, out int clientId) { - var x = r.ReadInt32(); - var y = r.ReadInt32(); - return new int2(x, y); + if (packet.Length == Order.DisconnectOrderLength + 4 && packet[4] == (byte)OrderType.Disconnect) + { + clientId = BitConverter.ToInt32(packet, 5); + return true; + } + + clientId = 0; + return false; } - public static void Write(this BinaryWriter w, int2 p) + public static bool TryParseSync(byte[] packet, out int frame, out int syncHash, out ulong defeatState) { - w.Write(p.X); - w.Write(p.Y); + if (packet.Length != 4 + Order.SyncHashOrderLength || packet[4] != (byte)OrderType.SyncHash) + { + frame = syncHash = 0; + defeatState = 0; + return false; + } + + frame = BitConverter.ToInt32(packet, 0); + syncHash = BitConverter.ToInt32(packet, 5); + defeatState = BitConverter.ToUInt64(packet, 9); + return true; } - public static void Write(this BinaryWriter w, CPos cell) + public static bool TryParseOrderPacket(byte[] packet, out int frame, out OrderPacket orders) { - w.Write(cell.Bits); - } + // Not a valid packet + if (packet.Length < 4) + { + frame = 0; + orders = null; + return false; + } - public static void Write(this BinaryWriter w, WPos pos) - { - w.Write(pos.X); - w.Write(pos.Y); - w.Write(pos.Z); + // Wrong packet type + if (packet.Length >= 5 && (packet[4] == (byte)OrderType.Disconnect || packet[4] == (byte)OrderType.SyncHash)) + { + frame = 0; + orders = null; + return false; + } + + frame = BitConverter.ToInt32(packet, 0); + + // PERF: Skip empty order frames, often per client each frame + orders = packet.Length > 4 ? new OrderPacket(new MemoryStream(packet, 4, packet.Length - 4)) : NoOrders; + return true; } } } diff --git a/OpenRA.Game/Network/OrderManager.cs b/OpenRA.Game/Network/OrderManager.cs index 58037ce297..9825f7ea98 100644 --- a/OpenRA.Game/Network/OrderManager.cs +++ b/OpenRA.Game/Network/OrderManager.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using OpenRA.Primitives; using OpenRA.Support; using OpenRA.Widgets; @@ -22,8 +21,8 @@ namespace OpenRA.Network public sealed class OrderManager : IDisposable { readonly SyncReport syncReport; - - readonly Dictionary> pendingPackets = new Dictionary>(); + readonly Dictionary> pendingOrders = new Dictionary>(); + readonly Dictionary syncForFrame = new Dictionary(); public Session LobbyInfo = new Session(); public Session.Client LocalClient => LobbyInfo.ClientWithIndex(Connection.LocalClientId); @@ -78,7 +77,7 @@ namespace OpenRA.Network foreach (var client in LobbyInfo.Clients) if (!client.IsBot) - pendingPackets.Add(client.Index, new Queue()); + pendingOrders.Add(client.Index, new Queue<(int, OrderPacket)>()); // Generating sync reports is expensive, so only do it if we have // other players to compare against if a desync did occur @@ -90,7 +89,7 @@ namespace OpenRA.Network if (GameSaveLastFrame < 0) for (var i = NetFrameNumber; i <= FramesAhead; i++) - Connection.Send(i, new List()); + Connection.Send(i, Array.Empty()); } public OrderManager(IConnection conn) @@ -125,74 +124,68 @@ namespace OpenRA.Network void SendImmediateOrders() { if (localImmediateOrders.Count != 0 && GameSaveLastFrame < NetFrameNumber + FramesAhead) - Connection.SendImmediate(localImmediateOrders.Select(o => o.Serialize())); + Connection.SendImmediate(localImmediateOrders); localImmediateOrders.Clear(); } + public void ReceiveDisconnect(int clientIndex) + { + // HACK: The shellmap relies on ticking a disposed OM + if (disposed && World.Type != WorldType.Shellmap) + return; + + pendingOrders.Remove(clientIndex); + } + + public void ReceiveSync(int frame, int syncHash, ulong defeatState) + { + // HACK: The shellmap relies on ticking a disposed OM + if (disposed && World.Type != WorldType.Shellmap) + return; + + if (syncForFrame.TryGetValue(frame, out var s)) + { + if (s.SyncHash != syncHash || s.DefeatState != defeatState) + OutOfSync(frame); + } + else + syncForFrame.Add(frame, (syncHash, defeatState)); + } + + public void ReceiveImmediateOrders(int clientId, OrderPacket orders) + { + // HACK: The shellmap relies on ticking a disposed OM + if (disposed && World.Type != WorldType.Shellmap) + return; + + foreach (var o in orders.GetOrders(World)) + { + UnitOrders.ProcessOrder(this, World, clientId, o); + + // A mod switch or other event has pulled the ground from beneath us + if (disposed) + return; + } + } + + public void ReceiveOrders(int clientId, int frame, OrderPacket orders) + { + // HACK: The shellmap relies on ticking a disposed OM + if (disposed && World.Type != WorldType.Shellmap) + return; + + if (pendingOrders.TryGetValue(clientId, out var queue)) + queue.Enqueue((frame, orders)); + else + Log.Write("debug", $"Received packet from disconnected client '{clientId}'"); + } + void ReceiveAllOrdersAndCheckSync() { - Connection.Receive( - (clientId, packet) => - { - // HACK: The shellmap relies on ticking a disposed OM - if (disposed && World.Type != WorldType.Shellmap) - return; - - var frame = BitConverter.ToInt32(packet, 0); - if (packet.Length == Order.DisconnectOrderLength + 4 && packet[4] == (byte)OrderType.Disconnect) - { - pendingPackets.Remove(BitConverter.ToInt32(packet, 5)); - } - else if (packet.Length > 4 && packet[4] == (byte)OrderType.SyncHash) - { - if (packet.Length != 4 + Order.SyncHashOrderLength) - { - Log.Write("debug", $"Dropped sync order with length {packet.Length}. Expected length {4 + Order.SyncHashOrderLength}."); - return; - } - - CheckSync(packet); - } - else if (frame == 0) - { - foreach (var o in packet.ToOrderList(World)) - { - UnitOrders.ProcessOrder(this, World, clientId, o); - - // A mod switch or other event has pulled the ground from beneath us - if (disposed) - return; - } - } - else - { - if (pendingPackets.TryGetValue(clientId, out var queue)) - queue.Enqueue(packet); - else - Log.Write("debug", $"Received packet from disconnected client '{clientId}'"); - } - }); + Connection.Receive(this); } - Dictionary syncForFrame = new Dictionary(); - - void CheckSync(byte[] packet) - { - var frame = BitConverter.ToInt32(packet, 0); - if (syncForFrame.TryGetValue(frame, out var existingSync)) - { - if (packet.Length != existingSync.Length) - OutOfSync(frame); - else - for (var i = 0; i < packet.Length; i++) - if (packet[i] != existingSync[i]) - OutOfSync(frame); - } - else - syncForFrame.Add(frame, packet); - } - - bool IsReadyForNextFrame => GameStarted && pendingPackets.All(p => p.Value.Count > 0); + bool IsReadyForNextFrame => GameStarted && pendingOrders.All(p => p.Value.Count > 0); int SuggestedTimestep { @@ -218,7 +211,7 @@ namespace OpenRA.Network if (GameSaveLastFrame < NetFrameNumber + FramesAhead) { - Connection.Send(NetFrameNumber + FramesAhead, localOrders.Select(o => o.Serialize()).ToList()); + Connection.Send(NetFrameNumber + FramesAhead, localOrders); localOrders.Clear(); } } @@ -227,20 +220,19 @@ namespace OpenRA.Network { var clientOrders = new List(); - foreach (var (clientId, clientPackets) in pendingPackets) + foreach (var (clientId, frameOrders) in pendingOrders) { // The IsReadyForNextFrame check above guarantees that all clients have sent a packet - var frameData = clientPackets.Dequeue(); + var (frameNumber, orders) = frameOrders.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)) + foreach (var order in orders.GetOrders(World)) { UnitOrders.ProcessOrder(this, World, clientId, order); clientOrders.Add(new ClientOrder { Client = clientId, Order = order }); @@ -254,10 +246,10 @@ namespace OpenRA.Network if (World.Players[i].WinState == WinState.Lost) defeatState |= 1UL << i; - Connection.SendSync(NetFrameNumber, OrderIO.SerializeSync(World.SyncHash(), defeatState)); + Connection.SendSync(NetFrameNumber, World.SyncHash(), defeatState); } else - Connection.SendSync(NetFrameNumber, OrderIO.SerializeSync(0, 0)); + Connection.SendSync(NetFrameNumber, 0, 0); if (generateSyncReport) using (new PerfSample("sync_report")) @@ -287,7 +279,7 @@ namespace OpenRA.Network { // Check whether or not we will be ready for a tick next frame // We don't need to include ourselves in the equation because we can always generate orders this frame - shouldTick = pendingPackets.All(p => p.Key == Connection.LocalClientId || p.Value.Count > 0); + shouldTick = pendingOrders.All(p => p.Key == Connection.LocalClientId || p.Value.Count > 0); // Send orders only if we are currently ready, this prevents us sending orders too soon if we are // stalling diff --git a/OpenRA.Game/Network/ReplayConnection.cs b/OpenRA.Game/Network/ReplayConnection.cs index c910957ff3..3c39ee7fa8 100644 --- a/OpenRA.Game/Network/ReplayConnection.cs +++ b/OpenRA.Game/Network/ReplayConnection.cs @@ -25,20 +25,14 @@ namespace OpenRA.Network public (int ClientId, byte[] Packet)[] Packets; } - Queue chunks = new Queue(); - Queue sync = new Queue(); - + readonly Queue chunks = new Queue(); + readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new Queue<(int, int, ulong)>(); + readonly Dictionary lastClientsFrame = new Dictionary(); readonly int orderLatency; int ordersFrame; - Dictionary lastClientsFrame = new Dictionary(); - public int LocalClientId => -1; - public IPEndPoint EndPoint => throw new NotSupportedException("A replay connection doesn't have an endpoint"); - - public string ErrorMessage => null; - public readonly int TickCount; public readonly int FinalGameTick; public readonly bool IsValid; @@ -76,13 +70,15 @@ namespace OpenRA.Network if (frame == 0) { // Parse replay metadata from orders stream - var orders = packet.ToOrderList(null); - foreach (var o in orders) + if (OrderIO.TryParseOrderPacket(packet, out _, out var orders)) { - if (o.OrderString == "StartGame") - IsValid = true; - else if (o.OrderString == "SyncInfo" && !IsValid) - LobbyInfo = Session.Deserialize(o.TargetString); + foreach (var o in orders.GetOrders(null)) + { + if (o.OrderString == "StartGame") + IsValid = true; + else if (o.OrderString == "SyncInfo" && !IsValid) + LobbyInfo = Session.Deserialize(o.TargetString); + } } } else @@ -131,28 +127,42 @@ namespace OpenRA.Network } // Do nothing: ignore locally generated orders - public void Send(int frame, List orders) { } - public void SendImmediate(IEnumerable orders) { } + public void Send(int frame, IEnumerable orders) { } + public void SendImmediate(IEnumerable orders) { } - public void SendSync(int frame, byte[] syncData) + public void SendSync(int frame, int syncHash, ulong defeatState) { - var ms = new MemoryStream(4 + syncData.Length); - ms.WriteArray(BitConverter.GetBytes(frame)); - ms.WriteArray(syncData); - sync.Enqueue(ms.GetBuffer()); + sync.Enqueue((frame, syncHash, defeatState)); // Store the current frame so Receive() can return the next chunk of orders. ordersFrame = frame + orderLatency; } - public void Receive(Action packetFn) + public void Receive(OrderManager orderManager) { while (sync.Count != 0) - packetFn(LocalClientId, sync.Dequeue()); + { + var (syncFrame, syncHash, defeatState) = sync.Dequeue(); + orderManager.ReceiveSync(syncFrame, syncHash, defeatState); + } while (chunks.Count != 0 && chunks.Peek().Frame <= ordersFrame) + { foreach (var o in chunks.Dequeue().Packets) - packetFn(o.ClientId, o.Packet); + { + if (OrderIO.TryParseDisconnect(o.Packet, out var disconnectClient)) + orderManager.ReceiveDisconnect(disconnectClient); + else if (OrderIO.TryParseSync(o.Packet, out var syncFrame, out var syncHash, out var defeatState)) + orderManager.ReceiveSync(syncFrame, syncHash, defeatState); + else if (OrderIO.TryParseOrderPacket(o.Packet, out var frame, out var orders)) + { + if (frame == 0) + orderManager.ReceiveImmediateOrders(o.ClientId, orders); + else + orderManager.ReceiveOrders(o.ClientId, frame, orders); + } + } + } } public void Dispose() { } diff --git a/OpenRA.Game/Network/ReplayRecorder.cs b/OpenRA.Game/Network/ReplayRecorder.cs index 1b7db91d30..d9e2bb823b 100644 --- a/OpenRA.Game/Network/ReplayRecorder.cs +++ b/OpenRA.Game/Network/ReplayRecorder.cs @@ -29,11 +29,10 @@ namespace OpenRA.Network static bool IsGameStart(byte[] data) { - if (data.Length > 4 && (data[4] == (byte)OrderType.Disconnect || data[4] == (byte)OrderType.SyncHash)) + if (!OrderIO.TryParseOrderPacket(data, out var frame, out var orders)) return false; - var frame = BitConverter.ToInt32(data, 0); - return frame == 0 && data.ToOrderList(null).Any(o => o.OrderString == "StartGame"); + return frame == 0 && orders.GetOrders(null).Any(o => o.OrderString == "StartGame"); } public ReplayRecorder(Func chooseFilename)