Files
OpenRA/OpenRA.Game/Network/ReplayConnection.cs
RoosterDragon b58c1ea5bc Provide names and pools when creating MiniYaml.
- Rename the filename parameter to name and make it mandatory. Review all callers and ensure a useful string is provided as input, to ensure sufficient context is included for logging and debugging. This can be a filename, url, or any arbitrary text so include whatever context seems reasonable.
- When several MiniYamls are created that have similar content, provide a shared string pool. This allows strings that are common between all the yaml to be shared, reducing long term memory usage. We also change the pool from a dictionary to a set. Originally a Dictionary had to be used so we could call TryGetValue to get a reference to the pooled string. Now that more recent versions of dotnet provide a TryGetValue on HashSet, we can use a set directly without the memory wasted by having to store both keys and values in a dictionary.
2024-01-21 12:39:10 +02:00

137 lines
4.1 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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 OpenRA.FileFormats;
namespace OpenRA.Network
{
public sealed class ReplayConnection : IConnection
{
sealed class Chunk
{
public int Frame;
public (int ClientId, byte[] Packet)[] Packets;
}
readonly Queue<Chunk> chunks = new();
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new();
readonly int orderLatency;
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 (packet.Length > 4 && (packet[4] == (byte)OrderType.Disconnect || packet[4] == (byte)OrderType.SyncHash))
continue;
if (frame == 0)
{
// Parse replay metadata from orders stream
if (OrderIO.TryParseOrderPacket(packet, out var orders))
{
foreach (var o in orders.Orders.GetOrders(null))
{
if (o.OrderString == "StartGame")
IsValid = true;
else if (o.OrderString == "SyncInfo" && !IsValid)
LobbyInfo = Session.Deserialize(o.TargetString, o.OrderString);
}
}
}
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 gameSpeeds = Game.ModData.Manifest.Get<GameSpeeds>();
var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed);
orderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency;
}
void IConnection.StartGame() { }
// Do nothing: ignore locally generated orders
void IConnection.Send(int frame, IEnumerable<Order> orders) { }
void IConnection.SendImmediate(IEnumerable<Order> orders) { }
void IConnection.SendSync(int frame, int syncHash, ulong defeatState)
{
sync.Enqueue((frame, syncHash, defeatState));
}
void IConnection.Receive(OrderManager orderManager)
{
while (sync.Count != 0)
orderManager.ReceiveSync(sync.Dequeue());
while (chunks.Count != 0 && chunks.Peek().Frame <= orderManager.NetFrameNumber + orderLatency)
{
foreach (var o in chunks.Dequeue().Packets)
{
if (OrderIO.TryParseDisconnect(o, out var disconnect))
orderManager.ReceiveDisconnect(disconnect.ClientId, disconnect.Frame);
else if (OrderIO.TryParseSync(o.Packet, out var sync))
orderManager.ReceiveSync(sync);
else if (OrderIO.TryParseOrderPacket(o.Packet, out var orders))
{
if (orders.Frame == 0)
orderManager.ReceiveImmediateOrders(o.ClientId, orders.Orders);
else
orderManager.ReceiveOrders(o.ClientId, orders);
}
else
throw new InvalidDataException($"Received unknown packet from client {o.ClientId} with length {o.Packet.Length}");
}
}
}
int IConnection.LocalClientId => -1;
void IDisposable.Dispose() { }
}
}