Add OrderBuffer and time synchronisation.
This commit is contained in:
136
OpenRA.Game/Server/OrderBuffer.cs
Normal file
136
OpenRA.Game/Server/OrderBuffer.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2022 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.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenRA.Server
|
||||||
|
{
|
||||||
|
public class OrderBuffer
|
||||||
|
{
|
||||||
|
const int NumberOfFrames = 20;
|
||||||
|
const int Interval = 1000;
|
||||||
|
|
||||||
|
// Limit the TickScale to maximum of 10%
|
||||||
|
const float MaxTickScale = 1.1f;
|
||||||
|
|
||||||
|
const int EmptyValue = -1;
|
||||||
|
|
||||||
|
Stopwatch gameTimer;
|
||||||
|
long nextUpdate = 0;
|
||||||
|
|
||||||
|
readonly ConcurrentDictionary<int, long> timestamps = new ConcurrentDictionary<int, long>();
|
||||||
|
readonly ConcurrentDictionary<int, Queue<long>> deltas = new ConcurrentDictionary<int, Queue<long>>();
|
||||||
|
|
||||||
|
int timestep;
|
||||||
|
int ticksPerInterval;
|
||||||
|
int baselinePlayer;
|
||||||
|
List<int> players;
|
||||||
|
|
||||||
|
public void AddOrderTimestamp(int playerIndex)
|
||||||
|
{
|
||||||
|
timestamps[playerIndex] = gameTimer.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
if (timestamps.Values.All(t => t != EmptyValue))
|
||||||
|
{
|
||||||
|
var baseline = timestamps[baselinePlayer];
|
||||||
|
|
||||||
|
foreach (var (p, q) in timestamps)
|
||||||
|
{
|
||||||
|
var dt = baseline - q;
|
||||||
|
|
||||||
|
var playerDeltas = deltas[p];
|
||||||
|
playerDeltas.Enqueue(dt);
|
||||||
|
|
||||||
|
if (playerDeltas.Count > NumberOfFrames)
|
||||||
|
playerDeltas.Dequeue();
|
||||||
|
|
||||||
|
timestamps[p] = EmptyValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(GameSpeed gameSpeed, IEnumerable<int> players)
|
||||||
|
{
|
||||||
|
timestep = gameSpeed.Timestep;
|
||||||
|
ticksPerInterval = Interval / timestep;
|
||||||
|
|
||||||
|
this.players = players.ToList();
|
||||||
|
baselinePlayer = this.players.First();
|
||||||
|
|
||||||
|
foreach (var player in this.players)
|
||||||
|
{
|
||||||
|
timestamps.TryAdd(player, EmptyValue);
|
||||||
|
deltas.TryAdd(player, new Queue<long>());
|
||||||
|
}
|
||||||
|
|
||||||
|
gameTimer = Stopwatch.StartNew();
|
||||||
|
nextUpdate = gameTimer.ElapsedMilliseconds + Interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<(int PlayerIndex, float TickScale)> GetTickScales()
|
||||||
|
{
|
||||||
|
var now = gameTimer.ElapsedMilliseconds;
|
||||||
|
if (now < nextUpdate)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
nextUpdate = now + Interval;
|
||||||
|
|
||||||
|
if (deltas.Values.Any(q => q.Count != NumberOfFrames))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var medians = deltas.Select(d => (PlayerIndex: d.Key, Delta: Median(d.Value.ToArray()))).ToList();
|
||||||
|
|
||||||
|
// We need to check if we have a connection slower than our baseline and then use that as our offset.
|
||||||
|
var minValue = medians.MinBy(p => p.Delta).Delta;
|
||||||
|
var offset = minValue < 0 ? Math.Abs(minValue) : 0;
|
||||||
|
|
||||||
|
foreach (var (playerIndex, delta) in medians)
|
||||||
|
{
|
||||||
|
var deltaPerTick = (delta + offset) / (float)ticksPerInterval;
|
||||||
|
|
||||||
|
var tickScale = (timestep + deltaPerTick) / timestep;
|
||||||
|
|
||||||
|
var adjustedTickScale = tickScale.Clamp(1f, MaxTickScale);
|
||||||
|
|
||||||
|
yield return (playerIndex, adjustedTickScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long Median(long[] a)
|
||||||
|
{
|
||||||
|
Array.Sort(a);
|
||||||
|
var n = a.Length;
|
||||||
|
|
||||||
|
if (n % 2 != 0)
|
||||||
|
return a[n / 2];
|
||||||
|
|
||||||
|
return (a[(n - 1) / 2] + a[n / 2]) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePlayer(int player)
|
||||||
|
{
|
||||||
|
players.Remove(player);
|
||||||
|
if (player == baselinePlayer && players.Count > 0)
|
||||||
|
{
|
||||||
|
var newBaseline = players.First();
|
||||||
|
Interlocked.Exchange(ref baselinePlayer, newBaseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamps.TryRemove(player, out _);
|
||||||
|
deltas.TryRemove(player, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,8 @@ namespace OpenRA.Server
|
|||||||
readonly TypeDictionary serverTraits = new TypeDictionary();
|
readonly TypeDictionary serverTraits = new TypeDictionary();
|
||||||
readonly PlayerDatabase playerDatabase;
|
readonly PlayerDatabase playerDatabase;
|
||||||
|
|
||||||
|
OrderBuffer orderBuffer;
|
||||||
|
|
||||||
volatile ServerState internalState = ServerState.WaitingPlayers;
|
volatile ServerState internalState = ServerState.WaitingPlayers;
|
||||||
|
|
||||||
readonly BlockingCollection<IServerEvent> events = new BlockingCollection<IServerEvent>();
|
readonly BlockingCollection<IServerEvent> events = new BlockingCollection<IServerEvent>();
|
||||||
@@ -361,6 +363,18 @@ namespace OpenRA.Server
|
|||||||
|
|
||||||
foreach (var t in serverTraits.WithInterface<ITick>())
|
foreach (var t in serverTraits.WithInterface<ITick>())
|
||||||
t.Tick(this);
|
t.Tick(this);
|
||||||
|
|
||||||
|
if (State == ServerState.GameStarted)
|
||||||
|
{
|
||||||
|
foreach (var (playerIndex, scale) in orderBuffer.GetTickScales())
|
||||||
|
{
|
||||||
|
var frame = CreateTickScaleFrame(scale);
|
||||||
|
var con = Conns.SingleOrDefault(c => c.PlayerIndex == playerIndex);
|
||||||
|
|
||||||
|
if (con != null && con.Validated)
|
||||||
|
DispatchFrameToClient(con, playerIndex, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (State == ServerState.ShuttingDown)
|
if (State == ServerState.ShuttingDown)
|
||||||
@@ -890,6 +904,8 @@ namespace OpenRA.Server
|
|||||||
frame += OrderLatency;
|
frame += OrderLatency;
|
||||||
DispatchFrameToClient(conn, conn.PlayerIndex, CreateAckFrame(frame, 1));
|
DispatchFrameToClient(conn, conn.PlayerIndex, CreateAckFrame(frame, 1));
|
||||||
|
|
||||||
|
orderBuffer.AddOrderTimestamp(conn.PlayerIndex);
|
||||||
|
|
||||||
// Track the last frame for each client so the disconnect handling can write
|
// Track the last frame for each client so the disconnect handling can write
|
||||||
// an EndOfOrders marker with the correct frame number.
|
// an EndOfOrders marker with the correct frame number.
|
||||||
// TODO: This should be handled by the order buffering system too
|
// TODO: This should be handled by the order buffering system too
|
||||||
@@ -1155,6 +1171,7 @@ namespace OpenRA.Server
|
|||||||
{
|
{
|
||||||
lock (LobbyInfo)
|
lock (LobbyInfo)
|
||||||
{
|
{
|
||||||
|
orderBuffer?.RemovePlayer(toDrop.PlayerIndex);
|
||||||
Conns.Remove(toDrop);
|
Conns.Remove(toDrop);
|
||||||
|
|
||||||
var dropClient = LobbyInfo.Clients.FirstOrDefault(c => c.Index == toDrop.PlayerIndex);
|
var dropClient = LobbyInfo.Clients.FirstOrDefault(c => c.Index == toDrop.PlayerIndex);
|
||||||
@@ -1330,12 +1347,16 @@ namespace OpenRA.Server
|
|||||||
SyncLobbyInfo();
|
SyncLobbyInfo();
|
||||||
State = ServerState.GameStarted;
|
State = ServerState.GameStarted;
|
||||||
|
|
||||||
if (Type != ServerType.Local)
|
|
||||||
{
|
|
||||||
var gameSpeeds = Game.ModData.Manifest.Get<GameSpeeds>();
|
var gameSpeeds = Game.ModData.Manifest.Get<GameSpeeds>();
|
||||||
var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed);
|
var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed);
|
||||||
OrderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency;
|
|
||||||
}
|
var gameSpeed = gameSpeeds.Speeds[gameSpeedName];
|
||||||
|
|
||||||
|
orderBuffer = new OrderBuffer();
|
||||||
|
orderBuffer.Start(gameSpeed, Conns.Where(c => c.Validated).Select(c => c.PlayerIndex));
|
||||||
|
|
||||||
|
if (Type != ServerType.Local)
|
||||||
|
OrderLatency = gameSpeed.OrderLatency;
|
||||||
|
|
||||||
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
|
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
|
||||||
GameSave = new GameSave();
|
GameSave = new GameSave();
|
||||||
|
|||||||
Reference in New Issue
Block a user