241 lines
6.6 KiB
C#
241 lines
6.6 KiB
C#
#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;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Support;
|
|
|
|
namespace OpenRA.Network
|
|
{
|
|
public sealed class OrderManager : IDisposable
|
|
{
|
|
static readonly IEnumerable<Session.Client> NoClients = new Session.Client[] { };
|
|
|
|
readonly SyncReport syncReport;
|
|
readonly FrameData frameData = new FrameData();
|
|
|
|
public Session LobbyInfo = new Session();
|
|
public Session.Client LocalClient => LobbyInfo.ClientWithIndex(Connection.LocalClientId);
|
|
public World World;
|
|
|
|
public readonly ConnectionTarget Endpoint;
|
|
public readonly string Password = "";
|
|
|
|
public string ServerError = null;
|
|
public bool AuthenticationFailed = false;
|
|
public ExternalMod ServerExternalMod = null;
|
|
|
|
public int NetFrameNumber { get; private set; }
|
|
public int LocalFrameNumber;
|
|
public int FramesAhead = 0;
|
|
|
|
public long LastTickTime = Game.RunTime;
|
|
|
|
public bool GameStarted => NetFrameNumber != 0;
|
|
public IConnection Connection { get; private set; }
|
|
|
|
internal int GameSaveLastFrame = -1;
|
|
internal int GameSaveLastSyncFrame = -1;
|
|
|
|
readonly List<Order> localOrders = new List<Order>();
|
|
readonly List<Order> localImmediateOrders = new List<Order>();
|
|
|
|
readonly List<ChatLine> chatCache = new List<ChatLine>();
|
|
|
|
public readonly ReadOnlyList<ChatLine> ChatCache;
|
|
|
|
bool disposed;
|
|
bool generateSyncReport = false;
|
|
|
|
void OutOfSync(int frame)
|
|
{
|
|
syncReport.DumpSyncReport(frame, frameData.OrdersForFrame(World, frame));
|
|
throw new InvalidOperationException("Out of sync in frame {0}.\n Compare syncreport.log with other players.".F(frame));
|
|
}
|
|
|
|
public void StartGame()
|
|
{
|
|
if (GameStarted)
|
|
return;
|
|
|
|
// Generating sync reports is expensive, so only do it if we have
|
|
// other players to compare against if a desync did occur
|
|
generateSyncReport = !(Connection is ReplayConnection) && LobbyInfo.GlobalSettings.EnableSyncReports;
|
|
|
|
NetFrameNumber = 1;
|
|
|
|
if (GameSaveLastFrame < 0)
|
|
for (var i = NetFrameNumber; i <= FramesAhead; i++)
|
|
Connection.Send(i, new List<byte[]>());
|
|
}
|
|
|
|
public OrderManager(ConnectionTarget endpoint, string password, IConnection conn)
|
|
{
|
|
Endpoint = endpoint;
|
|
Password = password;
|
|
Connection = conn;
|
|
syncReport = new SyncReport(this);
|
|
ChatCache = new ReadOnlyList<ChatLine>(chatCache);
|
|
AddChatLine += CacheChatLine;
|
|
}
|
|
|
|
public void IssueOrders(Order[] orders)
|
|
{
|
|
foreach (var order in orders)
|
|
IssueOrder(order);
|
|
}
|
|
|
|
public void IssueOrder(Order order)
|
|
{
|
|
if (order.IsImmediate)
|
|
localImmediateOrders.Add(order);
|
|
else
|
|
localOrders.Add(order);
|
|
}
|
|
|
|
public Action<string, Color, string, Color> AddChatLine = (n, nc, s, tc) => { };
|
|
void CacheChatLine(string name, Color nameColor, string text, Color textColor)
|
|
{
|
|
chatCache.Add(new ChatLine(name, nameColor, text, textColor));
|
|
}
|
|
|
|
public void TickImmediate()
|
|
{
|
|
if (localImmediateOrders.Count != 0 && GameSaveLastFrame < NetFrameNumber + FramesAhead)
|
|
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);
|
|
else if (packet.Length > 4 && packet[4] == (byte)OrderType.SyncHash)
|
|
{
|
|
if (packet.Length != 4 + Order.SyncHashOrderLength)
|
|
{
|
|
Log.Write("debug", "Dropped sync order with length {0}. Expected length {1}.".F(packet.Length, 4 + Order.SyncHashOrderLength));
|
|
return;
|
|
}
|
|
|
|
CheckSync(packet);
|
|
}
|
|
else if (frame == 0)
|
|
immediatePackets.Add((clientId, packet));
|
|
else
|
|
frameData.AddFrameOrders(clientId, frame, packet);
|
|
});
|
|
|
|
foreach (var p in immediatePackets)
|
|
{
|
|
foreach (var o in p.Packet.ToOrderList(World))
|
|
{
|
|
UnitOrders.ProcessOrder(this, World, p.ClientId, o);
|
|
|
|
// A mod switch or other event has pulled the ground from beneath us
|
|
if (disposed)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
Dictionary<int, byte[]> syncForFrame = new Dictionary<int, byte[]>();
|
|
|
|
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);
|
|
}
|
|
|
|
public bool IsReadyForNextFrame => GameStarted && frameData.IsReadyForFrame(NetFrameNumber);
|
|
|
|
public IEnumerable<Session.Client> GetClientsNotReadyForNextFrame
|
|
{
|
|
get
|
|
{
|
|
return GameStarted
|
|
? frameData.ClientsNotReadyForFrame(NetFrameNumber)
|
|
.Select(a => LobbyInfo.ClientWithIndex(a))
|
|
: NoClients;
|
|
}
|
|
}
|
|
|
|
public void Tick()
|
|
{
|
|
if (!IsReadyForNextFrame)
|
|
throw new InvalidOperationException();
|
|
|
|
if (GameSaveLastFrame < NetFrameNumber + FramesAhead)
|
|
Connection.Send(NetFrameNumber + FramesAhead, localOrders.Select(o => o.Serialize()).ToList());
|
|
|
|
localOrders.Clear();
|
|
|
|
foreach (var order in frameData.OrdersForFrame(World, NetFrameNumber))
|
|
UnitOrders.ProcessOrder(this, World, order.Client, order.Order);
|
|
|
|
if (NetFrameNumber + FramesAhead >= GameSaveLastSyncFrame)
|
|
{
|
|
var defeatState = 0UL;
|
|
for (var i = 0; i < World.Players.Length; i++)
|
|
if (World.Players[i].WinState == WinState.Lost)
|
|
defeatState |= 1UL << i;
|
|
|
|
Connection.SendSync(NetFrameNumber, OrderIO.SerializeSync(World.SyncHash(), defeatState));
|
|
}
|
|
else
|
|
Connection.SendSync(NetFrameNumber, OrderIO.SerializeSync(0, 0));
|
|
|
|
if (generateSyncReport)
|
|
using (new PerfSample("sync_report"))
|
|
syncReport.UpdateSyncReport();
|
|
|
|
++NetFrameNumber;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
disposed = true;
|
|
Connection?.Dispose();
|
|
}
|
|
}
|
|
|
|
public class ChatLine
|
|
{
|
|
public readonly Color Color;
|
|
public readonly string Name;
|
|
public readonly string Text;
|
|
public readonly Color TextColor;
|
|
|
|
public ChatLine(string name, Color nameColor, string text, Color textColor)
|
|
{
|
|
Color = nameColor;
|
|
Name = name;
|
|
Text = text;
|
|
TextColor = textColor;
|
|
}
|
|
}
|
|
}
|