using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Log; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Client.GameStates { /// internal sealed class GameStateProcessor : IGameStateProcessor { private readonly IGameTiming _timing; private readonly List _stateBuffer = new(); private GameState? _lastFullState; private bool _waitingForFull = true; private int _interpRatio; private GameTick _highestFromSequence; private readonly Dictionary> _lastStateFullRep = new(); /// public int MinBufferSize => Interpolation ? 3 : 2; /// public int TargetBufferSize => MinBufferSize + InterpRatio; /// public int CurrentBufferSize => CalculateBufferSize(_timing.CurTick); /// public bool Interpolation { get; set; } /// public int InterpRatio { get => _interpRatio; set => _interpRatio = value < 0 ? 0 : value; } /// public bool Extrapolation { get; set; } /// public bool Logging { get; set; } public GameTick LastProcessedRealState { get; set; } /// /// Constructs a new instance of . /// /// Timing information of the current state. public GameStateProcessor(IGameTiming timing) { _timing = timing; } /// public void AddNewState(GameState state) { // 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={state.PayloadSize}"); 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={state.PayloadSize}, 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={state.PayloadSize}, 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={state.PayloadSize}, buf={_stateBuffer.Count}"); } /// public bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] 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 = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f; } else { _timing.TickTimingAdjustment = 0f; } if (applyNextState) { DebugTools.Assert(curState!.Extrapolated || curState.FromSequence <= LastProcessedRealState, "Tried to apply a non-extrapolated state that has too high of a FromSequence!"); if (Logging) { Logger.DebugS("net.state", $"Applying State: ext={curState!.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}"); } } var cState = curState!; curState = cState; return applyNextState; } public void UpdateFullRep(GameState state) { // Logger.Debug($"UPDATE FULL REP: {string.Join(", ", state.EntityStates?.Select(e => e.Uid) ?? Enumerable.Empty())}"); if (state.FromSequence == GameTick.Zero) { // Full state. _lastStateFullRep.Clear(); } else { foreach (var deletion in state.EntityDeletions.Span) { _lastStateFullRep.Remove(deletion); } } foreach (var entityState in state.EntityStates.Span) { if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData)) { compData = new Dictionary(); _lastStateFullRep.Add(entityState.Uid, compData); } foreach (var change in entityState.ComponentChanges.Span) { if (change.Deleted) { compData.Remove(change.NetID); } else if (change.State is not null) { compData[change.NetID] = change.State; } } } } private bool CalculateFullState([NotNullWhen(true)] 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, [NotNullWhen(true)] out GameState? curState, out GameState? nextState) { var lastTick = new GameTick(curTick.Value - 1); var nextTick = new GameTick(curTick.Value + 1); curState = null; nextState = null; GameTick? futureStateLowestFromSeq = null; uint lastStateInput = 0; 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; _highestFromSequence = state.FromSequence; } else if (Interpolation && state.ToSequence == nextTick) { nextState = state; if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence) { futureStateLowestFromSeq = state.FromSequence; } } else if (state.ToSequence > curTick) { if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence) { futureStateLowestFromSeq = state.FromSequence; } } else if (state.ToSequence == lastTick) { lastStateInput = state.LastProcessedInput; } else if (state.ToSequence < _highestFromSequence) // remove any old states we find to keep the buffer clean { _stateBuffer.RemoveSwap(i); i--; } } // Make sure we can ACTUALLY apply this state. // Can happen that we can't if there is a hole and we're doing extrapolation. if (curState != null && curState.FromSequence > LastProcessedRealState) curState = null; // can't find current state, but we do have a future state. if (!Extrapolation && curState == null && futureStateLowestFromSeq != null && futureStateLowestFromSeq <= LastProcessedRealState) { //this is not actually extrapolation curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput); return true; // keep moving, we have a future state } // we won't extrapolate, and curState was not found, buffer is empty 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(_highestFromSequence, curTick, lastStateInput); } if (nextState == null && Interpolation) { nextState = ExtrapolateState(_highestFromSequence, nextTick, lastStateInput); } return true; } /// /// Generates a completely fake GameState. /// private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence, uint lastInput) { var state = new GameState(fromSequence, toSequence, lastInput, default, default, default, null); state.Extrapolated = true; return state; } /// public void Reset() { _stateBuffer.Clear(); _lastFullState = null; _waitingForFull = true; } public void MergeImplicitData(Dictionary> data) { foreach (var (uid, compData) in data) { var fullRep = _lastStateFullRep[uid]; foreach (var (netId, compState) in compData) { if (!fullRep.ContainsKey(netId)) { fullRep.Add(netId, compState); } } } } public Dictionary GetLastServerStates(EntityUid entity) { return _lastStateFullRep[entity]; } public bool TryGetLastServerStates(EntityUid entity, [NotNullWhen(true)] out Dictionary? dictionary) { return _lastStateFullRep.TryGetValue(entity, out dictionary); } public int CalculateBufferSize(GameTick fromTick) { return _stateBuffer.Count(s => s.ToSequence >= fromTick); } } }