Revert "PVS & client state handling changes" (#3151)

This commit is contained in:
Leon Friedrich
2022-08-21 07:27:16 +12:00
committed by GitHub
parent c5ba8b75c8
commit 9cd8adae93
43 changed files with 1107 additions and 1518 deletions

View File

@@ -1,6 +1,5 @@
using System;
using System.Threading;
using Robust.Client.Timing;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.IoC;
@@ -14,7 +13,7 @@ namespace Robust.Client
{
private IGameLoop? _mainLoop;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
private static bool _hasStarted;

View File

@@ -517,7 +517,7 @@ namespace Robust.Client
using (_prof.Group("Entity"))
{
// The last real tick is the current tick! This way we won't be in "prediction" mode.
_gameTiming.LastRealTick = _gameTiming.LastProcessedTick = _gameTiming.CurTick;
_gameTiming.LastRealTick = _gameTiming.CurTick;
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
}
}

View File

@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using Prometheus;
using Robust.Client.GameStates;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -18,7 +19,8 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
@@ -45,22 +47,6 @@ namespace Robust.Client.GameObjects
base.StartEntity(entity);
}
/// <inheritdoc />
public override void Dirty(EntityUid uid)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(uid);
}
/// <inheritdoc />
public override void Dirty(Component component)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(component);
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;
@@ -81,7 +67,7 @@ namespace Robust.Client.GameObjects
{
using (histogram?.WithLabels("EntityNet").NewTimer())
{
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick)
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
{
var (_, msg) = _queue.Take();
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
@@ -117,7 +103,7 @@ namespace Robust.Client.GameObjects
private void HandleEntityNetworkMessage(MsgEntity message)
{
if (message.SourceTick <= _gameTiming.LastRealTick)
if (message.SourceTick <= _gameStateManager.CurServerTick)
{
DispatchMsgEntity(message);
return;

View File

@@ -1470,9 +1470,11 @@ namespace Robust.Client.GameObjects
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState is not SpriteComponentState thestate)
if (curState == null)
return;
var thestate = (SpriteComponentState)curState;
Visible = thestate.Visible;
DrawDepth = thestate.DrawDepth;
scale = thestate.Scale;

View File

@@ -96,21 +96,16 @@ namespace Robust.Client.GameObjects
public override void FrameUpdate(float frameTime)
{
var spriteQuery = GetEntityQuery<SpriteComponent>();
var metaQuery = GetEntityQuery<MetaDataComponent>();
while (_queuedUpdates.TryDequeue(out var appearance))
{
if (appearance.Deleted)
continue;
UnmarkDirty(appearance);
// If the entity is no longer within the clients PVS, don't bother updating.
if ((metaQuery.GetComponent(appearance.Owner).Flags & MetaDataFlags.Detached) != 0)
continue;
// Sprite comp is allowed to be null, so that things like spriteless point-lights can use this system
spriteQuery.TryGetComponent(appearance.Owner, out var sprite);
OnChangeData(appearance.Owner, sprite, appearance);
UnmarkDirty(appearance);
}
}

View File

@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.ObjectPool;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
@@ -16,7 +15,7 @@ namespace Robust.Client.GameStates;
/// </summary>
internal sealed class ClientDirtySystem : EntitySystem
{
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly Dictionary<GameTick, HashSet<EntityUid>> _dirtyEntities = new();
@@ -50,14 +49,14 @@ internal sealed class ClientDirtySystem : EntitySystem
_dirtyEntities.Clear();
}
public IEnumerable<EntityUid> GetDirtyEntities()
public IEnumerable<EntityUid> GetDirtyEntities(GameTick currentTick)
{
_dirty.Clear();
// This is just to avoid collection being modified during iteration unfortunately.
foreach (var (tick, dirty) in _dirtyEntities)
{
if (tick < _timing.LastRealTick) continue;
if (tick < currentTick) continue;
foreach (var ent in dirty)
{
_dirty.Add(ent);

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Timing;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -14,144 +12,154 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor
{
private readonly IClientGameTiming _timing;
private readonly IGameTiming _timing;
private readonly List<GameState> _stateBuffer = new();
private GameState? _lastFullState;
private bool _waitingForFull = true;
private int _interpRatio;
private GameTick _highestFromSequence;
private readonly Dictionary<GameTick, List<EntityUid>> _pvsDetachMessages = new();
public GameState? LastFullState { get; private set; }
public bool WaitingForFull => LastFullStateRequested.HasValue;
public GameTick? LastFullStateRequested
{
get => _lastFullStateRequested;
set
{
_lastFullStateRequested = value;
LastFullState = null;
}
}
public GameTick? _lastFullStateRequested = GameTick.Zero;
private int _bufferSize;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
/// </summary>
private readonly Dictionary<EntityUid, Dictionary<ushort, ComponentState>> _lastStateFullRep
private readonly Dictionary<EntityUid, Dictionary<uint, ComponentState>> _lastStateFullRep
= new();
/// <inheritdoc />
public int MinBufferSize => Interpolation ? 2 : 1;
public int MinBufferSize => Interpolation ? 3 : 2;
/// <inheritdoc />
public int TargetBufferSize => MinBufferSize + BufferSize;
public int TargetBufferSize => MinBufferSize + InterpRatio;
/// <inheritdoc />
public int CurrentBufferSize => CalculateBufferSize(_timing.CurTick);
/// <inheritdoc />
public bool Interpolation { get; set; }
/// <inheritdoc />
public int BufferSize
public int InterpRatio
{
get => _bufferSize;
set => _bufferSize = value < 0 ? 0 : value;
get => _interpRatio;
set => _interpRatio = value < 0 ? 0 : value;
}
/// <inheritdoc />
public bool Extrapolation { get; set; }
/// <inheritdoc />
public bool Logging { get; set; }
public GameTick LastProcessedRealState { get; set; }
/// <summary>
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
public GameStateProcessor(IClientGameTiming timing)
public GameStateProcessor(IGameTiming timing)
{
_timing = timing;
}
/// <inheritdoc />
public bool AddNewState(GameState state)
public void AddNewState(GameState state)
{
// Check for old states.
if (state.ToSequence <= _timing.LastRealTick)
// any state from tick 0 is a full state, and needs to be handled different
if (state.FromSequence == GameTick.Zero)
{
if (Logging)
Logger.DebugS("net.state", $"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
// this is a newer full state, so discard the older one.
if (_lastFullState == null || (_lastFullState != null && _lastFullState.ToSequence < state.ToSequence))
{
_lastFullState = state;
return false;
if (Logging)
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return;
}
}
// Check for a duplicate states.
foreach (var bufferState in _stateBuffer)
// 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 (state.ToSequence != bufferState.ToSequence)
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: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
// Are we expecting a full state?
if (!WaitingForFull)
{
// This is a good state that we will be using.
_stateBuffer.Add(state);
if (Logging)
Logger.DebugS("net.state", $"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return true;
return;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
{
LastFullState = state;
if (Logging)
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
if (LastFullState == null || state.ToSequence <= LastFullState.ToSequence)
return false;
// this is a good state that we will be using.
_stateBuffer.Add(state);
return true;
if (Logging)
Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
}
/// <summary>
/// Attempts to get the current and next states to apply.
/// </summary>
/// <remarks>
/// If the processor is not currently waiting for a full state, the states to apply depends on <see
/// cref="IGameTiming.LastProcessedTick"/>.
/// </remarks>
/// <returns>Returns true if the states should be applied.</returns>
public bool TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
/// <inheritdoc />
public bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
{
var applyNextState = WaitingForFull
? TryGetFullState(out curState, out nextState)
: TryGetDeltaState(out curState, out nextState);
if (curState != null)
bool applyNextState;
if (_waitingForFull)
{
DebugTools.Assert(curState.FromSequence <= curState.ToSequence,
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: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
{
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)
{
// Note: the most recently received server state currently doesn't include pvs-leave messages (detaching
// transform to null-space). This is because a client should never predict an entity being moved back from
// null-space, so there should be no need to reset it back there.
// Logger.Debug($"UPDATE FULL REP: {string.Join(", ", state.EntityStates?.Select(e => e.Uid) ?? Enumerable.Empty<EntityUid>())}");
if (state.FromSequence == GameTick.Zero)
{
@@ -170,7 +178,7 @@ namespace Robust.Client.GameStates
{
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
{
compData = new Dictionary<ushort, ComponentState>();
compData = new Dictionary<uint, ComponentState>();
_lastStateFullRep.Add(entityState.Uid, compData);
}
@@ -188,138 +196,167 @@ namespace Robust.Client.GameStates
}
}
private bool TryGetFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
private bool CalculateFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState, int targetBufferSize)
{
nextState = null;
curState = null;
if (LastFullState == null)
return false;
// remove any old states we find to keep the buffer clean
// also look for the next state if we are interpolating.
var nextTick = LastFullState.ToSequence + 1;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
if (state.ToSequence < LastFullState.ToSequence)
{
_stateBuffer.RemoveSwap(i);
i--;
}
else if (Interpolation && state.ToSequence == nextTick)
{
nextState = state;
}
}
// we let the buffer fill up before starting to tick
if (_stateBuffer.Count >= TargetBufferSize && (!Interpolation || nextState != null))
if (_lastFullState != null)
{
if (Logging)
Logger.DebugS("net", $"Resync CurTick to: {LastFullState.ToSequence}");
Logger.DebugS("net", $"Resync CurTick to: {_lastFullState.ToSequence}");
curState = LastFullState;
return true;
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;
}
}
// waiting for buffer to fill
if (Logging)
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{TargetBufferSize})");
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{targetBufferSize})");
// waiting for full state or buffer to fill
curState = default;
nextState = default;
return false;
}
internal void AddLeavePvsMessage(MsgStateLeavePvs message)
private bool CalculateDeltaState(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
{
// Late message may still need to be processed,
DebugTools.Assert(message.Entities.Count > 0);
_pvsDetachMessages.TryAdd(message.Tick, message.Entities);
}
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
public List<(GameTick Tick, List<EntityUid> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
{
var result = new List<(GameTick Tick, List<EntityUid> Entities)>();
foreach (var (tick, entities) in _pvsDetachMessages)
{
if (tick > toTick)
continue;
if (budget >= entities.Count)
{
budget -= entities.Count;
_pvsDetachMessages.Remove(tick);
result.Add((tick, entities));
continue;
}
var index = entities.Count - budget;
result.Add((tick, entities.GetRange(index, budget)));
entities.RemoveRange(index, budget);
break;
}
return result;
}
private bool TryGetDeltaState(out GameState? curState, out GameState? nextState)
{
curState = null;
nextState = null;
var targetCurTick = _timing.LastProcessedTick + 1;
var targetNextTick = _timing.LastProcessedTick + 2;
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 == targetCurTick && state.FromSequence <= _timing.LastRealTick)
if (state.ToSequence == curTick)
{
curState = state;
continue;
_highestFromSequence = state.FromSequence;
}
if (Interpolation && state.ToSequence == targetNextTick)
else if (Interpolation && state.ToSequence == nextTick)
{
nextState = state;
if (state.ToSequence > targetCurTick && (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence))
{
futureStateLowestFromSeq = state.FromSequence;
continue;
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
{
futureStateLowestFromSeq = state.FromSequence;
}
}
// remove any old states we find to keep the buffer clean
if (state.ToSequence <= _timing.LastRealTick)
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--;
}
}
// Even if we can't find current state, maybe we have a future state?
return curState != null || (futureStateLowestFromSeq != null && futureStateLowestFromSeq <= _timing.LastRealTick);
// 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;
}
/// <summary>
/// Generates a completely fake GameState.
/// </summary>
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;
}
/// <inheritdoc />
public void Reset()
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = GameTick.Zero;
_lastFullState = null;
_waitingForFull = true;
}
public void RequestFullState()
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = _timing.LastRealTick;
}
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data)
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data)
{
foreach (var (uid, compData) in data)
{
@@ -335,39 +372,20 @@ namespace Robust.Client.GameStates
}
}
public Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity)
public Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity)
{
return _lastStateFullRep[entity];
}
public bool TryGetLastServerStates(EntityUid entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary)
{
return _lastStateFullRep.TryGetValue(entity, out dictionary);
}
public int CalculateBufferSize(GameTick fromTick)
{
bool foundState;
var nextTick = fromTick;
do
{
foundState = false;
foreach (var state in _stateBuffer)
{
if (state.ToSequence > nextTick && state.FromSequence <= nextTick)
{
foundState = true;
nextTick += 1;
}
}
}
while (foundState);
return (int) (nextTick.Value - fromTick.Value);
return _stateBuffer.Count(s => s.ToSequence >= fromTick);
}
}
}

View File

@@ -1,8 +1,7 @@
using System;
using System;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
@@ -28,10 +27,18 @@ namespace Robust.Client.GameStates
int TargetBufferSize { get; }
/// <summary>
/// Number of applicable game states currently in the state buffer.
/// Number of game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
/// <summary>
/// The current tick of the last server game state applied.
/// </summary>
/// <remarks>
/// Use this to synchronize server-sent simulation events with the client's game loop.
/// </remarks>
GameTick CurServerTick { get; }
/// <summary>
/// If the buffer size is this many states larger than the target buffer size,
/// apply the overflow of states in a single tick.
@@ -50,11 +57,6 @@ namespace Robust.Client.GameStates
/// </summary>
event Action<GameStateAppliedArgs> GameStateApplied;
/// <summary>
/// This is invoked whenever a pvs-leave message is received.
/// </summary>
public event Action<MsgStateLeavePvs>? PvsLeave;
/// <summary>
/// One time initialization of the service.
/// </summary>
@@ -76,11 +78,6 @@ namespace Robust.Client.GameStates
/// <param name="message">Message being dispatched.</param>
void InputCommandDispatched(FullInputCmdMessage message);
/// <summary>
/// Requests a full state from the server. This should override even implicit entity data.
/// </summary>
public void RequestFullState();
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -17,8 +17,8 @@ namespace Robust.Client.GameStates
/// Minimum number of states needed in the buffer for everything to work.
/// </summary>
/// <remarks>
/// With interpolation enabled minimum is 2 states in buffer for the system to work (cur, next).
/// Without interpolation enabled minimum is 2 states in buffer for the system to work (cur).
/// 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; }
@@ -28,6 +28,12 @@ namespace Robust.Client.GameStates
/// </summary>
int TargetBufferSize { get; }
/// <summary>
/// Number of game states currently in the state buffer.
/// </summary>
/// <seealso cref="CalculateBufferSize"/>
int CurrentBufferSize { get; }
/// <summary>
/// Is frame interpolation turned on?
/// </summary>
@@ -40,22 +46,29 @@ namespace Robust.Client.GameStates
/// 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 BufferSize { get; set; }
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>
/// The last REAL server tick that has been processed.
/// i.e. not incremented on extrapolation.
/// </summary>
GameTick LastProcessedRealState { 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>
/// <returns>Returns true if the state was accepted and should be acknowledged</returns>
bool AddNewState(GameState state);
//> usually from replays
//replays when
void AddNewState(GameState state);
/// <summary>
/// Calculates the current and next state to apply for a given game tick.
@@ -64,7 +77,7 @@ namespace Robust.Client.GameStates
/// <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 TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState);
bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState);
/// <summary>
/// Resets the processor back to its initial state.
@@ -83,22 +96,21 @@ namespace Robust.Client.GameStates
/// The data to merge.
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
/// </param>
void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data);
void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data);
/// <summary>
/// Get the last state data from the server for an entity.
/// </summary>
/// <returns>Dictionary (net ID -> ComponentState)</returns>
Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity);
Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity);
/// <summary>
/// Calculate the number of applicable states in the game state buffer from a given tick.
/// This includes only applicable states. If there is a gap, future buffers are not included.
/// Calculate the size of the game state buffer from a given tick.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int CalculateBufferSize(GameTick fromTick);
bool TryGetLastServerStates(EntityUid entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary);
}
}

View File

@@ -1,17 +1,17 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.Timing;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -21,20 +21,21 @@ namespace Robust.Client.GameStates
/// </summary>
sealed class NetEntityOverlay : Overlay
{
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
private const uint TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
private const int _maxEnts = 128; // maximum number of entities to track.
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
private readonly Font _font;
private readonly int _lineHeight;
private readonly Dictionary<EntityUid, NetEntData> _netEnts = new();
private readonly List<NetEntity> _netEnts = new();
public NetEntityOverlay()
{
@@ -44,58 +45,87 @@ namespace Robust.Client.GameStates
_lineHeight = _font.GetLineHeight(1);
_gameStateManager.GameStateApplied += HandleGameStateApplied;
_gameStateManager.PvsLeave += OnPvsLeave;
}
private void OnPvsLeave(MsgStateLeavePvs msg)
{
if (msg.Tick.Value + TrafficHistorySize < _gameTiming.LastRealTick.Value)
return;
foreach (var uid in msg.Entities)
{
if (!_netEnts.TryGetValue(uid, out var netEnt))
continue;
if (netEnt.LastUpdate < msg.Tick)
{
netEnt.InPVS = false;
netEnt.LastUpdate = msg.Tick;
}
netEnt.Traffic.Add(msg.Tick, NetEntData.EntState.PvsLeave);
}
}
private void HandleGameStateApplied(GameStateAppliedArgs args)
{
var gameState = args.AppliedState;
if (!gameState.EntityStates.HasContents)
if(_gameTiming.InPrediction) // we only care about real server states.
return;
foreach (var entityState in gameState.EntityStates.Span)
// Shift traffic history down one
for (var i = 0; i < _netEnts.Count; i++)
{
if (!_netEnts.TryGetValue(entityState.Uid, out var netEnt))
var traffic = _netEnts[i].Traffic;
for (int j = 1; j < TrafficHistorySize; j++)
{
if (_netEnts.Count >= _maxEnts)
traffic[j - 1] = traffic[j];
}
traffic[^1] = 0;
}
var gameState = args.AppliedState;
if(gameState.EntityStates.HasContents)
{
// Loop over every entity that gets updated this state and record the traffic
foreach (var entityState in gameState.EntityStates.Span)
{
var newEnt = true;
for(var i=0;i<_netEnts.Count;i++)
{
var netEnt = _netEnts[i];
if (netEnt.Id != entityState.Uid)
continue;
//TODO: calculate size of state and record it here.
netEnt.Traffic[^1] = 1;
netEnt.LastUpdate = gameState.ToSequence;
newEnt = false;
_netEnts[i] = netEnt; // copy struct back
break;
}
if (!newEnt)
continue;
_netEnts[entityState.Uid] = netEnt = new();
var newNetEnt = new NetEntity(entityState.Uid);
newNetEnt.Traffic[^1] = 1;
newNetEnt.LastUpdate = gameState.ToSequence;
_netEnts.Add(newNetEnt);
}
}
if (!netEnt.InPVS && netEnt.LastUpdate < gameState.ToSequence)
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange*2, pvsRange*2));
int timeout = _gameTiming.TickRate * 3;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if(_entityManager.EntityExists(netEnt.Id))
{
netEnt.InPVS = true;
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.PvsEnter);
//TODO: Whoever is working on PVS remake, change the InPVS detection.
var uid = netEnt.Id;
var position = _entityManager.GetComponent<TransformComponent>(uid).MapPosition;
netEnt.InPVS = !pvsEnabled || (pvsBox.Contains(position.Position) && position.MapId == pvsCenter.MapId);
_netEnts[i] = netEnt; // copy struct back
continue;
}
else
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.Data);
if (netEnt.LastUpdate < gameState.ToSequence)
netEnt.LastUpdate = gameState.ToSequence;
netEnt.Exists = false;
if (netEnt.LastUpdate.Value + timeout < _gameTiming.LastRealTick.Value)
{
_netEnts.RemoveAt(i);
i--;
continue;
}
//TODO: calculate size of state and record it here.
_netEnts[i] = netEnt; // copy struct back
}
}
@@ -109,128 +139,145 @@ namespace Robust.Client.GameStates
case OverlaySpace.ScreenSpace:
DrawScreen(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args);
break;
}
}
private void DrawWorld(in OverlayDrawArgs args)
{
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
if(!pvsEnabled)
return;
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange * 2, pvsRange * 2));
var worldHandle = args.WorldHandle;
worldHandle.DrawRect(pvsBox, Color.Red, false);
}
private void DrawScreen(in OverlayDrawArgs args)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var screenHandle = args.ScreenHandle;
int i = 0;
foreach (var (uid, netEnt) in _netEnts)
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
var uid = netEnt.Id;
if (!_entityManager.EntityExists(uid))
{
_netEnts.Remove(uid);
_netEnts.RemoveSwap(i);
i--;
continue;
}
var xPos = 100;
var yPos = 10 + _lineHeight * i++;
var name = $"({uid}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
var color = netEnt.TextColor(_gameTiming);
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
var color = CalcTextColor(ref netEnt);
screenHandle.DrawString(_font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, netEnt, xPos, yPos);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
}
}
private void DrawTrafficBox(DrawingHandleScreen handle, NetEntData netEntity, int x, int y)
private void DrawTrafficBox(DrawingHandleScreen handle, ref NetEntity netEntity, int x, int y)
{
handle.DrawRect(UIBox2.FromDimensions(x + 1, y, TrafficHistorySize + 1, _lineHeight), new Color(32, 32, 32, 128));
handle.DrawRect(UIBox2.FromDimensions(x+1, y, TrafficHistorySize + 1, _lineHeight), new Color(32, 32, 32, 128));
handle.DrawRect(UIBox2.FromDimensions(x, y, TrafficHistorySize + 2, _lineHeight), Color.Gray.WithAlpha(0.15f), false);
var traffic = netEntity.Traffic;
//TODO: Local peak size, actually scale the peaks
for (uint i = 1; i <= TrafficHistorySize; i++)
for (int i = 0; i < TrafficHistorySize; i++)
{
if (!traffic.TryGetValue(_gameTiming.LastRealTick + (i - TrafficHistorySize), out var tickData))
if(traffic[i] == 0)
continue;
var color = tickData switch
{
NetEntData.EntState.Data => Color.Green,
NetEntData.EntState.PvsLeave => Color.Orange,
NetEntData.EntState.PvsEnter => Color.Cyan,
_ => throw new Exception("Unexpected value")
};
var xPos = x + 1 + i;
var yPosA = y + 1;
var yPosB = yPosA + _lineHeight - 1;
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), color);
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), Color.Green);
}
}
private Color CalcTextColor(ref NetEntity ent)
{
if(!ent.Exists)
return Color.Gray; // Entity is deleted, will be removed from list soon.
if(!ent.InPVS)
return Color.Red; // Entity still exists outside PVS, but not updated anymore.
if(_gameTiming.LastRealTick < ent.LastUpdate + _gameTiming.TickRate)
return Color.Blue; //Entity in PVS generating ongoing traffic.
return Color.Green; // Entity in PVS, but not updated recently.
}
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
_gameStateManager.PvsLeave -= OnPvsLeave;
base.DisposeBehavior();
}
private sealed class NetEntData
private struct NetEntity
{
public GameTick LastUpdate = GameTick.Zero;
public readonly OverflowDictionary<GameTick, EntState> Traffic = new((int) TrafficHistorySize);
public bool Exists = true;
public bool InPVS = true;
public GameTick LastUpdate;
public readonly EntityUid Id;
public readonly int[] Traffic;
public bool Exists;
public bool InPVS;
public Color TextColor(IClientGameTiming timing)
public NetEntity(EntityUid id)
{
if (!InPVS)
return Color.Orange; // Entity still exists outside PVS, but not updated anymore.
if (timing.LastRealTick < LastUpdate + timing.TickRate)
return Color.Blue; //Entity in PVS generating ongoing traffic.
return Color.Green; // Entity in PVS, but not updated recently.
}
public enum EntState : byte
{
Nothing = 0,
Data = 1,
PvsLeave = 2,
PvsEnter = 3
LastUpdate = GameTick.Zero;
Id = id;
Traffic = new int[TrafficHistorySize];
Exists = true;
InPVS = true;
}
}
private sealed class NetEntityReportCommand : IConsoleCommand
{
public string Command => "net_entityreport";
public string Help => "net_entityreport";
public string Help => "net_entityreport <0|1>";
public string Description => "Toggles the net entity report panel.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
return;
}
if (!byte.TryParse(args[0], out var iValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or 1.");
return;
}
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (!overlayMan.HasOverlay(typeof(NetEntityOverlay)))
if(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.AddOverlay(new NetEntityOverlay());
shell.WriteLine("Enabled network entity report overlay.");
}
else
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.RemoveOverlay(typeof(NetEntityOverlay));
shell.WriteLine("Disabled network entity report overlay.");
}
}
}
private sealed class NetShowGraphCommand : IConsoleCommand
{
// Yeah commands should be localized, but I'm lazy and this is really just a debug command.
public string Command => "net_refresh";
public string Help => "net_refresh";
public string Description => "requests a full server state";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
IoCManager.Resolve<IClientGameStateManager>().RequestFullState();
}
}
}
}

View File

@@ -10,7 +10,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Client.Player;
namespace Robust.Client.GameStates
{
@@ -29,7 +28,6 @@ namespace Robust.Client.GameStates
private const int MidrangePayloadBps = 33600 / 8; // mid-range line
private const int BytesPerPixel = 2; // If you are running the game on a DSL connection, you can scale the graph to fit your absurd bandwidth.
private const int LowerGraphOffset = 100; // Offset on the Y axis in pixels of the lower lag/interp graph.
private const int LeftMargin = 500; // X offset, to avoid interfering with the f3 menu.
private const int MsPerPixel = 4; // Latency Milliseconds per pixel, for scaling the graph.
/// <inheritdoc />
@@ -86,46 +84,38 @@ namespace Robust.Client.GameStates
var sb = new StringBuilder();
foreach (var entState in entStates.Span)
{
if (entState.Uid != WatchEntId)
continue;
if (!entState.ComponentChanges.HasContents)
if (entState.Uid == WatchEntId)
{
sb.Append("\n Entered PVS");
break;
}
if(entState.ComponentChanges.HasContents)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges.Span)
{
var registration = _componentFactory.GetRegistration(compChange.NetID);
var create = compChange.Created ? 'C' : '\0';
var mod = !(compChange.Created || compChange.Created) ? 'M' : '\0';
var del = compChange.Deleted ? 'D' : '\0';
sb.Append($"\n [{create}{mod}{del}]{compChange.NetID}:{registration.Name}");
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges.Span)
{
var registration = _componentFactory.GetRegistration(compChange.NetID);
var create = compChange.Created ? 'C' : '\0';
var mod = !(compChange.Created || compChange.Created) ? 'M' : '\0';
var del = compChange.Deleted ? 'D' : '\0';
sb.Append($"\n [{create}{mod}{del}]{compChange.NetID}:{registration.Name}");
if (compChange.State is not null)
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
if(compChange.State is not null)
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
}
}
}
}
entStateString = sb.ToString();
}
foreach (var ent in args.Detached)
{
if (ent != WatchEntId)
continue;
conShell.WriteLine($"watchEnt: Left PVS at tick {args.AppliedState.ToSequence}, eid={WatchEntId}" + "\n");
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes.HasContents)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes.Span)
{
if (entDelete == WatchEntId)
{
entDelString = "\n Deleted";
}
}
}
@@ -165,16 +155,17 @@ namespace Robust.Client.GameStates
{
// remember, 0,0 is top left of ui with +X right and +Y down
var leftMargin = 300;
var width = HistorySize;
var height = 500;
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
var handle = args.ScreenHandle;
// bottom payload line
handle.DrawLine(new Vector2(LeftMargin, height), new Vector2(LeftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
// bottom lag line
handle.DrawLine(new Vector2(LeftMargin, height + LowerGraphOffset), new Vector2(LeftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(leftMargin, height + LowerGraphOffset), new Vector2(leftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
int lastLagY = -1;
int lastLagMs = -1;
@@ -184,7 +175,7 @@ namespace Robust.Client.GameStates
var state = _history[i];
// draw the payload size
var xOff = LeftMargin + i;
var xOff = leftMargin + i;
var yoff = height - state.Payload / BytesPerPixel;
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
@@ -220,25 +211,25 @@ namespace Robust.Client.GameStates
// average payload line
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
handle.DrawLine(new Vector2(LeftMargin, avgyoff), new Vector2(LeftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(leftMargin, avgyoff), new Vector2(leftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
// top payload warning line
var warnYoff = height - _warningPayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
// mid payload line
var midYoff = height - _midrangePayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(LeftMargin, midYoff), new Vector2(LeftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
// payload text
handle.DrawString(_font, new Vector2(LeftMargin + width, warnYoff), "56K");
handle.DrawString(_font, new Vector2(LeftMargin + width, midYoff), "33.6K");
handle.DrawString(_font, new Vector2(leftMargin + width, warnYoff), "56K");
handle.DrawString(_font, new Vector2(leftMargin + width, midYoff), "33.6K");
// interp text info
if(lastLagY != -1)
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
handle.DrawString(_font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
handle.DrawString(_font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
}
protected override void DisposeBehavior()
@@ -251,19 +242,32 @@ namespace Robust.Client.GameStates
private sealed class NetShowGraphCommand : IConsoleCommand
{
public string Command => "net_graph";
public string Help => "net_graph";
public string Help => "net_graph <0|1>";
public string Description => "Toggles the net statistics pannel.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
return;
}
if (!byte.TryParse(args[0], out var iValue))
{
shell.WriteLine("Invalid argument: Needs to be 0 or 1.");
return;
}
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(!overlayMan.HasOverlay(typeof(NetGraphOverlay)))
if(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.AddOverlay(new NetGraphOverlay());
shell.WriteLine("Enabled network overlay.");
}
else
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
shell.WriteLine("Disabled network overlay.");
@@ -279,12 +283,13 @@ namespace Robust.Client.GameStates
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntityUid eValue;
if (args.Length == 0)
if (args.Length != 1)
{
eValue = IoCManager.Resolve<IPlayerManager>().LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
shell.WriteError("Invalid argument amount. Expected 1 argument.");
return;
}
else if (!EntityUid.TryParse(args[0], out eValue))
if (!EntityUid.TryParse(args[0], out var eValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
@@ -292,13 +297,12 @@ namespace Robust.Client.GameStates
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (!overlayMan.TryGetOverlay(out NetGraphOverlay? overlay))
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlay = new();
overlayMan.AddOverlay(overlay);
}
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
overlay.WatchEntId = eValue;
netOverlay.WatchEntId = eValue;
}
}
}
}

View File

@@ -16,8 +16,8 @@ namespace Robust.Client.Graphics
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;

View File

@@ -10,14 +10,6 @@ namespace Robust.Client.Timing
{
[Dependency] private readonly IClientNetManager _netManager = default!;
public override bool InPrediction => !ApplyingState && CurTick > LastRealTick;
/// <inheritdoc />
public GameTick LastRealTick { get; set; }
/// <inheritdoc />
public GameTick LastProcessedTick { get; set; }
public override TimeSpan ServerTime
{
get

View File

@@ -5,20 +5,6 @@ namespace Robust.Client.Timing
{
public interface IClientGameTiming : IGameTiming
{
/// <summary>
/// This is functionally the clients "current-tick" before prediction, and represents the target value for <see
/// cref="LastRealTick"/>. This value should increment by at least one every tick. It may increase by more than
/// that if we apply several server states within a single tick.
/// </summary>
GameTick LastProcessedTick { get; set; }
/// <summary>
/// The last real non-extrapolated server state that was applied. Without networking issues, this tick should
/// always correspond to <see cref="LastRealTick"/>, however if there is a missing states or the buffer has run
/// out, this value may be smaller..
/// </summary>
GameTick LastRealTick { get; set; }
void StartPastPrediction();
void EndPastPrediction();

View File

@@ -1,11 +1,10 @@
using System;
using System;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.Profiling;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.CustomControls
private readonly Control[] _monitors = new Control[Enum.GetNames<DebugMonitor>().Length];
//TODO: Think about a factory for this
public DebugMonitors(IClientGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
public DebugMonitors(IGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
IInputManager inputManager, IStateManager stateManager, IClyde displayManager, IClientNetManager netManager,
IMapManager mapManager)
{

View File

@@ -1,7 +1,6 @@
using System;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -11,13 +10,13 @@ namespace Robust.Client.UserInterface.CustomControls
{
public sealed class DebugTimePanel : PanelContainer
{
private readonly IClientGameTiming _gameTiming;
private readonly IGameTiming _gameTiming;
private readonly IClientGameStateManager _gameState;
private readonly char[] _textBuffer = new char[256];
private readonly Label _contents;
public DebugTimePanel(IClientGameTiming gameTiming, IClientGameStateManager gameState)
public DebugTimePanel(IGameTiming gameTiming, IClientGameStateManager gameState)
{
_gameTiming = gameTiming;
_gameState = gameState;
@@ -54,7 +53,7 @@ namespace Robust.Client.UserInterface.CustomControls
// This is why there's a -1 on Pred:.
_contents.TextMemory = FormatHelpers.FormatIntoMem(_textBuffer,
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}, LastProcessed: {_gameTiming.LastProcessedTick}, LastRealTick: {_gameTiming.LastRealTick}, Pred: {_gameTiming.CurTick.Value - _gameTiming.LastRealTick.Value - 1}
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}/{_gameTiming.CurTick - 1}, CurServerTick: {_gameState.CurServerTick}, Pred: {_gameTiming.CurTick.Value - _gameState.CurServerTick.Value - 1}
CurTime: {_gameTiming.CurTime:hh\:mm\:ss\.ff}, RealTime: {_gameTiming.RealTime:hh\:mm\:ss\.ff}, CurFrame: {_gameTiming.CurFrame}
ServerTime: {_gameTiming.ServerTime}, TickTimingAdjustment: {_gameTiming.TickTimingAdjustment}");
}

View File

@@ -5,7 +5,6 @@ using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
@@ -27,7 +26,7 @@ namespace Robust.Client.UserInterface
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IFontManager _fontManager = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;

View File

@@ -646,6 +646,9 @@ namespace Robust.Server
ServerCurTick.Set(_time.CurTick.Value);
ServerCurTime.Set(_time.CurTime.TotalSeconds);
// These are always the same on the server, there is no prediction.
_time.LastRealTick = _time.CurTick;
_systemConsole.UpdateTick();
using (TickUsage.WithLabels("PreEngine").NewTimer())

View File

@@ -1,7 +1,3 @@
using System;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates
{
/// <summary>
@@ -20,9 +16,5 @@ namespace Robust.Server.GameStates
void SendGameStateUpdate();
ushort TransformNetId { get; set; }
Action<ICommonSession, GameTick, GameTick>? ClientAck { get; set; }
Action<ICommonSession, GameTick, GameTick>? ClientRequestFull { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
namespace Robust.Server.GameStates;
namespace Robust.Server.GameStates;
public enum PVSEntityVisiblity : byte
{

View File

@@ -11,11 +11,10 @@ using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -28,16 +27,15 @@ internal sealed partial class PVSSystem : EntitySystem
[Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
[Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
[Shared.IoC.Dependency] private readonly IServerEntityManager _serverEntManager = default!;
[Shared.IoC.Dependency] private readonly IServerGameStateManager _stateManager = default!;
[Shared.IoC.Dependency] private readonly SharedTransformSystem _transform = default!;
[Shared.IoC.Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
[Shared.IoC.Dependency] private readonly IServerGameStateManager _serverGameStateManager = default!;
public const float ChunkSize = 8;
public const int TickBuffer = 10;
// TODO make this a cvar. Make it in terms of seconds and tie it to tick rate?
public const int TickBuffer = 20;
// Note: If a client has ping higher than TickBuffer / TickRate, then the server will treat every entity as if it
// had entered PVS for the first time. Note that due to the PVS budget, this buffer is easily overwhelmed.
private static TransformComponentState _transformCullState =
new(Vector2.Zero, Angle.Zero, EntityUid.Invalid, false, false);
/// <summary>
/// Maximum number of pooled objects
@@ -60,16 +58,18 @@ internal sealed partial class PVSSystem : EntitySystem
/// </summary>
public HashSet<ICommonSession> SeenAllEnts = new();
private readonly Dictionary<ICommonSession, SessionPVSData> _playerVisibleSets = new();
/// <summary>
/// All <see cref="Robust.Shared.GameObjects.EntityUid"/>s a <see cref="ICommonSession"/> saw last iteration.
/// </summary>
private readonly Dictionary<ICommonSession, OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>> _playerVisibleSets = new();
private PVSCollection<EntityUid> _entityPvsCollection = default!;
public PVSCollection<EntityUid> EntityPVSCollection => _entityPvsCollection;
private readonly List<IPVSCollection> _pvsCollections = new();
private readonly ObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>> _visSetPool
= new DefaultObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>>(
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize);
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize*TickBuffer);
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
@@ -97,14 +97,10 @@ internal sealed partial class PVSSystem : EntitySystem
private readonly List<(uint, IChunkIndexLocation)> _chunkList = new(64);
private readonly List<MapGrid> _gridsPool = new(8);
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("PVS");
_entityPvsCollection = RegisterPVSCollection<EntityUid>();
SubscribeLocalEvent<MapChangedEvent>(ev =>
@@ -127,9 +123,6 @@ internal sealed partial class PVSSystem : EntitySystem
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
InitializeDirty();
}
@@ -165,75 +158,9 @@ internal sealed partial class PVSSystem : EntitySystem
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
_serverGameStateManager.ClientAck -= OnClientAck;
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
ShutdownDirty();
}
private void OnClientRequestFull(ICommonSession session, GameTick tick, GameTick lastAcked)
{
if (!_playerVisibleSets.TryGetValue(session, out var sessionData))
return;
// TODO rate limit this?
_sawmill.Warning($"Client {session} requested full state on tick {tick}. Last Acked: {lastAcked}. They probably encountered a PVS / missing meta-data exception.");
sessionData.LastSeenAt.Clear();
if (sessionData.Overflow != null)
{
_visSetPool.Return(sessionData.Overflow.Value.SentEnts);
sessionData.Overflow = null;
}
// return last acked to pool, but only if it is not still in the overflow dictionary.
if (sessionData.LastAcked != null && _gameTiming.CurTick.Value - lastAcked.Value > TickBuffer)
_visSetPool.Return(sessionData.LastAcked);
sessionData.RequestedFull = true;
}
private void OnClientAck(ICommonSession session, GameTick ackedTick, GameTick lastAckedTick)
{
if (!_playerVisibleSets.TryGetValue(session, out var sessionData))
return;
if (sessionData.Overflow != null && sessionData.Overflow.Value.Tick < ackedTick)
{
var (overflowTick, overflowEnts) = sessionData.Overflow.Value;
sessionData.Overflow = null;
if (overflowTick == ackedTick)
{
ProcessAckedTick(sessionData, overflowEnts, ackedTick, lastAckedTick);
return;
}
// Even though the acked tick is newer, we have no guarantee that the client received the cached set, so
// we just discard it.
_visSetPool.Return(overflowEnts);
}
if (sessionData.SentEntities.TryGetValue(ackedTick, out var ackedData))
ProcessAckedTick(sessionData, ackedData, ackedTick, lastAckedTick);
}
private void ProcessAckedTick(SessionPVSData sessionData, Dictionary<EntityUid, PVSEntityVisiblity> ackedData, GameTick tick, GameTick lastAckedTick)
{
// return last acked to pool, but only if it is not still in the overflow dictionary.
if (sessionData.LastAcked != null && _gameTiming.CurTick.Value - lastAckedTick.Value > TickBuffer)
_visSetPool.Return(sessionData.LastAcked);
sessionData.LastAcked = ackedData;
foreach (var ent in ackedData.Keys)
{
sessionData.LastSeenAt[ent] = tick;
}
// The client acked a tick. If they requested a full state, this ack happened some time after that, so we can safely set this to false
sessionData.RequestedFull = false;
}
private void OnViewsizeChanged(float obj)
{
_viewSize = obj * 2;
@@ -244,6 +171,7 @@ internal sealed partial class PVSSystem : EntitySystem
CullingEnabled = value;
}
public void ProcessCollections()
{
foreach (var collection in _pvsCollections)
@@ -300,15 +228,6 @@ internal sealed partial class PVSSystem : EntitySystem
private void OnEntityDeleted(EntityUid e)
{
_entityPvsCollection.RemoveIndex(EntityManager.CurrentTick, e);
var previousTick = _gameTiming.CurTick - 1;
foreach (var sessionData in _playerVisibleSets.Values)
{
sessionData.LastSeenAt.Remove(e);
if (sessionData.SentEntities.TryGetValue(previousTick, out var ents))
ents.Remove(e);
}
}
private void OnEntityMove(ref MoveEvent ev)
@@ -350,35 +269,24 @@ internal sealed partial class PVSSystem : EntitySystem
{
if (e.NewStatus == SessionStatus.InGame)
{
_playerVisibleSets.Add(e.Session, new());
_playerVisibleSets.Add(e.Session, new OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>(TickBuffer, _visSetPool.Return));
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.AddPlayer(e.Session);
}
return;
}
if (e.NewStatus != SessionStatus.Disconnected)
return;
foreach (var pvsCollection in _pvsCollections)
else if (e.NewStatus == SessionStatus.Disconnected)
{
pvsCollection.RemovePlayer(e.Session);
}
if (!_playerVisibleSets.Remove(e.Session, out var data))
return;
if (data.Overflow != null)
_visSetPool.Return(data.Overflow.Value.SentEnts);
if (data.LastAcked != null)
_visSetPool.Return(data.LastAcked);
foreach (var (_, visSet) in data.SentEntities)
{
if (visSet != data.LastAcked)
_visSetPool.Return(visSet);
var overflowDict = _playerVisibleSets[e.Session];
_playerVisibleSets.Remove(e.Session);
foreach (var (_, playerVisSet) in overflowDict)
{
_visSetPool.Return(playerVisSet);
}
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.RemovePlayer(e.Session);
}
}
}
@@ -658,7 +566,7 @@ internal sealed partial class PVSSystem : EntitySystem
return true;
}
public (List<EntityState>? updates, List<EntityUid>? deletions, List<EntityUid>? leftPvs, GameTick fromTick) CalculateEntityStates(IPlayerSession session,
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(IPlayerSession session,
GameTick fromTick, GameTick toTick,
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache,
HashSet<int> chunkIndices, EntityQuery<MetaDataComponent> mQuery, EntityQuery<TransformComponent> tQuery,
@@ -667,13 +575,8 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(session.Status == SessionStatus.InGame);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
var entitiesSent = 0;
var sessionData = _playerVisibleSets[session];
sessionData.SentEntities.TryGetValue(toTick - 1, out var lastSent);
var lastAcked = sessionData.LastAcked;
var lastSeen = sessionData.LastSeenAt;
_playerVisibleSets[session].TryGetValue(fromTick, out var playerVisibleSet);
var visibleEnts = _visSetPool.Get();
DebugTools.Assert(visibleEnts.Count == 0);
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
foreach (var i in chunkIndices)
@@ -682,7 +585,7 @@ internal sealed partial class PVSSystem : EntitySystem
if(!cache.HasValue) continue;
foreach (var rootNode in cache.Value.tree.RootNodes)
{
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, lastAcked, lastSent, visibleEnts, fromTick,
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, cache.Value.metadata, in enteredEntityBudget);
}
}
@@ -691,7 +594,7 @@ internal sealed partial class PVSSystem : EntitySystem
while (globalEnumerator.MoveNext())
{
var uid = globalEnumerator.Current;
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, fromTick,
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
globalEnumerator.Dispose();
@@ -700,14 +603,14 @@ internal sealed partial class PVSSystem : EntitySystem
while (localEnumerator.MoveNext())
{
var uid = localEnumerator.Current;
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, fromTick,
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
localEnumerator.Dispose();
foreach (var viewerEntity in viewerEntities)
{
RecursivelyAddOverride(in viewerEntity, lastAcked, lastSent, visibleEnts, fromTick,
RecursivelyAddOverride(in viewerEntity, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
@@ -715,87 +618,56 @@ internal sealed partial class PVSSystem : EntitySystem
RaiseLocalEvent(ref expandEvent);
foreach (var entityUid in expandEvent.Entities)
{
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, fromTick,
RecursivelyAddOverride(in entityUid, playerVisibleSet, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
var entityStates = new List<EntityState>();
foreach (var (uid, visiblity) in visibleEnts)
foreach (var (entityUid, visiblity) in visibleEnts)
{
if (sessionData.RequestedFull)
{
entityStates.Add(GetFullEntityState(session, uid, mQuery.GetComponent(uid)));
continue;
}
if (visiblity == PVSEntityVisiblity.StayedUnchanged)
continue;
var entered = visiblity == PVSEntityVisiblity.Entered;
var entFromTick = entered ? lastSeen.GetValueOrDefault(uid) : fromTick;
var state = GetEntityState(session, uid, entFromTick, mQuery.GetComponent(uid));
var @new = visiblity == PVSEntityVisiblity.Entered;
var state = GetEntityState(session, entityUid, @new ? GameTick.Zero : fromTick, mQuery.GetComponent(entityUid).Flags);
if (entered || !state.Empty)
entityStates.Add(state);
//this entity is not new & nothing changed
if(!@new && state.Empty) continue;
entityStates.Add(state);
}
// tell a client to detach entities that have left their view
var leftView = ProcessLeavePVS(visibleEnts, lastSent);
if (sessionData.SentEntities.Add(toTick, visibleEnts, out var oldEntry))
if(playerVisibleSet != null)
{
if (oldEntry.Value.Key > fromTick && sessionData.Overflow == null)
foreach (var (entityUid, _) in playerVisibleSet)
{
// The clients last ack is too late, the overflow dictionary size has been exceeded, and we will no
// longer have information about the sent entities. This means we would no longer be able to add
// entities to _ackedEnts.
//
// If the client has enough latency, this result in a situation where we must constantly assume that every entity
// that needs to get sent to the client is being received by them for the first time.
//
// In order to avoid this, while also keeping the overflow dictionary limited in size, we keep a single
// overflow state, so we can at least periodically update the acked entities.
// it was deleted, so we dont need to exit pvs
if (deletions.Contains(entityUid)) continue;
// This is pretty shit and there is probably a better way of doing this.
sessionData.Overflow = oldEntry.Value;
_sawmill.Warning($"Client {session} exceeded tick buffer.");
//TODO: HACK: somehow an entity left the view, transform does not exist (deleted?), but was not in the
// deleted list. This seems to happen with the map entity on round restart.
if (!EntityManager.EntityExists(entityUid))
continue;
entityStates.Add(new EntityState(entityUid, new NetListAsArray<ComponentChange>(new[]
{
ComponentChange.Changed(_stateManager.TransformNetId, _transformCullState),
}), true));
}
else if (oldEntry.Value.Value != lastAcked)
_visSetPool.Return(oldEntry.Value.Value);
}
_playerVisibleSets[session].Add(toTick, visibleEnts);
if (deletions.Count == 0) deletions = default;
if (entityStates.Count == 0) entityStates = default;
return (entityStates, deletions, leftView, sessionData.RequestedFull ? GameTick.Zero : fromTick);
}
/// <summary>
/// Figure out what entities are no longer visible to the client. These entities are sent reliably to the client
/// in a separate net message.
/// </summary>
private List<EntityUid>? ProcessLeavePVS(
Dictionary<EntityUid, PVSEntityVisiblity> visibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent)
{
if (lastSent == null)
return null;
var leftView = new List<EntityUid>();
foreach (var uid in lastSent.Keys)
{
if (!visibleEnts.ContainsKey(uid))
leftView.Add(uid);
}
return leftView.Count > 0 ? leftView : null;
return (entityStates, deletions);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private bool RecursivelyAddTreeNode(in EntityUid nodeIndex,
private void RecursivelyAddTreeNode(in EntityUid nodeIndex,
RobustTree<EntityUid> tree,
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int totalEnteredEntities,
@@ -811,12 +683,12 @@ internal sealed partial class PVSSystem : EntitySystem
if (nodeIndex.IsValid() && !toSend.ContainsKey(nodeIndex))
{
//are we new?
var (entered, budgetFull) = ProcessEntry(in nodeIndex, lastAcked, lastSent,
var (entered, budgetFail) = ProcessEntry(in nodeIndex, previousVisibleEnts,
ref totalEnteredEntities, in enteredEntityBudget);
AddToSendSet(in nodeIndex, metaDataCache[nodeIndex], toSend, fromTick, entered);
if (budgetFail) return;
if (budgetFull) return true;
AddToSendSet(in nodeIndex, metaDataCache[nodeIndex], toSend, fromTick, entered);
}
var node = tree[nodeIndex];
@@ -825,19 +697,15 @@ internal sealed partial class PVSSystem : EntitySystem
{
foreach (var child in node.Children)
{
if (RecursivelyAddTreeNode(in child, tree, lastAcked, lastSent, toSend, fromTick,
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget))
return true;
RecursivelyAddTreeNode(in child, tree, previousVisibleEnts, toSend, fromTick,
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget);
}
}
return false;
}
public bool RecursivelyAddOverride(
in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int totalEnteredEntities,
@@ -853,40 +721,29 @@ internal sealed partial class PVSSystem : EntitySystem
if (toSend.ContainsKey(uid)) return true;
var parent = transQuery.GetComponent(uid).ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, lastAcked, lastSent, toSend, fromTick,
if (parent.IsValid() && !RecursivelyAddOverride(in parent, previousVisibleEnts, toSend, fromTick,
ref totalEnteredEntities, metaQuery, transQuery, in enteredEntityBudget))
return false;
var (entered, _) = ProcessEntry(in uid, lastAcked, lastSent,
var (entered, _) = ProcessEntry(in uid, previousVisibleEnts,
ref totalEnteredEntities, in enteredEntityBudget);
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, entered);
return true;
}
private (bool entering, bool budgetFull) ProcessEntry(in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
private (bool entered, bool budgetFail) ProcessEntry(in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
ref int totalEnteredEntities, in int enteredEntityBudget)
{
var enteredSinceLastSent = lastSent == null || !lastSent.ContainsKey(uid);
var entered = previousVisibleEnts?.Remove(uid) == false;
var entered = enteredSinceLastSent || // OR, entered since last ack:
lastAcked == null || !lastAcked.ContainsKey(uid);
// If the entity is entering, but we already sent this entering entity, in the last message, we won't add it to
// the budget. Chances are the packet will arrive in a nice and orderly fashion, and the client will stick to
// their requested budget. However this can cause issues if a packet gets dropped, because a player may create
// 2x or more times the normal entity creation budget.
//
// The fix for that would be to just also give the PVS budget a client-side aspect that controls entity creation
// rate.
if (enteredSinceLastSent)
if (entered)
{
// TODO: should we separate this budget into "entered-but-seen" and "completely-new"?
// completely new entities are significantly more intensive for both server sending and client processing.
if (totalEnteredEntities++ >= enteredEntityBudget)
if (totalEnteredEntities >= enteredEntityBudget)
return (entered, true);
totalEnteredEntities++;
}
return (entered, false);
@@ -900,7 +757,7 @@ internal sealed partial class PVSSystem : EntitySystem
return;
}
if (metaDataComponent.EntityLastModifiedTick <= fromTick)
if (metaDataComponent.EntityLastModifiedTick < fromTick)
{
//entity has been sent before and hasnt been updated since
toSend.Add(uid, PVSEntityVisiblity.StayedUnchanged);
@@ -914,7 +771,7 @@ internal sealed partial class PVSSystem : EntitySystem
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
public (List<EntityState>?, List<EntityUid>?, List<EntityUid>?, GameTick fromTick) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
public (List<EntityState>? updates, List<EntityUid>? deletions) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
{
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
// no point sending an empty collection
@@ -934,10 +791,10 @@ internal sealed partial class PVSSystem : EntitySystem
foreach (var md in EntityManager.EntityQuery<MetaDataComponent>(true))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md));
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md.Flags));
}
return (stateEntities.Count == 0 ? default : stateEntities, deletions, null, fromTick);
return (stateEntities.Count == 0 ? default : stateEntities, deletions);
}
// Just get the relevant entities that have been dirtied
@@ -962,8 +819,8 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick > fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md));
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md.Flags));
}
foreach (var uid in dirty)
@@ -975,8 +832,8 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick > fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick, md));
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick, md.Flags));
}
}
}
@@ -985,7 +842,7 @@ internal sealed partial class PVSSystem : EntitySystem
{
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions, null, fromTick);
return (stateEntities, deletions);
}
stateEntities = new List<EntityState>(EntityManager.EntityCount);
@@ -996,13 +853,13 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md));
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md.Flags));
}
// no point sending an empty collection
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions, null, fromTick);
return (stateEntities, deletions);
}
/// <summary>
@@ -1011,27 +868,20 @@ internal sealed partial class PVSSystem : EntitySystem
/// <param name="player">The player to generate this state for.</param>
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <param name="meta">The entity's metadata component</param>
/// <param name="includeImplicit">If true, the state will include even the implicit component data</param>
/// <param name="flags">Any applicable metadata flags</param>
/// <returns>New entity State for the given entity.</returns>
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataFlags flags)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
// Whether this entity has any component states that should only be sent to specific sessions.
var entitySpecific = (meta.Flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
// Whether this entity has any component states that are only for a specific session.
// TODO: This GetComp is probably expensive, less expensive than before, but ideally we'd cache it somewhere or something from a previous getcomp
// Probably still needs tweaking but checking for add / changed states up front should do most of the work.
var specificStates = (flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
{
if (!component.NetSyncEnabled)
continue;
if (component.Deleted || !component.Initialized)
{
_sawmill.Error("Entity manager returned deleted or uninitialized components while sending entity data");
continue;
}
DebugTools.Assert(component.Initialized);
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
// "not different from entity creation".
@@ -1042,20 +892,37 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
var addState = component.CreationTick != GameTick.Zero && component.CreationTick > fromTick;
var changedState = component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick > fromTick;
var addState = false;
var changeState = false;
if (!(addState || changedState))
// We'll check the properties first; if we ever have specific states then doing the struct event is expensive.
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
addState = true;
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
changeState = true;
if (!addState && !changeState)
continue;
if (component.SendOnlyToOwner && player.AttachedEntity != component.Owner)
if (specificStates && !EntityManager.CanGetComponentState(bus, component, player))
continue;
if (entitySpecific && !EntityManager.CanGetComponentState(bus, component, player))
continue;
if (addState)
{
ComponentState? state = null;
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero &&
component.LastModifiedTick >= fromTick)
state = EntityManager.GetComponentState(bus, component);
var state = changedState ? EntityManager.GetComponentState(bus, component) : null;
changed.Add(ComponentChange.Added(netId, state, component.LastModifiedTick));
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChange.Added(netId, state));
}
else
{
DebugTools.Assert(changeState);
changed.Add(ComponentChange.Changed(netId, EntityManager.GetComponentState(bus, component)));
}
}
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, fromTick))
@@ -1063,38 +930,7 @@ internal sealed partial class PVSSystem : EntitySystem
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray(), meta.EntityLastModifiedTick);
}
/// <summary>
/// Variant of <see cref="GetEntityState"/> that includes all entity data, including data that can be inferred implicitly from the entity prototype.
/// </summary>
private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
var entitySpecific = (meta.Flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
{
if (!component.NetSyncEnabled)
continue;
if (component.SendOnlyToOwner && player.AttachedEntity != component.Owner)
continue;
if (entitySpecific && !EntityManager.CanGetComponentState(bus, component, player))
continue;
changed.Add(ComponentChange.Added(netId, EntityManager.GetComponentState(bus, component), component.LastModifiedTick));
}
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, GameTick.Zero))
{
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray(), meta.EntityLastModifiedTick);
return new EntityState(entityUid, changed.ToArray());
}
private EntityUid[] GetSessionViewers(ICommonSession session)
@@ -1194,39 +1030,6 @@ internal sealed partial class PVSSystem : EntitySystem
return true;
}
}
/// <summary>
/// Session data class used to avoid having to lock session dictionaries.
/// </summary>
private sealed class SessionPVSData
{
/// <summary>
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="TickBuffer"/> ticks.
/// </summary>
public readonly OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>> SentEntities = new(TickBuffer);
/// <summary>
/// The most recently acked entities
/// </summary>
public Dictionary<EntityUid, PVSEntityVisiblity>? LastAcked = new();
/// <summary>
/// Stores the last tick at which a given entity was acked by a player. Used to avoid re-sending the whole entity
/// state when an item re-enters PVS.
/// </summary>
public readonly Dictionary<EntityUid, GameTick> LastSeenAt = new();
/// <summary>
/// <see cref="_sentData"/> overflow in case a player's last ack is more than <see cref="TickBuffer"/> ticks behind the current tick.
/// </summary>
public (GameTick Tick, Dictionary<EntityUid, PVSEntityVisiblity> SentEnts)? Overflow;
/// <summary>
/// If true, the client has explicitly requested a full state. Unlike the first state, we will send them
/// all data, not just data that cannot be implicitly inferred from entity prototypes.
/// </summary>
public bool RequestedFull = false;
}
}
[ByRefEvent]

View File

@@ -22,7 +22,6 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SharpZstd.Interop;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Players;
namespace Robust.Server.GameStates
{
@@ -52,9 +51,6 @@ namespace Robust.Server.GameStates
public ushort TransformNetId { get; set; }
public Action<ICommonSession, GameTick, GameTick>? ClientAck { get; set; }
public Action<ICommonSession, GameTick, GameTick>? ClientRequestFull { get; set; }
public void PostInject()
{
_logger = Logger.GetSawmill("PVS");
@@ -64,9 +60,7 @@ namespace Robust.Server.GameStates
public void Initialize()
{
_networkManager.RegisterNetMessage<MsgState>();
_networkManager.RegisterNetMessage<MsgStateLeavePvs>();
_networkManager.RegisterNetMessage<MsgStateAck>(HandleStateAck);
_networkManager.RegisterNetMessage<MsgStateRequestFull>(HandleFullStateRequest);
_networkManager.Connected += HandleClientConnected;
_networkManager.Disconnect += HandleClientDisconnect;
@@ -124,7 +118,10 @@ namespace Robust.Server.GameStates
private void HandleClientConnected(object? sender, NetChannelArgs e)
{
_ackedStates[e.Channel.ConnectionId] = GameTick.Zero;
if (!_ackedStates.ContainsKey(e.Channel.ConnectionId))
_ackedStates.Add(e.Channel.ConnectionId, GameTick.Zero);
else
_ackedStates[e.Channel.ConnectionId] = GameTick.Zero;
}
private void HandleClientDisconnect(object? sender, NetChannelArgs e)
@@ -132,36 +129,38 @@ namespace Robust.Server.GameStates
_ackedStates.Remove(e.Channel.ConnectionId);
}
private void HandleFullStateRequest(MsgStateRequestFull msg)
{
if (!_playerManager.TryGetSessionById(msg.MsgChannel.UserId, out var session) ||
!_ackedStates.TryGetValue(msg.MsgChannel.ConnectionId, out var lastAcked))
return;
ClientRequestFull?.Invoke(session, msg.Tick, lastAcked);
// Update acked tick so that OnClientAck doesn't get invoked by any late acks.
_ackedStates[msg.MsgChannel.ConnectionId] = msg.Tick;
}
private void HandleStateAck(MsgStateAck msg)
{
if (_playerManager.TryGetSessionById(msg.MsgChannel.UserId, out var session))
Ack(msg.MsgChannel.ConnectionId, msg.Sequence, session);
Ack(msg.MsgChannel.ConnectionId, msg.Sequence);
}
private void Ack(long uniqueIdentifier, GameTick stateAcked, IPlayerSession playerSession)
private void Ack(long uniqueIdentifier, GameTick stateAcked)
{
if (!_ackedStates.TryGetValue(uniqueIdentifier, out var lastAck) || stateAcked <= lastAck)
return;
DebugTools.Assert(_networkManager.IsServer);
ClientAck?.Invoke(playerSession, stateAcked, lastAck);
_ackedStates[uniqueIdentifier] = stateAcked;
if (_ackedStates.TryGetValue(uniqueIdentifier, out var lastAck))
{
if (stateAcked > lastAck) // most of the time this is true
{
_ackedStates[uniqueIdentifier] = stateAcked;
}
else if (stateAcked == GameTick.Zero) // client signaled they need a full state
{
//Performance/Abuse: Should this be rate limited?
_ackedStates[uniqueIdentifier] = GameTick.Zero;
}
//else stateAcked was out of order or client is being silly, just ignore
}
else
DebugTools.Assert("How did the client send us an ack without being connected?");
}
/// <inheritdoc />
public void SendGameStateUpdate()
{
DebugTools.Assert(_networkManager.IsServer);
if (!_networkManager.IsConnected)
{
// Prevent deletions piling up if we have no clients.
@@ -258,7 +257,7 @@ namespace Robust.Server.GameStates
DebugTools.Assert("Why does this channel not have an entry?");
}
var (entStates, deletions, leftPvs, fromTick) = _pvs.CullingEnabled
var (entStates, deletions) = _pvs.CullingEnabled
? _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick, chunkCache,
playerChunks[sessionIndex], metadataQuery, transformQuery, viewerEntities[sessionIndex])
: _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
@@ -268,7 +267,7 @@ namespace Robust.Server.GameStates
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
var lastSystemMessage = _entityNetworkManager.GetLastMessageSequence(session);
var state = new GameState(fromTick, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
var state = new GameState(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
entStates, playerStates, deletions, mapData);
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
@@ -278,8 +277,6 @@ namespace Robust.Server.GameStates
stateUpdateMessage.State = state;
stateUpdateMessage.CompressionContext = resources.CompressionContext;
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
// If the state is too big we let Lidgren send it reliably.
// This is to avoid a situation where a state is so large that it consistently gets dropped
// (or, well, part of it).
@@ -289,15 +286,11 @@ namespace Robust.Server.GameStates
// TODO: remove this lock by having a single state object per session that contains all per-session state needed.
lock (_ackedStates)
{
Ack(channel.ConnectionId, _gameTiming.CurTick, session);
_ackedStates[channel.ConnectionId] = _gameTiming.CurTick;
}
}
// separately, we send PVS detach / left-view messages reliably. This is not resistant to packet loss,
// but unlike game state it doesn't really matter. This also significantly reduces the size of game
// state messages PVS chunks move out of view.
if (leftPvs != null && leftPvs.Count > 0)
_networkManager.ServerSendMessage(new MsgStateLeavePvs() { Entities = leftPvs, Tick = _gameTiming.CurTick }, channel);
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
}
if (_pvs.CullingEnabled)

View File

@@ -70,13 +70,13 @@ namespace Robust.Shared
/// Whether to interpolate between server game states for render frames on the client.
/// </summary>
public static readonly CVarDef<bool> NetInterp =
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENTONLY);
CVarDef.Create("net.interp", true, CVar.ARCHIVE);
/// <summary>
/// The target number of game states to keep buffered up to smooth out network inconsistency.
/// The target number of game states to keep buffered up to smooth out against network inconsistency.
/// </summary>
public static readonly CVarDef<int> NetBufferSize =
CVarDef.Create("net.buffer_size", 0, CVar.ARCHIVE | CVar.CLIENTONLY);
public static readonly CVarDef<int> NetInterpRatio =
CVarDef.Create("net.interp_ratio", 0, CVar.ARCHIVE);
/// <summary>
/// Enable verbose game state/networking logging.
@@ -137,12 +137,6 @@ namespace Robust.Shared
public static readonly CVarDef<int> NetPVSEntityBudget =
CVarDef.Create("net.pvs_budget", 50, CVar.ARCHIVE | CVar.REPLICATED);
/// <summary>
/// The amount of pvs-exiting entities that a client will process in a single tick.
/// </summary>
public static readonly CVarDef<int> NetPVSEntityExitBudget =
CVarDef.Create("net.pvs_exit_budget", 75, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// ZSTD compression level to use when compressing game states.
/// </summary>

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Shared.Collections;
@@ -74,36 +76,6 @@ public sealed class OverflowDictionary<TKey, TValue> : IDictionary<TKey, TValue>
}
}
/// <summary>
/// Variant of <see cref="Add(TKey, TValue)"/> that also returns any entry that was removed to make room for the new entry.
/// </summary>
public bool Add(TKey key, TValue value, [NotNullWhen(true)] out (TKey Key, TValue Value)? old)
{
if (_dict.ContainsKey(key))
throw new InvalidOperationException("Tried inserting duplicate key.");
if (Count == Capacity)
{
var startIndex = GetArrayStartIndex();
var entry = _insertionQueue[startIndex];
_dict.Remove(entry, out var oldValue);
Array.Clear(_insertionQueue, startIndex, 1);
_valueDisposer?.Invoke(oldValue!);
old = (entry, oldValue!);
}
else
old = null;
_dict.Add(key, value);
_insertionQueue[_currentIndex++] = key;
if (_currentIndex == Capacity)
{
_currentIndex = 0;
}
return old != null;
}
public bool Remove(TKey key)
{
//it doesnt make sense for my usecase so i left this unimplemented. i cba to bother with moving all the entries in the array around etc.

View File

@@ -139,7 +139,7 @@ namespace Robust.Shared.Configuration
/// <inheritdoc />
public void TickProcessMessages()
{
if (!_timing.InSimulation || _timing.InPrediction)
if(!_timing.InSimulation || _timing.InPrediction)
return;
// _netVarsMessages is not in any particular ordering.
@@ -150,7 +150,7 @@ namespace Robust.Shared.Configuration
{
var msg = _netVarsMessages[i];
if (msg.Tick > _timing.CurTick)
if (msg.Tick > _timing.LastRealTick)
continue;
toApply.Add(msg);
@@ -168,8 +168,8 @@ namespace Robust.Shared.Configuration
{
ApplyNetVarChange(msg.MsgChannel, msg.NetworkedVars, msg.Tick);
if(msg.Tick != default && msg.Tick < _timing.CurTick)
_sawmill.Warning($"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.CurTick} ).");
if(msg.Tick != default && msg.Tick < _timing.LastRealTick)
_sawmill.Warning($"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.LastRealTick} ).");
}
}

View File

@@ -22,9 +22,9 @@ namespace Robust.Shared.GameObjects
public virtual string Name => IoCManager.Resolve<IComponentFactory>().GetComponentName(GetType());
/// <inheritdoc />
[ViewVariables]
[DataField("netsync")]
public bool NetSyncEnabled { get; } = true;
//readonly. If you want to make it writable, you need to add the component to the entity's net-components
public bool NetSyncEnabled { get; set; } = true;
/// <inheritdoc />
[ViewVariables]
@@ -34,13 +34,6 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public ComponentLifeStage LifeStage { get; private set; } = ComponentLifeStage.PreAdd;
/// <summary>
/// If true, and if this is a networked component, then component data will only be sent to players if their
/// controlled entity is the owner of this component. This is a faster alternative to <see
/// cref="MetaDataFlags.EntitySpecific"/>.
/// </summary>
public virtual bool SendOnlyToOwner => false;
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.PreAdd" /> to <see cref="ComponentLifeStage.Added" />,
/// after raising a <see cref="ComponentAdd"/> event.

View File

@@ -60,12 +60,6 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public GameTick EntityLastModifiedTick { get; internal set; } = new(1);
/// <summary>
/// This is the tick at which the client last applied state data received from the server.
/// </summary>
[ViewVariables]
public GameTick LastStateApplied { get; internal set; } = GameTick.Zero;
/// <summary>
/// The in-game name of this entity.
/// </summary>
@@ -191,21 +185,13 @@ namespace Robust.Shared.GameObjects
public enum MetaDataFlags : byte
{
None = 0,
/// <summary>
/// Whether the entity has states specific to particular players. This will cause many state-attempt events to
/// be raised, and is generally somewhat expensive.
/// Whether the entity has states specific to a particular player.
/// </summary>
EntitySpecific = 1 << 0,
/// <summary>
/// Whether the entity is currently inside of a container.
/// </summary>
InContainer = 1 << 1,
/// <summary>
/// Used by clients to indicate that an entity has left their visible set.
/// </summary>
Detached = 1 << 2,
}
}

View File

@@ -1123,7 +1123,6 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public ComponentState GetComponentState(IEventBus eventBus, IComponent component)
{
DebugTools.Assert(component.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {component.GetType()}");
var getState = new ComponentGetState();
eventBus.RaiseComponentEvent(component, ref getState);

View File

@@ -6,7 +6,6 @@ using Prometheus;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
@@ -28,7 +27,6 @@ namespace Robust.Shared.GameObjects
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ISerializationManager _serManager = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly ProfManager _prof = default!;
#endregion Dependencies
@@ -252,17 +250,19 @@ namespace Robust.Shared.GameObjects
/// <remarks>
/// Calling Dirty on a component will call this directly.
/// </remarks>
public virtual void Dirty(EntityUid uid)
public void Dirty(EntityUid uid)
{
var currentTick = CurrentTick;
// We want to retrieve MetaDataComponent even if its Deleted flag is set.
if (!_entTraitArray[CompIdx.ArrayIndex<MetaDataComponent>()].TryGetValue(uid, out var component))
throw new KeyNotFoundException($"Entity {uid} does not exist, cannot dirty it.");
var metadata = (MetaDataComponent)component;
if (metadata.EntityLastModifiedTick == _gameTiming.CurTick) return;
if (metadata.EntityLastModifiedTick == currentTick) return;
metadata.EntityLastModifiedTick = _gameTiming.CurTick;
metadata.EntityLastModifiedTick = currentTick;
if (metadata.EntityLifeStage > EntityLifeStage.Initializing)
{
@@ -270,7 +270,7 @@ namespace Robust.Shared.GameObjects
}
}
public virtual void Dirty(Component component)
public void Dirty(Component component)
{
var owner = component.Owner;

View File

@@ -1,7 +1,6 @@
using Robust.Shared.Serialization;
using System;
using NetSerializer;
using Robust.Shared.Timing;
namespace Robust.Shared.GameObjects
{
@@ -14,13 +13,10 @@ namespace Robust.Shared.GameObjects
public bool Empty => ComponentChanges.Value is null or { Count: 0 };
public readonly GameTick EntityLastModified;
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents, GameTick lastModified)
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents, bool hide = false)
{
Uid = uid;
ComponentChanges = changedComponents;
EntityLastModified = lastModified;
}
}
@@ -49,15 +45,12 @@ namespace Robust.Shared.GameObjects
/// </summary>
public readonly ushort NetID;
public readonly GameTick LastModifiedTick;
public ComponentChange(ushort netId, bool created, bool deleted, ComponentState? state, GameTick lastModifiedTick)
public ComponentChange(ushort netId, bool created, bool deleted, ComponentState? state)
{
Deleted = deleted;
State = state;
NetID = netId;
Created = created;
LastModifiedTick = lastModifiedTick;
}
public override string ToString()
@@ -65,19 +58,19 @@ namespace Robust.Shared.GameObjects
return $"{(Deleted ? "D" : "C")} {NetID} {State?.GetType().Name}";
}
public static ComponentChange Added(ushort netId, ComponentState? state, GameTick lastModifiedTick)
public static ComponentChange Added(ushort netId, ComponentState? state)
{
return new(netId, true, false, state, lastModifiedTick);
return new(netId, true, false, state);
}
public static ComponentChange Changed(ushort netId, ComponentState state, GameTick lastModifiedTick)
public static ComponentChange Changed(ushort netId, ComponentState state)
{
return new(netId, false, false, state, lastModifiedTick);
return new(netId, false, false, state);
}
public static ComponentChange Removed(ushort netId)
{
return new(netId, false, true, null, default);
return new(netId, false, true, null);
}
}
}

View File

@@ -734,7 +734,7 @@ public abstract partial class SharedTransformSystem
DebugTools.Assert(!xform.Anchored);
}
public void DetachParentToNull(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery, TransformComponent? oldConcrete = null)
public void DetachParentToNull(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery)
{
var oldParent = xform._parent;
@@ -763,7 +763,7 @@ public abstract partial class SharedTransformSystem
RaiseLocalEvent(xform.Owner, ref anchorStateChangedEvent, true);
}
oldConcrete ??= xformQuery.GetComponent(oldParent);
var oldConcrete = xformQuery.GetComponent(oldParent);
oldConcrete._children.Remove(xform.Owner);
xform._parent = EntityUid.Invalid;

View File

@@ -1,4 +1,4 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using System;
using System.Diagnostics;
@@ -11,6 +11,13 @@ namespace Robust.Shared.GameStates
[Serializable, NetSerializable]
public sealed class GameState
{
/// <summary>
/// An extrapolated state that was created artificially by the client.
/// It does not contain any real data from the server.
/// </summary>
[field:NonSerialized]
public bool Extrapolated { get; set; }
/// <summary>
/// The serialized size in bytes of this game state.
/// </summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
@@ -27,7 +27,7 @@ namespace Robust.Shared.Network.Messages
public GameState State;
public ZStdCompressionContext CompressionContext;
internal bool _hasWritten;
private bool _hasWritten;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
@@ -70,7 +70,7 @@ namespace Robust.Shared.Network.Messages
// We compress the state.
if (stateStream.Length > CompressionThreshold)
{
// var sw = Stopwatch.StartNew();
var sw = Stopwatch.StartNew();
stateStream.Position = 0;
var buf = ArrayPool<byte>.Shared.Rent(ZStd.CompressBound((int)stateStream.Length));
var length = CompressionContext.Compress2(buf, stateStream.AsSpan());
@@ -79,7 +79,7 @@ namespace Robust.Shared.Network.Messages
buffer.Write(buf.AsSpan(0, length));
// var elapsed = sw.Elapsed;
var elapsed = sw.Elapsed;
// System.Console.WriteLine(
// $"From: {State.FromSequence} To: {State.ToSequence} Size: {length} B Before: {stateStream.Length} B time: {elapsed}");
@@ -94,7 +94,8 @@ namespace Robust.Shared.Network.Messages
buffer.Write(stateStream.AsSpan());
}
_hasWritten = true;
_hasWritten = false;
MsgSize = buffer.LengthBytes;
}
@@ -105,7 +106,13 @@ namespace Robust.Shared.Network.Messages
/// <returns></returns>
public bool ShouldSendReliably()
{
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
// This check will be true in integration tests.
// TODO: Maybe handle this better so that packet loss integration testing can be done?
if (!_hasWritten)
{
return true;
}
return MsgSize > ReliableThreshold;
}

View File

@@ -1,47 +0,0 @@
using System.Collections.Generic;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Timing;
#nullable disable
namespace Robust.Shared.Network.Messages;
/// <summary>
/// Message containing a list of entities that have left a clients view.
/// </summary>
/// <remarks>
/// These messages are only sent if PVS is enabled. These messages are sent separately from the main game state.
/// </remarks>
public sealed class MsgStateLeavePvs : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Entity;
public List<EntityUid> Entities;
public GameTick Tick;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Tick = buffer.ReadGameTick();
var length = buffer.ReadInt32();
Entities = new(length);
for (int i = 0; i < length; i++)
{
Entities.Add(buffer.ReadEntityUid());
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Tick);
buffer.Write(Entities.Count);
foreach (var ent in Entities)
{
buffer.Write(ent);
}
}
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
}

View File

@@ -1,25 +0,0 @@
using Lidgren.Network;
using Robust.Shared.Timing;
#nullable disable
namespace Robust.Shared.Network.Messages;
public sealed class MsgStateRequestFull : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Entity;
public GameTick Tick;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Tick = buffer.ReadGameTick();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Tick);
}
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
}

View File

@@ -25,7 +25,6 @@ namespace Robust.Shared.Physics
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FixturesComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<FixturesComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<FixturesComponent, ComponentHandleState>(OnHandleState);
@@ -289,7 +288,8 @@ namespace Robust.Shared.Physics
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? physics))
{
Logger.ErrorS("physics", $"Tried to apply fixture state for an entity without physics: {ToPrettyString(uid)}");
DebugTools.Assert(false);
Logger.ErrorS("physics", $"Tried to apply fixture state for {uid} which has name {nameof(PhysicsComponent)}");
return;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using Robust.Shared.Log;
using Robust.Shared.Exceptions;
@@ -177,13 +177,12 @@ namespace Robust.Shared.Timing
}
#endif
_timing.InSimulation = true;
var tickPeriod = _timing.CalcAdjustedTickPeriod();
var tickPeriod = CalcTickPeriod();
using (_prof.Group("Ticks"))
{
var countTicksRan = 0;
// run the simulation for every accumulated tick
while (accumulator >= tickPeriod)
{
accumulator -= tickPeriod;
@@ -193,7 +192,6 @@ namespace Robust.Shared.Timing
if (_timing.Paused)
continue;
_timing.TickRemainder = accumulator;
countTicksRan += 1;
// update the simulation
@@ -239,7 +237,7 @@ namespace Robust.Shared.Timing
}
#endif
_timing.CurTick = new GameTick(_timing.CurTick.Value + 1);
tickPeriod = _timing.CalcAdjustedTickPeriod();
tickPeriod = CalcTickPeriod();
if (SingleStep)
_timing.Paused = true;
@@ -306,6 +304,14 @@ namespace Robust.Shared.Timing
Thread.Sleep((int)SleepMode);
}
}
private TimeSpan CalcTickPeriod()
{
// ranges from -1 to 1, with 0 being 'default'
var ratio = MathHelper.Clamp(_timing.TickTimingAdjustment, -0.99f, 0.99f);
var diff = TimeSpan.FromTicks((long)(_timing.TickPeriod.Ticks * ratio));
return _timing.TickPeriod - diff;
}
}
/// <summary>

View File

@@ -1,6 +1,5 @@
using System;
using System.Linq;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Timing
@@ -154,14 +153,6 @@ namespace Robust.Shared.Timing
}
}
public TimeSpan CalcAdjustedTickPeriod()
{
// ranges from -1 to 1, with 0 being 'default'
var ratio = MathHelper.Clamp(TickTimingAdjustment, -0.99f, 0.99f);
var ticks = (1.0 / TickRate * TimeSpan.TicksPerSecond) - (TickPeriod.Ticks * ratio);
return TimeSpan.FromTicks((long) ticks);
}
/// <summary>
/// Current graphics frame since init OpenGL which is taken as frame 1, from swapbuffer to swapbuffer. Useful to set a
/// conditional breakpoint on specific frames, and synchronize with OGL debugging tools that capture frames.
@@ -260,11 +251,14 @@ namespace Robust.Shared.Timing
public bool IsFirstTimePredicted { get; protected set; } = true;
/// <inheritdoc />
public virtual bool InPrediction => false;
public bool InPrediction => !ApplyingState && CurTick > LastRealTick;
/// <inheritdoc />
public bool ApplyingState {get; protected set; }
/// <inheritdoc />
public GameTick LastRealTick { get; set; }
/// <summary>
/// Calculates the average FPS of the last 50 real frame times.
/// </summary>

View File

@@ -102,8 +102,6 @@ namespace Robust.Shared.Timing
/// </summary>
TimeSpan TickRemainder { get; set; }
TimeSpan CalcAdjustedTickPeriod();
/// <summary>
/// Fraction of how far into the tick we are. <c>0</c> is 0% and <see cref="ushort.MaxValue"/> is 100%.
/// </summary>
@@ -150,6 +148,11 @@ namespace Robust.Shared.Timing
/// </summary>
bool ApplyingState { get; }
/// <summary>
/// The last real non-predicted tick that was processed.
/// </summary>
GameTick LastRealTick { get; set; }
string TickStamp => $"{CurTick}, predFirst: {IsFirstTimePredicted}, tickRem: {TickRemainder.TotalSeconds}, sim: {InSimulation}";
static string TickStampStatic => IoCManager.Resolve<IGameTiming>().TickStamp;

View File

@@ -1,7 +1,6 @@
using Moq;
using Moq;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Client.Timing;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
@@ -13,27 +12,27 @@ namespace Robust.UnitTesting.Client.GameStates
[Test]
public void FillBufferBlocksProcessing()
{
var timingMock = new Mock<IClientGameTiming>();
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
var processor = new GameStateProcessor(timing);
processor.Interpolation = true;
processor.AddNewState(GameStateFactory(0, 1));
processor.AddNewState(GameStateFactory(1, 2)); // buffer is at 2/3, so processing should be blocked
// calculate states for first tick
timing.LastProcessedTick = new GameTick(0);
var result = processor.TryGetServerState(out _, out _);
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<IClientGameTiming>();
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
@@ -44,11 +43,12 @@ namespace Robust.UnitTesting.Client.GameStates
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.LastProcessedTick = new GameTick(0);
var result = processor.TryGetServerState(out var curState, out var nextState);
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);
}
@@ -60,7 +60,7 @@ namespace Robust.UnitTesting.Client.GameStates
[Test]
public void FullStateResyncsCurTick()
{
var timingMock = new Mock<IClientGameTiming>();
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
@@ -71,11 +71,10 @@ namespace Robust.UnitTesting.Client.GameStates
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.LastProcessedTick = new GameTick(2);
processor.TryGetServerState(out var state, out _);
timing.CurTick = new GameTick(3);
processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.NotNull(state);
Assert.That(state.ToSequence.Value, Is.EqualTo(1));
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
}
[Test]
@@ -83,10 +82,12 @@ namespace Robust.UnitTesting.Client.GameStates
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.LastProcessedTick = new GameTick(4); // current clock is ahead of server
timing.CurTick = new GameTick(5); // current clock is ahead of server
processor.AddNewState(GameStateFactory(3, 4)); // received a late state
var result = processor.TryGetServerState(out _, out _);
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(result, Is.False);
}
@@ -100,13 +101,57 @@ namespace Robust.UnitTesting.Client.GameStates
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.LastProcessedTick = new GameTick(4); // current clock is ahead of server
var result = processor.TryGetServerState(out _, out _);
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>
/// There is a hole in the state buffer, we have a future state but their FromSequence is too high!
/// In this case we stop and wait for the server to get us the missing link.
@@ -117,10 +162,11 @@ namespace Robust.UnitTesting.Client.GameStates
var (timing, processor) = SetupProcessorFactory();
processor.AddNewState(GameStateFactory(4, 5));
timing.LastRealTick = new GameTick(3);
timing.LastProcessedTick = new GameTick(3);
processor.LastProcessedRealState = new GameTick(3);
var result = processor.TryGetServerState(out _, out _);
timing.CurTick = new GameTick(4);
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(result, Is.False);
}
@@ -136,24 +182,52 @@ namespace Robust.UnitTesting.Client.GameStates
processor.Interpolation = true;
timing.LastProcessedTick = new GameTick(3);
timing.CurTick = new GameTick(4);
timing.LastRealTick = new GameTick(3);
processor.LastProcessedRealState = new GameTick(3);
processor.AddNewState(GameStateFactory(3, 5));
// We're missing the state for this tick so go into extrap.
var result = processor.TryGetServerState(out var curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Null);
timing.LastProcessedTick = new GameTick(4);
// But we DO have the state for the tick after so apply away!
result = processor.TryGetServerState(out curState, out _);
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.Extrapolated, Is.True);
timing.CurTick = new GameTick(5);
// But we DO have the state for the tick after so apply away!
result = processor.ProcessTickStates(timing.CurTick, out curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.Extrapolated, Is.False);
}
/// <summary>
/// The client started extrapolating and now received the state it needs to "continue as normal".
/// In this scenario the CurTick passed to the game state processor
/// is higher than the real next tick to apply, IF it went into extrapolation.
/// The processor needs to go back to the next REAL tick.
/// </summary>
[Test, Ignore("Extrapolation is currently non functional anyways")]
public void UndoExtrapolation()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = true;
processor.AddNewState(GameStateFactory(4, 5));
processor.AddNewState(GameStateFactory(3, 4));
processor.LastProcessedRealState = new GameTick(3);
timing.CurTick = new GameTick(5);
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.ToSequence, Is.EqualTo(new GameTick(4)));
}
/// <summary>
@@ -168,12 +242,10 @@ namespace Robust.UnitTesting.Client.GameStates
/// 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 (IClientGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
private static (IGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
{
var timingMock = new Mock<IClientGameTiming>();
var timingMock = new Mock<IGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
timingMock.SetupProperty(p => p.LastProcessedTick);
timingMock.SetupProperty(p => p.LastRealTick);
timingMock.SetupProperty(p => p.TickTimingAdjustment);
var timing = timingMock.Object;
@@ -183,8 +255,9 @@ namespace Robust.UnitTesting.Client.GameStates
processor.AddNewState(GameStateFactory(1, 2));
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
processor.LastFullStateRequested = null;
timing.LastProcessedTick = timing.LastRealTick = new GameTick(1);
// calculate states for first tick
timing.CurTick = new GameTick(1);
processor.ProcessTickStates(timing.CurTick, out _, out _);
return (timing, processor);
}

View File

@@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -253,10 +252,6 @@ namespace Robust.UnitTesting
{
DebugTools.Assert(IsServer);
// MsgState sending method depends on the size of the possible compressed buffer. But tests bypass buffer read/write.
if (message is MsgState stateMsg)
stateMsg._hasWritten = true;
var channel = (IntegrationNetChannel) recipient;
channel.OtherChannel.TryWrite(new DataMessage(message, channel.RemoteUid));
}

View File

@@ -62,8 +62,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
new EntityUid(512),
new []
{
new ComponentChange(0, true, false, new MapGridComponentState(new GridId(0), 16), default)
}, default);
new ComponentChange(0, true, false, new MapGridComponentState(new GridId(0), 16))
});
serializer.Serialize(stream, payload);
array = stream.ToArray();