mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
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:
committed by
Pieter-Jan Briers
parent
ca97a46387
commit
39eeab14ea
@@ -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)
|
||||
|
||||
268
Robust.Client/GameStates/GameStateProcessor.cs
Normal file
268
Robust.Client/GameStates/GameStateProcessor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Robust.Client/GameStates/IGameStateProcessor.cs
Normal file
76
Robust.Client/GameStates/IGameStateProcessor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
188
Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs
Normal file
188
Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user