From 999af0c05bc5143cc99857f13d00f4de580a326e Mon Sep 17 00:00:00 2001 From: teinarss Date: Sun, 10 Oct 2021 16:14:26 +0200 Subject: [PATCH] Add OrderBuffer and time synchronisation. --- OpenRA.Game/Server/OrderBuffer.cs | 136 ++++++++++++++++++++++++++++++ OpenRA.Game/Server/Server.cs | 31 +++++-- 2 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 OpenRA.Game/Server/OrderBuffer.cs diff --git a/OpenRA.Game/Server/OrderBuffer.cs b/OpenRA.Game/Server/OrderBuffer.cs new file mode 100644 index 0000000000..397a594267 --- /dev/null +++ b/OpenRA.Game/Server/OrderBuffer.cs @@ -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 timestamps = new ConcurrentDictionary(); + readonly ConcurrentDictionary> deltas = new ConcurrentDictionary>(); + + int timestep; + int ticksPerInterval; + int baselinePlayer; + List 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 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()); + } + + 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 _); + } + } +} diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 35fa90b2c2..351a25f016 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -69,6 +69,8 @@ namespace OpenRA.Server readonly TypeDictionary serverTraits = new TypeDictionary(); readonly PlayerDatabase playerDatabase; + OrderBuffer orderBuffer; + volatile ServerState internalState = ServerState.WaitingPlayers; readonly BlockingCollection events = new BlockingCollection(); @@ -361,6 +363,18 @@ namespace OpenRA.Server foreach (var t in serverTraits.WithInterface()) 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) @@ -890,6 +904,8 @@ namespace OpenRA.Server frame += OrderLatency; DispatchFrameToClient(conn, conn.PlayerIndex, CreateAckFrame(frame, 1)); + orderBuffer.AddOrderTimestamp(conn.PlayerIndex); + // Track the last frame for each client so the disconnect handling can write // an EndOfOrders marker with the correct frame number. // TODO: This should be handled by the order buffering system too @@ -1155,6 +1171,7 @@ namespace OpenRA.Server { lock (LobbyInfo) { + orderBuffer?.RemovePlayer(toDrop.PlayerIndex); Conns.Remove(toDrop); var dropClient = LobbyInfo.Clients.FirstOrDefault(c => c.Index == toDrop.PlayerIndex); @@ -1330,12 +1347,16 @@ namespace OpenRA.Server SyncLobbyInfo(); State = ServerState.GameStarted; + var gameSpeeds = Game.ModData.Manifest.Get(); + var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed); + + var gameSpeed = gameSpeeds.Speeds[gameSpeedName]; + + orderBuffer = new OrderBuffer(); + orderBuffer.Start(gameSpeed, Conns.Where(c => c.Validated).Select(c => c.PlayerIndex)); + if (Type != ServerType.Local) - { - var gameSpeeds = Game.ModData.Manifest.Get(); - var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed); - OrderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency; - } + OrderLatency = gameSpeed.OrderLatency; if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled) GameSave = new GameSave();