#region Copyright & License Information /* * Copyright 2007-2021 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; using System.Collections.Generic; using System.IO; using System.Net; using OpenRA.FileFormats; namespace OpenRA.Network { public sealed class ReplayConnection : IConnection { class Chunk { public int Frame; public (int ClientId, byte[] Packet)[] Packets; } Queue chunks = new Queue(); Queue sync = new Queue(); 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; public readonly Session LobbyInfo; public readonly string Filename; public ReplayConnection(string replayFilename) { Filename = replayFilename; FinalGameTick = ReplayMetadata.Read(replayFilename).GameInfo.FinalGameTick; // Parse replay data into a struct that can be fed to the game in chunks // to avoid issues with all immediate orders being resolved on the first tick. using (var rs = File.OpenRead(replayFilename)) { var packets = new List<(int ClientId, byte[] Packet)>(); var chunk = new Chunk(); while (rs.Position < rs.Length) { var client = rs.ReadInt32(); if (client == ReplayMetadata.MetaStartMarker) break; var packetLen = rs.ReadInt32(); var packet = rs.ReadBytes(packetLen); var frame = BitConverter.ToInt32(packet, 0); packets.Add((client, packet)); if (frame != int.MaxValue && (!lastClientsFrame.ContainsKey(client) || frame > lastClientsFrame[client])) lastClientsFrame[client] = frame; if (packet.Length > 4 && (packet[4] == (byte)OrderType.Disconnect || packet[4] == (byte)OrderType.SyncHash)) continue; if (frame == 0) { // Parse replay metadata from orders stream var orders = packet.ToOrderList(null); foreach (var o in orders) { if (o.OrderString == "StartGame") IsValid = true; else if (o.OrderString == "SyncInfo" && !IsValid) LobbyInfo = Session.Deserialize(o.TargetString); } } else { // Regular order - finalize the chunk chunk.Frame = frame; chunk.Packets = packets.ToArray(); packets.Clear(); chunks.Enqueue(chunk); chunk = new Chunk(); TickCount = Math.Max(TickCount, frame); } } var lastClientToDisconnect = lastClientsFrame.MaxBy(kvp => kvp.Value).Key; // 2nd parse : replace all disconnect packets without frame with real // disconnect frame // NOTE: to modify/remove if a reconnect feature is set foreach (var tmpChunk in chunks) { foreach (var tmpPacketPair in tmpChunk.Packets) { var client = tmpPacketPair.ClientId; // Don't replace the final disconnection packet - we still want this to end the replay. if (client == lastClientToDisconnect) continue; var packet = tmpPacketPair.Packet; if (packet.Length == Order.DisconnectOrderLength + 4 && packet[4] == (byte)OrderType.Disconnect) { var lastClientFrame = lastClientsFrame[client]; var lastFramePacket = BitConverter.GetBytes(lastClientFrame); Array.Copy(lastFramePacket, packet, lastFramePacket.Length); } } } } var gameSpeeds = Game.ModData.Manifest.Get(); var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed); orderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency; ordersFrame = orderLatency; } // Do nothing: ignore locally generated orders public void Send(int frame, List orders) { } public void SendImmediate(IEnumerable orders) { } public void SendSync(int frame, byte[] syncData) { var ms = new MemoryStream(4 + syncData.Length); ms.WriteArray(BitConverter.GetBytes(frame)); ms.WriteArray(syncData); sync.Enqueue(ms.GetBuffer()); // Store the current frame so Receive() can return the next chunk of orders. ordersFrame = frame + orderLatency; } public void Receive(Action packetFn) { while (sync.Count != 0) packetFn(LocalClientId, sync.Dequeue()); while (chunks.Count != 0 && chunks.Peek().Frame <= ordersFrame) foreach (var o in chunks.Dequeue().Packets) packetFn(o.ClientId, o.Packet); } public void Dispose() { } } }