Disable Extrapolation (#803)

* Split GameState processing logic out of ClientGameStateManager into the new GameStateProcessor class.

* Adds a test fixture for GameStateProcessor with a basic use test.

* More unit tests and added Extrapolated property to GameStateProcessor.
This commit is contained in:
Acruid
2019-05-03 04:26:06 -07:00
committed by Pieter-Jan Briers
parent ca97a46387
commit 39eeab14ea
6 changed files with 556 additions and 220 deletions

View File

@@ -1,11 +1,9 @@
using System.Collections.Generic;
using Robust.Client.Interfaces;
using Robust.Client.Interfaces;
using Robust.Client.Interfaces.GameObjects;
using Robust.Client.Interfaces.GameStates;
using Robust.Shared.GameStates;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Client.Player;
using Robust.Shared.Configuration;
@@ -13,20 +11,14 @@ using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
public class ClientGameStateManager : IClientGameStateManager
{
private readonly List<GameState> _stateBuffer = new List<GameState>();
private GameState _lastFullState;
private bool _waitingForFull = true;
private bool _interpEnabled;
private int _interpRatio;
private bool _logging;
private GameStateProcessor _processor;
[Dependency] private readonly IClientEntityManager _entities;
[Dependency] private readonly IPlayerManager _players;
[Dependency] private readonly IClientNetManager _network;
@@ -36,41 +28,38 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IConfigurationManager _config;
/// <inheritdoc />
public int MinBufferSize => _interpEnabled ? 3 : 2;
public int MinBufferSize => _processor.MinBufferSize;
/// <inheritdoc />
public int TargetBufferSize => MinBufferSize + _interpRatio;
public int TargetBufferSize => _processor.TargetBufferSize;
/// <inheritdoc />
public void Initialize()
{
_processor = new GameStateProcessor(_timing);
_network.RegisterNetMessage<MsgState>(MsgState.NAME, HandleStateMessage);
_network.RegisterNetMessage<MsgStateAck>(MsgStateAck.NAME);
_client.RunLevelChanged += RunLevelChanged;
if(!_config.IsCVarRegistered("net.interp"))
_config.RegisterCVar("net.interp", false, CVar.ARCHIVE, b => _interpEnabled = b);
_config.RegisterCVar("net.interp", false, CVar.ARCHIVE, b => _processor.Interpolation = b);
if (!_config.IsCVarRegistered("net.interp_ratio"))
_config.RegisterCVar("net.interp_ratio", 0, CVar.ARCHIVE, i => _interpRatio = i < 0 ? 0 : i);
_config.RegisterCVar("net.interp_ratio", 0, CVar.ARCHIVE, i => _processor.InterpRatio = i);
if (!_config.IsCVarRegistered("net.logging"))
_config.RegisterCVar("net.logging", false, CVar.ARCHIVE, b => _logging = b);
_config.RegisterCVar("net.logging", false, CVar.ARCHIVE, b => _processor.Logging = b);
_interpEnabled = _config.GetCVar<bool>("net.interp");
_interpRatio = _config.GetCVar<int>("net.interp_ratio");
_interpRatio = _interpRatio < 0 ? 0 : _interpRatio; // min bound, < 0 makes no sense
_logging = _config.GetCVar<bool>("net.logging");
_processor.Interpolation = _config.GetCVar<bool>("net.interp");
_processor.InterpRatio = _config.GetCVar<int>("net.interp_ratio");
_processor.Logging = _config.GetCVar<bool>("net.logging");
}
/// <inheritdoc />
public void Reset()
{
_stateBuffer.Clear();
_lastFullState = null;
_waitingForFull = true;
_processor.Reset();
}
private void RunLevelChanged(object sender, RunLevelChangedEventArgs args)
@@ -78,7 +67,7 @@ namespace Robust.Client.GameStates
if (args.NewLevel == ClientRunLevel.Initialize)
{
// We JUST left a server or the client started up, Reset everything.
_stateBuffer.Clear();
Reset();
}
}
@@ -86,207 +75,19 @@ namespace Robust.Client.GameStates
{
var state = message.State;
_processor.AddNewState(state, message.MsgSize);
// we always ack everything we receive, even if it is late
AckGameState(state.ToSequence);
// any state from tick 0 is a full state, and needs to be handled different
if (state.FromSequence == GameTick.Zero)
{
// this is a newer full state, so discard the older one.
if(_lastFullState == null || (_lastFullState != null && _lastFullState.ToSequence < state.ToSequence))
{
_lastFullState = state;
if(_logging)
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={message.MsgSize}");
return;
}
}
// NOTE: DispatchTick will be modifying CurTick, this is NOT thread safe.
var lastTick = new GameTick(_timing.CurTick.Value - 1);
if (state.ToSequence <= lastTick && !_waitingForFull) // CurTick isn't set properly when WaitingForFull
{
if (_logging)
Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={message.MsgSize}, buf={_stateBuffer.Count}");
return;
}
// lets check for a duplicate state now.
for (var i = 0; i < _stateBuffer.Count; i++)
{
var iState = _stateBuffer[i];
if (state.ToSequence != iState.ToSequence)
continue;
if (iState.Extrapolated)
{
_stateBuffer.RemoveSwap(i); // remove the fake extrapolated state
break; // break from the loop, add the new state normally
}
if (_logging)
Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={message.MsgSize}, buf={_stateBuffer.Count}");
return;
}
// this is a good state that we will be using.
_stateBuffer.Add(state);
if (_logging)
Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={message.MsgSize}, buf={_stateBuffer.Count}");
}
/// <inheritdoc />
public void ApplyGameState()
{
if (CalculateNextStates(_timing.CurTick, out var curState, out var nextState, TargetBufferSize))
{
if (_logging)
Logger.DebugS("net.state", $"Applying State: ext={curState.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
if (!_processor.ProcessTickStates(_timing.CurTick, out var curState, out var nextState))
return;
ApplyGameState(curState, nextState);
}
if (!_waitingForFull)
{
// This will slightly speed up or slow down the client tickrate based on the contents of the buffer.
// CalcNextState should have just cleaned out any old states, so the buffer contains [t-1(last), t+0(cur), t+1(next), t+2, t+3, ..., t+n]
// we can use this info to properly time our tickrate so it does not run fast or slow compared to the server.
_timing.TickTimingAdjustment = (_stateBuffer.Count - (float) TargetBufferSize) * 0.10f;
}
else
{
_timing.TickTimingAdjustment = 0f;
}
}
private bool CalculateNextStates(GameTick curTick, out GameState curState, out GameState nextState, int targetBufferSize)
{
if (_waitingForFull)
{
return CalculateFullState(out curState, out nextState, targetBufferSize);
}
else // this will be how almost all states are calculated
{
return CalculateDeltaState(curTick, out curState, out nextState);
}
}
private bool CalculateFullState(out GameState curState, out GameState nextState, int targetBufferSize)
{
if (_lastFullState != null)
{
if (_logging)
Logger.DebugS("net", $"Resync CurTick to: {_lastFullState.ToSequence}");
var curTick = _timing.CurTick = _lastFullState.ToSequence;
if (_interpEnabled)
{
// look for the next state
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
nextState = null;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
if (state.ToSequence == nextTick)
{
nextState = state;
}
else if (state.ToSequence < lastTick) // remove any old states we find to keep the buffer clean
{
_stateBuffer.RemoveSwap(i);
i--;
}
}
// we let the buffer fill up before starting to tick
if (nextState != null && _stateBuffer.Count >= targetBufferSize)
{
curState = _lastFullState;
_waitingForFull = false;
return true;
}
}
else if (_stateBuffer.Count >= targetBufferSize)
{
curState = _lastFullState;
nextState = default;
_waitingForFull = false;
return true;
}
}
if (_logging)
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{targetBufferSize})");
// waiting for full state or buffer to fill
curState = default;
nextState = default;
return false;
}
private bool CalculateDeltaState(GameTick curTick, out GameState curState, out GameState nextState)
{
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
curState = null;
nextState = null;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
// remember there are no duplicate ToSequence states in the list.
if (state.ToSequence == curTick)
{
curState = state;
}
else if (_interpEnabled && state.ToSequence == nextTick)
{
nextState = state;
}
else if (state.ToSequence < lastTick) // remove any old states we find to keep the buffer clean
{
_stateBuffer.RemoveSwap(i);
i--;
}
}
// we found both the states to interpolate between, this should almost always be true.
if ((_interpEnabled && curState != null) || (!_interpEnabled && curState != null && nextState != null))
return true;
if (curState == null)
{
curState = ExtrapolateState(lastTick, curTick);
}
if (nextState == null && _interpEnabled)
{
nextState = ExtrapolateState(curTick, nextTick);
}
return true;
}
/// <summary>
/// Generates a completely fake GameState.
/// </summary>
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence)
{
var state = new GameState(fromSequence, toSequence, null, null, null, null);
state.Extrapolated = true;
return state;
ApplyGameState(curState, nextState);
}
private void AckGameState(GameTick sequence)

View File

@@ -0,0 +1,268 @@
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
internal class GameStateProcessor : IGameStateProcessor
{
private readonly IGameTiming _timing;
private readonly List<GameState> _stateBuffer = new List<GameState>();
private GameState _lastFullState;
private bool _waitingForFull = true;
private int _interpRatio;
private GameTick _lastProcessedRealState;
/// <inheritdoc />
public int MinBufferSize => Interpolation ? 3 : 2;
/// <inheritdoc />
public int TargetBufferSize => MinBufferSize + InterpRatio;
/// <inheritdoc />
public int CurrentBufferSize => _stateBuffer.Count;
/// <inheritdoc />
public bool Interpolation { get; set; }
/// <inheritdoc />
public int InterpRatio
{
get => _interpRatio;
set => _interpRatio = value < 0 ? 0 : value;
}
/// <inheritdoc />
public bool Extrapolation { get; set; }
/// <inheritdoc />
public bool Logging { get; set; }
/// <summary>
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
public GameStateProcessor(IGameTiming timing)
{
_timing = timing;
}
/// <inheritdoc />
public void AddNewState(GameState state, int stateSize)
{
// any state from tick 0 is a full state, and needs to be handled different
if (state.FromSequence == GameTick.Zero)
{
// this is a newer full state, so discard the older one.
if (_lastFullState == null || (_lastFullState != null && _lastFullState.ToSequence < state.ToSequence))
{
_lastFullState = state;
if (Logging)
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={stateSize}");
return;
}
}
// NOTE: DispatchTick will be modifying CurTick, this is NOT thread safe.
var lastTick = new GameTick(_timing.CurTick.Value - 1);
if (state.ToSequence <= lastTick && !_waitingForFull) // CurTick isn't set properly when WaitingForFull
{
if (Logging)
Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={stateSize}, buf={_stateBuffer.Count}");
return;
}
// lets check for a duplicate state now.
for (var i = 0; i < _stateBuffer.Count; i++)
{
var iState = _stateBuffer[i];
if (state.ToSequence != iState.ToSequence)
continue;
if (Logging)
Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={stateSize}, buf={_stateBuffer.Count}");
return;
}
// this is a good state that we will be using.
_stateBuffer.Add(state);
if (Logging)
Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={stateSize}, buf={_stateBuffer.Count}");
}
/// <inheritdoc />
public bool ProcessTickStates(GameTick curTick, out GameState curState, out GameState nextState)
{
bool applyNextState;
if (_waitingForFull)
{
applyNextState = CalculateFullState(out curState, out nextState, TargetBufferSize);
}
else // this will be how almost all states are calculated
{
applyNextState = CalculateDeltaState(curTick, out curState, out nextState);
}
if (applyNextState && !curState.Extrapolated)
_lastProcessedRealState = curState.ToSequence;
if (!_waitingForFull)
{
if (!applyNextState)
_timing.CurTick = _lastProcessedRealState;
// This will slightly speed up or slow down the client tickrate based on the contents of the buffer.
// CalcNextState should have just cleaned out any old states, so the buffer contains [t-1(last), t+0(cur), t+1(next), t+2, t+3, ..., t+n]
// we can use this info to properly time our tickrate so it does not run fast or slow compared to the server.
_timing.TickTimingAdjustment = (_stateBuffer.Count - (float)TargetBufferSize) * 0.10f;
}
else
{
_timing.TickTimingAdjustment = 0f;
}
if (Logging && applyNextState)
Logger.DebugS("net.state", $"Applying State: ext={curState.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
return applyNextState;
}
private bool CalculateFullState(out GameState curState, out GameState nextState, int targetBufferSize)
{
if (_lastFullState != null)
{
if (Logging)
Logger.DebugS("net", $"Resync CurTick to: {_lastFullState.ToSequence}");
var curTick = _timing.CurTick = _lastFullState.ToSequence;
if (Interpolation)
{
// look for the next state
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
nextState = null;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
if (state.ToSequence == nextTick)
{
nextState = state;
}
else if (state.ToSequence < lastTick) // remove any old states we find to keep the buffer clean
{
_stateBuffer.RemoveSwap(i);
i--;
}
}
// we let the buffer fill up before starting to tick
if (nextState != null && _stateBuffer.Count >= targetBufferSize)
{
curState = _lastFullState;
_waitingForFull = false;
return true;
}
}
else if (_stateBuffer.Count >= targetBufferSize)
{
curState = _lastFullState;
nextState = default;
_waitingForFull = false;
return true;
}
}
if (Logging)
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{targetBufferSize})");
// waiting for full state or buffer to fill
curState = default;
nextState = default;
return false;
}
private bool CalculateDeltaState(GameTick curTick, out GameState curState, out GameState nextState)
{
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
curState = null;
nextState = null;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
// remember there are no duplicate ToSequence states in the list.
if (state.ToSequence == curTick)
{
curState = state;
}
else if (Interpolation && state.ToSequence == nextTick)
{
nextState = state;
}
else if (state.ToSequence < lastTick) // remove any old states we find to keep the buffer clean
{
_stateBuffer.RemoveSwap(i);
i--;
}
}
// we won't extrapolate, and curState was not found.
if (!Extrapolation && curState == null)
return false;
// we found both the states to interpolate between, this should almost always be true.
if (Interpolation && curState != null)
return true;
if (!Interpolation && curState != null && nextState != null)
return true;
if (curState == null)
{
curState = ExtrapolateState(lastTick, curTick);
}
if (nextState == null && Interpolation)
{
nextState = ExtrapolateState(curTick, nextTick);
}
return true;
}
/// <summary>
/// Generates a completely fake GameState.
/// </summary>
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence)
{
var state = new GameState(fromSequence, toSequence, null, null, null, null);
state.Extrapolated = true;
return state;
}
/// <inheritdoc />
public void Reset()
{
_stateBuffer.Clear();
_lastFullState = null;
_waitingForFull = true;
}
}
}

View File

@@ -0,0 +1,76 @@
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
{
/// <summary>
/// Holds a collection of game states and calculates which ones to apply at a given game tick.
/// </summary>
internal interface IGameStateProcessor
{
/// <summary>
/// Minimum number of states needed in the buffer for everything to work.
/// </summary>
/// <remarks>
/// With interpolation enabled minimum is 3 states in buffer for the system to work (last, cur, next).
/// Without interpolation enabled minimum is 2 states in buffer for the system to work (last, cur).
/// </remarks>
int MinBufferSize { get; }
/// <summary>
/// The number of states the system is trying to keep in the buffer. This will always
/// be greater or equal to <see cref="MinBufferSize"/>.
/// </summary>
int TargetBufferSize { get; }
/// <summary>
/// Number of game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
/// <summary>
/// Is frame interpolation turned on?
/// </summary>
bool Interpolation { get; set; }
/// <summary>
/// The target number of states to keep in the buffer for network smoothing.
/// </summary>
/// <remarks>
/// For Lan, set this to 0. For Excellent net conditions, set this to 1. For normal network conditions,
/// set this to 2. For worse conditions, set it higher.
/// </remarks>
int InterpRatio { get; set; }
/// <summary>
/// If the client clock runs ahead of the server and the buffer gets emptied, should fake extrapolated states be generated?
/// </summary>
bool Extrapolation { get; set; }
/// <summary>
/// Is debug logging enabled? This will dump debug info about every state to the log.
/// </summary>
bool Logging { get; set; }
/// <summary>
/// Adds a new state into the processor. These are usually from networking or replays.
/// </summary>
/// <param name="state">Newly received state.</param>
/// <param name="stateSize">Optionally provide the size in bytes of this new state. This is strictly for debug logging.</param>
void AddNewState(GameState state, int stateSize);
/// <summary>
/// Calculates the current and next state to apply for a given game tick.
/// </summary>
/// <param name="curTick">Tick to get the states for.</param>
/// <param name="curState">Current state for the given tick. This can be null.</param>
/// <param name="nextState">Current state for tick + 1. This can be null.</param>
/// <returns>Was the function able to correctly calculate the states for the given tick?</returns>
bool ProcessTickStates(GameTick curTick, out GameState curState, out GameState nextState);
/// <summary>
/// Resets the processor back to its initial state.
/// </summary>
void Reset();
}
}

View File

@@ -140,6 +140,8 @@
<Compile Include="Console\Commands\QuitCommand.cs" />
<Compile Include="Console\Commands\Debug.cs" />
<Compile Include="GameStates\ClientGameStateManager.cs" />
<Compile Include="GameStates\GameStateProcessor.cs" />
<Compile Include="GameStates\IGameStateProcessor.cs" />
<Compile Include="GameStates\NetGraphOverlay.cs" />
<Compile Include="GameStates\NetInterpOverlay.cs" />
<Compile Include="Graphics\CanvasLayers.cs" />

View File

@@ -0,0 +1,188 @@
using Moq;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Shared.GameStates;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Client.GameStates
{
[TestFixture, Parallelizable, TestOf(typeof(GameStateProcessor))]
class GameStateProcessor_Tests
{
[Test]
public void FillBufferBlocksProcessing()
{
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
var processor = new GameStateProcessor(timing);
processor.AddNewState(GameStateFactory(0, 1), 0);
processor.AddNewState(GameStateFactory(1, 2), 0); // buffer is at 2/3, so processing should be blocked
// calculate states for first tick
timing.CurTick = new GameTick(3);
var result = processor.ProcessTickStates(new GameTick(1), out _, out _);
Assert.That(result, Is.False);
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
}
[Test]
public void FillBufferAndCalculateFirstState()
{
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
var processor = new GameStateProcessor(timing);
processor.AddNewState(GameStateFactory(0, 1), 0);
processor.AddNewState(GameStateFactory(1, 2), 0);
processor.AddNewState(GameStateFactory(2, 3), 0); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.CurTick = new GameTick(1);
var result = processor.ProcessTickStates(new GameTick(1), out var curState, out var nextState);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState.Extrapolated, Is.False);
Assert.That(curState.ToSequence.Value, Is.EqualTo(1));
Assert.That(nextState, Is.Null);
}
/// <summary>
/// When a full state is in the queue (fromSequence = 0), it will modify CurTick to the states' toSequence,
/// then return the state as curState.
/// </summary>
[Test]
public void FullStateResyncsCurTick()
{
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
var processor = new GameStateProcessor(timing);
processor.AddNewState(GameStateFactory(0, 1), 0);
processor.AddNewState(GameStateFactory(1, 2), 0);
processor.AddNewState(GameStateFactory(2, 3), 0); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.CurTick = new GameTick(3);
processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
}
[Test]
public void StatesReceivedPastCurTickAreDropped()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.CurTick = new GameTick(5); // current clock is ahead of server
processor.AddNewState(GameStateFactory(3, 4), 0); // received a late state
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(result, Is.False);
}
/// <summary>
/// The server fell behind the client, so the client clock is now ahead of the incoming states.
/// Without extrapolation, processing blocks.
/// </summary>
[Test]
public void ServerLagsWithoutExtrapolation()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.CurTick = new GameTick(5); // current clock is ahead of server
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(result, Is.False);
}
/// <summary>
/// When processing is blocked because the client is ahead of the server, reset CurTick to the last
/// received state.
/// </summary>
[Test]
public void ServerLagsWithoutExtrapolationSetsCurTick()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.CurTick = new GameTick(4); // current clock is ahead of server (server=1, client=5)
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(result, Is.False);
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
}
/// <summary>
/// The server fell behind the client, so the client clock is now ahead of the incoming states.
/// With extrapolation, processing returns a fake extrapolated state for the current tick.
/// </summary>
[Test]
public void ServerLagsWithExtrapolation()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = true;
// a few moments later...
timing.CurTick = new GameTick(5); // current clock is ahead of server
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out var nextState);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState.Extrapolated, Is.True);
Assert.That(curState.ToSequence.Value, Is.EqualTo(5));
Assert.That(nextState, Is.Null);
}
/// <summary>
/// Creates a new empty GameState with the given to and from properties.
/// </summary>
private static GameState GameStateFactory(uint from, uint to)
{
return new GameState(new GameTick(from), new GameTick(to), null, null, null, null);
}
/// <summary>
/// Creates a new GameTiming and GameStateProcessor, fills the processor with enough states, and calculate the first tick.
/// CurTick = 1, states 1 - 3 are in the buffer.
/// </summary>
private static (IGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
{
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
timingMock.SetupProperty(p => p.TickTimingAdjustment);
var timing = timingMock.Object;
var processor = new GameStateProcessor(timing);
processor.AddNewState(GameStateFactory(0, 1), 0);
processor.AddNewState(GameStateFactory(1, 2), 0);
processor.AddNewState(GameStateFactory(2, 3), 0); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.CurTick = new GameTick(1);
processor.ProcessTickStates(timing.CurTick, out _, out _);
return (timing, processor);
}
}
}

View File

@@ -92,6 +92,7 @@
<Compile Include="ApproxEqualityConstraint.cs" />
<Compile Include="Client\GameObjects\Components\Transform_Test.cs" />
<Compile Include="Client\GameControllerProxyDummy.cs" />
<Compile Include="Client\GameStates\GameStateProcessor_Tests.cs" />
<Compile Include="Client\Graphics\StyleBoxTest.cs" />
<Compile Include="Client\Graphics\TextureLoadParametersTest.cs" />
<Compile Include="Client\UserInterface\Controls\BoxContainerTest.cs" />