diff --git a/CHANGELOG b/CHANGELOG index c9ad1d084b..e776ac8067 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ NEW: Added a toggle spectators to multiplayer. Removed the ability of commandos to plant C4 on walls. Order lines are now shown on unit selection. + Fixed chat synchronization in replays. Dune 2000: Added the Atreides grenadier from the 1.06 patch. Added randomized tiles for Sand and Rock terrain. diff --git a/OpenRA.Game/Network/Replay.cs b/OpenRA.Game/Network/Replay.cs index 3e6970d756..155489bcc4 100644 --- a/OpenRA.Game/Network/Replay.cs +++ b/OpenRA.Game/Network/Replay.cs @@ -12,7 +12,6 @@ using System; namespace OpenRA.Network { - /* HACK: a maze of twisty little hacks... */ public class Replay { public readonly string Filename; @@ -22,34 +21,12 @@ namespace OpenRA.Network public Replay(string filename) { Filename = filename; - var lastFrame = 0; - var hasSeenGameStart = false; - var lobbyInfo = null as Session; using (var conn = new ReplayConnection(filename)) - conn.Receive((client, packet) => - { - var frame = BitConverter.ToInt32(packet, 0); - if (packet.Length == 5 && packet[4] == 0xBF) - return; // disconnect - else if (packet.Length >= 5 && packet[4] == 0x65) - return; // sync - else if (frame == 0) - { - /* decode this to recover lobbyinfo, etc */ - var orders = packet.ToOrderList(null); - foreach (var o in orders) - if (o.OrderString == "StartGame") - hasSeenGameStart = true; - else if (o.OrderString == "SyncInfo" && !hasSeenGameStart) - lobbyInfo = Session.Deserialize(o.TargetString); - } - else - lastFrame = Math.Max(lastFrame, frame); - }); - - Duration = lastFrame * Game.NetTickScale; - LobbyInfo = lobbyInfo; + { + Duration = conn.TickCount * Game.NetTickScale; + LobbyInfo = conn.LobbyInfo; + } } public Map Map() diff --git a/OpenRA.Game/Network/ReplayConnection.cs b/OpenRA.Game/Network/ReplayConnection.cs index a705a2196b..7e031fc486 100755 --- a/OpenRA.Game/Network/ReplayConnection.cs +++ b/OpenRA.Game/Network/ReplayConnection.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2014 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. For more information, @@ -11,52 +11,99 @@ using System; using System.Collections.Generic; using System.IO; +using OpenRA.FileFormats; namespace OpenRA.Network { public class ReplayConnection : IConnection { - FileStream replayStream; - List sync = new List(); + class Chunk + { + public int Frame; + public List> Packets = new List>(); + } - public ReplayConnection( string replayFilename ) { replayStream = File.OpenRead( replayFilename ); } + Queue chunks = new Queue(); + List sync = new List(); + int ordersFrame = 1; public int LocalClientId { get { return 0; } } public ConnectionState ConnectionState { get { return ConnectionState.Connected; } } + public readonly int TickCount; + public readonly bool IsValid; + public readonly Session LobbyInfo; - // do nothing; ignore locally generated orders - public void Send( int frame, List orders ) { } - public void SendImmediate( List orders ) { } - - public void SendSync( int frame, byte[] syncData ) + public ReplayConnection(string replayFilename) { - var ms = new MemoryStream(); - ms.Write( BitConverter.GetBytes( frame ) ); - ms.Write( syncData ); - sync.Add( ms.ToArray() ); + // 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 chunk = new Chunk(); + + while (rs.Position < rs.Length) + { + var client = rs.ReadInt32(); + var packetLen = rs.ReadInt32(); + var packet = rs.ReadBytes(packetLen); + var frame = BitConverter.ToInt32(packet, 0); + chunk.Packets.Add(Pair.New(client, packet)); + + if (packet.Length == 5 && packet[4] == 0xBF) + continue; // disconnect + else if (packet.Length >= 5 && packet[4] == 0x65) + continue; // sync + else 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; + chunks.Enqueue(chunk); + chunk = new Chunk(); + + TickCount = Math.Max(TickCount, frame); + } + } + } } - public void Receive( Action packetFn ) + // Do nothing: ignore locally generated orders + public void Send(int frame, List orders) { } + public void SendImmediate(List orders) { } + + public void SendSync(int frame, byte[] syncData) { - while( sync.Count != 0 ) + var ms = new MemoryStream(); + ms.Write(BitConverter.GetBytes(frame)); + ms.Write(syncData); + sync.Add(ms.ToArray()); + + // Store the current frame so Receive() can return the next chunk of orders. + ordersFrame = frame + 1; + } + + public void Receive(Action packetFn) + { + while (sync.Count != 0) { - packetFn( LocalClientId, sync[ 0 ] ); - sync.RemoveAt( 0 ); + packetFn(LocalClientId, sync[0]); + sync.RemoveAt(0); } - if( replayStream == null ) return; - - var reader = new BinaryReader( replayStream ); - - while( replayStream.Position < replayStream.Length ) - { - var client = reader.ReadInt32(); - var packetLen = reader.ReadInt32(); - var packet = reader.ReadBytes( packetLen ); - packetFn( client, packet ); - } - - replayStream = null; + while (chunks.Count != 0 && chunks.Peek().Frame <= ordersFrame) + foreach (var o in chunks.Dequeue().Packets) + packetFn(o.First, o.Second); } public void Dispose() { }