mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
PVS & client state handling changes (#3000)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.LoaderApi;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -13,7 +14,7 @@ namespace Robust.Client
|
||||
{
|
||||
private IGameLoop? _mainLoop;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
|
||||
private static bool _hasStarted;
|
||||
|
||||
@@ -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.CurTick;
|
||||
_gameTiming.LastRealTick = _gameTiming.LastProcessedTick = _gameTiming.CurTick;
|
||||
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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
|
||||
@@ -19,8 +18,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
|
||||
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
|
||||
|
||||
@@ -47,6 +45,22 @@ 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;
|
||||
@@ -67,7 +81,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
using (histogram?.WithLabels("EntityNet").NewTimer())
|
||||
{
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick)
|
||||
{
|
||||
var (_, msg) = _queue.Take();
|
||||
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
|
||||
@@ -103,7 +117,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void HandleEntityNetworkMessage(MsgEntity message)
|
||||
{
|
||||
if (message.SourceTick <= _gameStateManager.CurServerTick)
|
||||
if (message.SourceTick <= _gameTiming.LastRealTick)
|
||||
{
|
||||
DispatchMsgEntity(message);
|
||||
return;
|
||||
|
||||
@@ -1470,11 +1470,9 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState == null)
|
||||
if (curState is not SpriteComponentState thestate)
|
||||
return;
|
||||
|
||||
var thestate = (SpriteComponentState)curState;
|
||||
|
||||
Visible = thestate.Visible;
|
||||
DrawDepth = thestate.DrawDepth;
|
||||
scale = thestate.Scale;
|
||||
|
||||
@@ -96,16 +96,21 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -15,7 +16,7 @@ namespace Robust.Client.GameStates;
|
||||
/// </summary>
|
||||
internal sealed class ClientDirtySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
|
||||
private readonly Dictionary<GameTick, HashSet<EntityUid>> _dirtyEntities = new();
|
||||
|
||||
@@ -49,14 +50,14 @@ internal sealed class ClientDirtySystem : EntitySystem
|
||||
_dirtyEntities.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> GetDirtyEntities(GameTick currentTick)
|
||||
public IEnumerable<EntityUid> GetDirtyEntities()
|
||||
{
|
||||
_dirty.Clear();
|
||||
|
||||
// This is just to avoid collection being modified during iteration unfortunately.
|
||||
foreach (var (tick, dirty) in _dirtyEntities)
|
||||
{
|
||||
if (tick < currentTick) continue;
|
||||
if (tick < _timing.LastRealTick) continue;
|
||||
foreach (var ent in dirty)
|
||||
{
|
||||
_dirty.Add(ent);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Timing;
|
||||
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;
|
||||
|
||||
@@ -12,154 +14,144 @@ namespace Robust.Client.GameStates
|
||||
/// <inheritdoc />
|
||||
internal sealed class GameStateProcessor : IGameStateProcessor
|
||||
{
|
||||
private readonly IGameTiming _timing;
|
||||
private readonly IClientGameTiming _timing;
|
||||
|
||||
private readonly List<GameState> _stateBuffer = new();
|
||||
private GameState? _lastFullState;
|
||||
private bool _waitingForFull = true;
|
||||
private int _interpRatio;
|
||||
private GameTick _highestFromSequence;
|
||||
|
||||
private readonly Dictionary<EntityUid, Dictionary<uint, ComponentState>> _lastStateFullRep
|
||||
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
|
||||
= new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinBufferSize => Interpolation ? 3 : 2;
|
||||
public int MinBufferSize => Interpolation ? 2 : 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TargetBufferSize => MinBufferSize + InterpRatio;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentBufferSize => CalculateBufferSize(_timing.CurTick);
|
||||
public int TargetBufferSize => MinBufferSize + BufferSize;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Interpolation { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int InterpRatio
|
||||
public int BufferSize
|
||||
{
|
||||
get => _interpRatio;
|
||||
set => _interpRatio = value < 0 ? 0 : value;
|
||||
get => _bufferSize;
|
||||
set => _bufferSize = 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(IGameTiming timing)
|
||||
public GameStateProcessor(IClientGameTiming timing)
|
||||
{
|
||||
_timing = timing;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddNewState(GameState state)
|
||||
public bool AddNewState(GameState state)
|
||||
{
|
||||
// any state from tick 0 is a full state, and needs to be handled different
|
||||
if (state.FromSequence == GameTick.Zero)
|
||||
{
|
||||
// this is a newer full state, so discard the older one.
|
||||
if (_lastFullState == null || (_lastFullState != null && _lastFullState.ToSequence < state.ToSequence))
|
||||
{
|
||||
_lastFullState = state;
|
||||
|
||||
if (Logging)
|
||||
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: DispatchTick will be modifying CurTick, this is NOT thread safe.
|
||||
var lastTick = new GameTick(_timing.CurTick.Value - 1);
|
||||
|
||||
if (state.ToSequence <= lastTick && !_waitingForFull) // CurTick isn't set properly when WaitingForFull
|
||||
// Check for old states.
|
||||
if (state.ToSequence <= _timing.LastRealTick)
|
||||
{
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
Logger.DebugS("net.state", $"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// lets check for a duplicate state now.
|
||||
for (var i = 0; i < _stateBuffer.Count; i++)
|
||||
// Check for a duplicate states.
|
||||
foreach (var bufferState in _stateBuffer)
|
||||
{
|
||||
var iState = _stateBuffer[i];
|
||||
|
||||
if (state.ToSequence != iState.ToSequence)
|
||||
if (state.ToSequence != bufferState.ToSequence)
|
||||
continue;
|
||||
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
Logger.DebugS("net.state", $"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
// this is a good state that we will be using.
|
||||
_stateBuffer.Add(state);
|
||||
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
|
||||
{
|
||||
LastFullState = state;
|
||||
|
||||
if (Logging)
|
||||
Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
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;
|
||||
|
||||
_stateBuffer.Add(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
/// <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)
|
||||
{
|
||||
bool applyNextState;
|
||||
if (_waitingForFull)
|
||||
{
|
||||
applyNextState = CalculateFullState(out curState, out nextState, TargetBufferSize);
|
||||
}
|
||||
else // this will be how almost all states are calculated
|
||||
{
|
||||
applyNextState = CalculateDeltaState(curTick, out curState, out nextState);
|
||||
}
|
||||
var applyNextState = WaitingForFull
|
||||
? TryGetFullState(out curState, out nextState)
|
||||
: TryGetDeltaState(out curState, out nextState);
|
||||
|
||||
if (applyNextState && !curState!.Extrapolated)
|
||||
LastProcessedRealState = curState.ToSequence;
|
||||
|
||||
if (!_waitingForFull)
|
||||
if (curState != null)
|
||||
{
|
||||
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,
|
||||
DebugTools.Assert(curState.FromSequence <= curState.ToSequence,
|
||||
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
|
||||
|
||||
if (Logging)
|
||||
{
|
||||
Logger.DebugS("net.state", $"Applying State: ext={curState!.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
|
||||
}
|
||||
Logger.DebugS("net.state", $"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
|
||||
}
|
||||
|
||||
var cState = curState!;
|
||||
curState = cState;
|
||||
|
||||
return applyNextState;
|
||||
}
|
||||
|
||||
public void UpdateFullRep(GameState state)
|
||||
{
|
||||
// Logger.Debug($"UPDATE FULL REP: {string.Join(", ", state.EntityStates?.Select(e => e.Uid) ?? Enumerable.Empty<EntityUid>())}");
|
||||
// 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.
|
||||
|
||||
if (state.FromSequence == GameTick.Zero)
|
||||
{
|
||||
@@ -178,7 +170,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
|
||||
{
|
||||
compData = new Dictionary<uint, ComponentState>();
|
||||
compData = new Dictionary<ushort, ComponentState>();
|
||||
_lastStateFullRep.Add(entityState.Uid, compData);
|
||||
}
|
||||
|
||||
@@ -196,167 +188,138 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
private bool CalculateFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState, int targetBufferSize)
|
||||
private bool TryGetFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
{
|
||||
if (_lastFullState != null)
|
||||
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++)
|
||||
{
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Resync CurTick to: {_lastFullState.ToSequence}");
|
||||
var state = _stateBuffer[i];
|
||||
|
||||
var curTick = _timing.CurTick = _lastFullState.ToSequence;
|
||||
|
||||
if (Interpolation)
|
||||
if (state.ToSequence < LastFullState.ToSequence)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
_stateBuffer.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
else if (_stateBuffer.Count >= targetBufferSize)
|
||||
else if (Interpolation && state.ToSequence == nextTick)
|
||||
{
|
||||
curState = _lastFullState;
|
||||
nextState = default;
|
||||
_waitingForFull = false;
|
||||
return true;
|
||||
nextState = state;
|
||||
}
|
||||
}
|
||||
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{targetBufferSize})");
|
||||
// we let the buffer fill up before starting to tick
|
||||
if (_stateBuffer.Count >= TargetBufferSize && (!Interpolation || nextState != null))
|
||||
{
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Resync CurTick to: {LastFullState.ToSequence}");
|
||||
|
||||
// waiting for full state or buffer to fill
|
||||
curState = default;
|
||||
nextState = default;
|
||||
curState = LastFullState;
|
||||
return true;
|
||||
}
|
||||
|
||||
// waiting for buffer to fill
|
||||
if (Logging)
|
||||
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{TargetBufferSize})");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CalculateDeltaState(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
|
||||
internal void AddLeavePvsMessage(MsgStateLeavePvs message)
|
||||
{
|
||||
var lastTick = new GameTick(curTick.Value - 1);
|
||||
var nextTick = new GameTick(curTick.Value + 1);
|
||||
// Late message may still need to be processed,
|
||||
DebugTools.Assert(message.Entities.Count > 0);
|
||||
_pvsDetachMessages.TryAdd(message.Tick, message.Entities);
|
||||
}
|
||||
|
||||
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 == curTick)
|
||||
if (state.ToSequence == targetCurTick && state.FromSequence <= _timing.LastRealTick)
|
||||
{
|
||||
curState = state;
|
||||
_highestFromSequence = state.FromSequence;
|
||||
continue;
|
||||
}
|
||||
else if (Interpolation && state.ToSequence == nextTick)
|
||||
{
|
||||
|
||||
if (Interpolation && state.ToSequence == targetNextTick)
|
||||
nextState = state;
|
||||
|
||||
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
|
||||
{
|
||||
futureStateLowestFromSeq = state.FromSequence;
|
||||
}
|
||||
}
|
||||
else if (state.ToSequence > curTick)
|
||||
if (state.ToSequence > targetCurTick && (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence))
|
||||
{
|
||||
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
|
||||
{
|
||||
futureStateLowestFromSeq = state.FromSequence;
|
||||
}
|
||||
futureStateLowestFromSeq = state.FromSequence;
|
||||
continue;
|
||||
}
|
||||
else if (state.ToSequence == lastTick)
|
||||
{
|
||||
lastStateInput = state.LastProcessedInput;
|
||||
}
|
||||
else if (state.ToSequence < _highestFromSequence) // remove any old states we find to keep the buffer clean
|
||||
|
||||
// remove any old states we find to keep the buffer clean
|
||||
if (state.ToSequence <= _timing.LastRealTick)
|
||||
{
|
||||
_stateBuffer.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we can ACTUALLY apply this state.
|
||||
// Can happen that we can't if there is a hole and we're doing extrapolation.
|
||||
if (curState != null && curState.FromSequence > LastProcessedRealState)
|
||||
curState = null;
|
||||
|
||||
// can't find current state, but we do have a future state.
|
||||
if (!Extrapolation && curState == null && futureStateLowestFromSeq != null
|
||||
&& futureStateLowestFromSeq <= LastProcessedRealState)
|
||||
{
|
||||
//this is not actually extrapolation
|
||||
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
|
||||
return true; // keep moving, we have a future state
|
||||
}
|
||||
|
||||
// we won't extrapolate, and curState was not found, buffer is empty
|
||||
if (!Extrapolation && curState == null)
|
||||
return false;
|
||||
|
||||
// we found both the states to interpolate between, this should almost always be true.
|
||||
if (Interpolation && curState != null)
|
||||
return true;
|
||||
|
||||
if (!Interpolation && curState != null && nextState != null)
|
||||
return true;
|
||||
|
||||
if (curState == null)
|
||||
{
|
||||
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
|
||||
}
|
||||
|
||||
if (nextState == null && Interpolation)
|
||||
{
|
||||
nextState = ExtrapolateState(_highestFromSequence, nextTick, lastStateInput);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
// Even if we can't find current state, maybe we have a future state?
|
||||
return curState != null || (futureStateLowestFromSeq != null && futureStateLowestFromSeq <= _timing.LastRealTick);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
_stateBuffer.Clear();
|
||||
_lastFullState = null;
|
||||
_waitingForFull = true;
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = GameTick.Zero;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data)
|
||||
public void RequestFullState()
|
||||
{
|
||||
_stateBuffer.Clear();
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = _timing.LastRealTick;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data)
|
||||
{
|
||||
foreach (var (uid, compData) in data)
|
||||
{
|
||||
@@ -372,20 +335,39 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity)
|
||||
public Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity)
|
||||
{
|
||||
return _lastStateFullRep[entity];
|
||||
}
|
||||
|
||||
public bool TryGetLastServerStates(EntityUid entity,
|
||||
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary)
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
|
||||
{
|
||||
return _lastStateFullRep.TryGetValue(entity, out dictionary);
|
||||
}
|
||||
|
||||
public int CalculateBufferSize(GameTick fromTick)
|
||||
{
|
||||
return _stateBuffer.Count(s => s.ToSequence >= 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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
|
||||
@@ -27,18 +28,10 @@ namespace Robust.Client.GameStates
|
||||
int TargetBufferSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of game states currently in the state buffer.
|
||||
/// Number of applicable 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.
|
||||
@@ -57,6 +50,11 @@ 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>
|
||||
@@ -78,6 +76,11 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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).
|
||||
/// 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).
|
||||
/// </remarks>
|
||||
int MinBufferSize { get; }
|
||||
|
||||
@@ -28,12 +28,6 @@ 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>
|
||||
@@ -46,29 +40,22 @@ 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 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; }
|
||||
int BufferSize { 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>
|
||||
void AddNewState(GameState state);
|
||||
/// <returns>Returns true if the state was accepted and should be acknowledged</returns>
|
||||
bool AddNewState(GameState state);
|
||||
//> usually from replays
|
||||
//replays when
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the current and next state to apply for a given game tick.
|
||||
@@ -77,7 +64,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 ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState);
|
||||
bool TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState);
|
||||
|
||||
/// <summary>
|
||||
/// Resets the processor back to its initial state.
|
||||
@@ -96,21 +83,22 @@ 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<uint, ComponentState>> data);
|
||||
void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data);
|
||||
|
||||
/// <summary>
|
||||
/// Get the last state data from the server for an entity.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary (net ID -> ComponentState)</returns>
|
||||
Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity);
|
||||
Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the size of the game state buffer from a given tick.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="fromTick">The tick to calculate from.</param>
|
||||
int CalculateBufferSize(GameTick fromTick);
|
||||
|
||||
bool TryGetLastServerStates(EntityUid entity,
|
||||
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary);
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.Collections;
|
||||
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.Prototypes;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
@@ -21,21 +21,20 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
sealed class NetEntityOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _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 int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
|
||||
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.
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
private readonly Font _font;
|
||||
private readonly int _lineHeight;
|
||||
private readonly List<NetEntity> _netEnts = new();
|
||||
private readonly Dictionary<EntityUid, NetEntData> _netEnts = new();
|
||||
|
||||
public NetEntityOverlay()
|
||||
{
|
||||
@@ -45,87 +44,58 @@ 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)
|
||||
{
|
||||
if(_gameTiming.InPrediction) // we only care about real server states.
|
||||
return;
|
||||
|
||||
// Shift traffic history down one
|
||||
for (var i = 0; i < _netEnts.Count; i++)
|
||||
{
|
||||
var traffic = _netEnts[i].Traffic;
|
||||
for (int j = 1; j < TrafficHistorySize; j++)
|
||||
{
|
||||
traffic[j - 1] = traffic[j];
|
||||
}
|
||||
|
||||
traffic[^1] = 0;
|
||||
}
|
||||
|
||||
var gameState = args.AppliedState;
|
||||
|
||||
if(gameState.EntityStates.HasContents)
|
||||
if (!gameState.EntityStates.HasContents)
|
||||
return;
|
||||
|
||||
foreach (var entityState in gameState.EntityStates.Span)
|
||||
{
|
||||
// Loop over every entity that gets updated this state and record the traffic
|
||||
foreach (var entityState in gameState.EntityStates.Span)
|
||||
if (!_netEnts.TryGetValue(entityState.Uid, out var netEnt))
|
||||
{
|
||||
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)
|
||||
if (_netEnts.Count >= _maxEnts)
|
||||
continue;
|
||||
|
||||
var newNetEnt = new NetEntity(entityState.Uid);
|
||||
newNetEnt.Traffic[^1] = 1;
|
||||
newNetEnt.LastUpdate = gameState.ToSequence;
|
||||
_netEnts.Add(newNetEnt);
|
||||
_netEnts[entityState.Uid] = netEnt = new();
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if (!netEnt.InPVS && netEnt.LastUpdate < gameState.ToSequence)
|
||||
{
|
||||
//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;
|
||||
netEnt.InPVS = true;
|
||||
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.PvsEnter);
|
||||
}
|
||||
else
|
||||
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.Data);
|
||||
|
||||
netEnt.Exists = false;
|
||||
if (netEnt.LastUpdate.Value + timeout < _gameTiming.LastRealTick.Value)
|
||||
{
|
||||
_netEnts.RemoveAt(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (netEnt.LastUpdate < gameState.ToSequence)
|
||||
netEnt.LastUpdate = gameState.ToSequence;
|
||||
|
||||
_netEnts[i] = netEnt; // copy struct back
|
||||
//TODO: calculate size of state and record it here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,145 +109,128 @@ 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;
|
||||
|
||||
for (int i = 0; i < _netEnts.Count; i++)
|
||||
int i = 0;
|
||||
foreach (var (uid, netEnt) in _netEnts)
|
||||
{
|
||||
var netEnt = _netEnts[i];
|
||||
var uid = netEnt.Id;
|
||||
|
||||
if (!_entityManager.EntityExists(uid))
|
||||
{
|
||||
_netEnts.RemoveSwap(i);
|
||||
i--;
|
||||
_netEnts.Remove(uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
var xPos = 100;
|
||||
var yPos = 10 + _lineHeight * i;
|
||||
var name = $"({netEnt.Id}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
|
||||
var color = CalcTextColor(ref netEnt);
|
||||
var yPos = 10 + _lineHeight * i++;
|
||||
var name = $"({uid}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
|
||||
var color = netEnt.TextColor(_gameTiming);
|
||||
screenHandle.DrawString(_font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
|
||||
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
|
||||
DrawTrafficBox(screenHandle, netEnt, xPos, yPos);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTrafficBox(DrawingHandleScreen handle, ref NetEntity netEntity, int x, int y)
|
||||
private void DrawTrafficBox(DrawingHandleScreen handle, NetEntData 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 (int i = 0; i < TrafficHistorySize; i++)
|
||||
for (uint i = 1; i <= TrafficHistorySize; i++)
|
||||
{
|
||||
if(traffic[i] == 0)
|
||||
if (!traffic.TryGetValue(_gameTiming.LastRealTick + (i - TrafficHistorySize), out var tickData))
|
||||
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.Green);
|
||||
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), color);
|
||||
}
|
||||
}
|
||||
|
||||
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 struct NetEntity
|
||||
private sealed class NetEntData
|
||||
{
|
||||
public GameTick LastUpdate;
|
||||
public readonly EntityUid Id;
|
||||
public readonly int[] Traffic;
|
||||
public bool Exists;
|
||||
public bool InPVS;
|
||||
public GameTick LastUpdate = GameTick.Zero;
|
||||
public readonly OverflowDictionary<GameTick, EntState> Traffic = new((int) TrafficHistorySize);
|
||||
public bool Exists = true;
|
||||
public bool InPVS = true;
|
||||
|
||||
public NetEntity(EntityUid id)
|
||||
public Color TextColor(IClientGameTiming timing)
|
||||
{
|
||||
LastUpdate = GameTick.Zero;
|
||||
Id = id;
|
||||
Traffic = new int[TrafficHistorySize];
|
||||
Exists = true;
|
||||
InPVS = true;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NetEntityReportCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "net_entityreport";
|
||||
public string Help => "net_entityreport <0|1>";
|
||||
public string Help => "net_entityreport";
|
||||
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(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
if (!overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
{
|
||||
overlayMan.AddOverlay(new NetEntityOverlay());
|
||||
shell.WriteLine("Enabled network entity report overlay.");
|
||||
}
|
||||
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
|
||||
else
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
@@ -28,6 +29,7 @@ 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 />
|
||||
@@ -84,38 +86,46 @@ namespace Robust.Client.GameStates
|
||||
var sb = new StringBuilder();
|
||||
foreach (var entState in entStates.Span)
|
||||
{
|
||||
if (entState.Uid == WatchEntId)
|
||||
{
|
||||
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}");
|
||||
if (entState.Uid != WatchEntId)
|
||||
continue;
|
||||
|
||||
if(compChange.State is not null)
|
||||
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
|
||||
}
|
||||
}
|
||||
if (!entState.ComponentChanges.HasContents)
|
||||
{
|
||||
sb.Append("\n Entered PVS");
|
||||
break;
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,17 +165,16 @@ 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;
|
||||
@@ -175,7 +184,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));
|
||||
|
||||
@@ -211,25 +220,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()
|
||||
@@ -242,32 +251,19 @@ namespace Robust.Client.GameStates
|
||||
private sealed class NetShowGraphCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "net_graph";
|
||||
public string Help => "net_graph <0|1>";
|
||||
public string Help => "net_graph";
|
||||
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(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
if(!overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
{
|
||||
overlayMan.AddOverlay(new NetGraphOverlay());
|
||||
shell.WriteLine("Enabled network overlay.");
|
||||
}
|
||||
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
else
|
||||
{
|
||||
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
|
||||
shell.WriteLine("Disabled network overlay.");
|
||||
@@ -283,13 +279,12 @@ namespace Robust.Client.GameStates
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
EntityUid eValue;
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 1 argument.");
|
||||
return;
|
||||
eValue = IoCManager.Resolve<IPlayerManager>().LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var eValue))
|
||||
else if (!EntityUid.TryParse(args[0], out eValue))
|
||||
{
|
||||
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
|
||||
return;
|
||||
@@ -297,12 +292,13 @@ namespace Robust.Client.GameStates
|
||||
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
if (!overlayMan.TryGetOverlay(out NetGraphOverlay? overlay))
|
||||
{
|
||||
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
|
||||
|
||||
netOverlay.WatchEntId = eValue;
|
||||
overlay = new();
|
||||
overlayMan.AddOverlay(overlay);
|
||||
}
|
||||
|
||||
overlay.WatchEntId = eValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace Robust.Client.Graphics
|
||||
bool RemoveOverlay(Type overlayClass);
|
||||
bool RemoveOverlay<T>() where T : Overlay;
|
||||
|
||||
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
|
||||
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
|
||||
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
|
||||
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
|
||||
|
||||
Overlay GetOverlay(Type overlayClass);
|
||||
T GetOverlay<T>() where T : Overlay;
|
||||
|
||||
@@ -10,6 +10,14 @@ 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
|
||||
|
||||
@@ -5,6 +5,20 @@ 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();
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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;
|
||||
@@ -19,7 +20,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(IGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
|
||||
public DebugMonitors(IClientGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
|
||||
IInputManager inputManager, IStateManager stateManager, IClyde displayManager, IClientNetManager netManager,
|
||||
IMapManager mapManager)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -10,13 +11,13 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
public sealed class DebugTimePanel : PanelContainer
|
||||
{
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly IClientGameTiming _gameTiming;
|
||||
private readonly IClientGameStateManager _gameState;
|
||||
|
||||
private readonly char[] _textBuffer = new char[256];
|
||||
private readonly Label _contents;
|
||||
|
||||
public DebugTimePanel(IGameTiming gameTiming, IClientGameStateManager gameState)
|
||||
public DebugTimePanel(IClientGameTiming gameTiming, IClientGameStateManager gameState)
|
||||
{
|
||||
_gameTiming = gameTiming;
|
||||
_gameState = gameState;
|
||||
@@ -53,7 +54,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}/{_gameTiming.CurTick - 1}, CurServerTick: {_gameState.CurServerTick}, Pred: {_gameTiming.CurTick.Value - _gameState.CurServerTick.Value - 1}
|
||||
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}, LastProcessed: {_gameTiming.LastProcessedTick}, LastRealTick: {_gameTiming.LastRealTick}, Pred: {_gameTiming.CurTick.Value - _gameTiming.LastRealTick.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}");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -26,7 +27,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 IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
@@ -646,9 +646,6 @@ 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())
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using System;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
/// <summary>
|
||||
@@ -16,5 +20,9 @@ namespace Robust.Server.GameStates
|
||||
void SendGameStateUpdate();
|
||||
|
||||
ushort TransformNetId { get; set; }
|
||||
|
||||
Action<ICommonSession, GameTick, GameTick>? ClientAck { get; set; }
|
||||
|
||||
Action<ICommonSession, GameTick, GameTick>? ClientRequestFull { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Robust.Server.GameStates;
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
public enum PVSEntityVisiblity : byte
|
||||
{
|
||||
|
||||
@@ -11,10 +11,11 @@ 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;
|
||||
@@ -27,15 +28,16 @@ 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;
|
||||
|
||||
private static TransformComponentState _transformCullState =
|
||||
new(Vector2.Zero, Angle.Zero, EntityUid.Invalid, false, false);
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of pooled objects
|
||||
@@ -58,18 +60,16 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
/// </summary>
|
||||
public HashSet<ICommonSession> SeenAllEnts = 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 readonly Dictionary<ICommonSession, SessionPVSData> _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*TickBuffer);
|
||||
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize);
|
||||
|
||||
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
|
||||
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
|
||||
@@ -97,10 +97,14 @@ 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 =>
|
||||
@@ -123,6 +127,9 @@ 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();
|
||||
}
|
||||
|
||||
@@ -158,9 +165,75 @@ 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;
|
||||
@@ -171,7 +244,6 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
CullingEnabled = value;
|
||||
}
|
||||
|
||||
|
||||
public void ProcessCollections()
|
||||
{
|
||||
foreach (var collection in _pvsCollections)
|
||||
@@ -228,6 +300,15 @@ 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)
|
||||
@@ -269,24 +350,35 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
{
|
||||
if (e.NewStatus == SessionStatus.InGame)
|
||||
{
|
||||
_playerVisibleSets.Add(e.Session, new OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>(TickBuffer, _visSetPool.Return));
|
||||
_playerVisibleSets.Add(e.Session, new());
|
||||
foreach (var pvsCollection in _pvsCollections)
|
||||
{
|
||||
pvsCollection.AddPlayer(e.Session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (e.NewStatus == SessionStatus.Disconnected)
|
||||
|
||||
if (e.NewStatus != SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
foreach (var pvsCollection in _pvsCollections)
|
||||
{
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +658,7 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(IPlayerSession session,
|
||||
public (List<EntityState>? updates, List<EntityUid>? deletions, List<EntityUid>? leftPvs, GameTick fromTick) CalculateEntityStates(IPlayerSession session,
|
||||
GameTick fromTick, GameTick toTick,
|
||||
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache,
|
||||
HashSet<int> chunkIndices, EntityQuery<MetaDataComponent> mQuery, EntityQuery<TransformComponent> tQuery,
|
||||
@@ -575,8 +667,13 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
DebugTools.Assert(session.Status == SessionStatus.InGame);
|
||||
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
|
||||
var entitiesSent = 0;
|
||||
_playerVisibleSets[session].TryGetValue(fromTick, out var playerVisibleSet);
|
||||
var sessionData = _playerVisibleSets[session];
|
||||
sessionData.SentEntities.TryGetValue(toTick - 1, out var lastSent);
|
||||
var lastAcked = sessionData.LastAcked;
|
||||
var lastSeen = sessionData.LastSeenAt;
|
||||
var visibleEnts = _visSetPool.Get();
|
||||
DebugTools.Assert(visibleEnts.Count == 0);
|
||||
|
||||
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
|
||||
|
||||
foreach (var i in chunkIndices)
|
||||
@@ -585,7 +682,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, playerVisibleSet, visibleEnts, fromTick,
|
||||
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, lastAcked, lastSent, visibleEnts, fromTick,
|
||||
ref entitiesSent, cache.Value.metadata, in enteredEntityBudget);
|
||||
}
|
||||
}
|
||||
@@ -594,7 +691,7 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
while (globalEnumerator.MoveNext())
|
||||
{
|
||||
var uid = globalEnumerator.Current;
|
||||
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, fromTick,
|
||||
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
|
||||
}
|
||||
globalEnumerator.Dispose();
|
||||
@@ -603,14 +700,14 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
while (localEnumerator.MoveNext())
|
||||
{
|
||||
var uid = localEnumerator.Current;
|
||||
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, fromTick,
|
||||
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
|
||||
}
|
||||
localEnumerator.Dispose();
|
||||
|
||||
foreach (var viewerEntity in viewerEntities)
|
||||
{
|
||||
RecursivelyAddOverride(in viewerEntity, playerVisibleSet, visibleEnts, fromTick,
|
||||
RecursivelyAddOverride(in viewerEntity, lastAcked, lastSent, visibleEnts, fromTick,
|
||||
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
|
||||
}
|
||||
|
||||
@@ -618,56 +715,87 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
RaiseLocalEvent(ref expandEvent);
|
||||
foreach (var entityUid in expandEvent.Entities)
|
||||
{
|
||||
RecursivelyAddOverride(in entityUid, playerVisibleSet, visibleEnts, fromTick,
|
||||
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, fromTick,
|
||||
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
|
||||
}
|
||||
|
||||
var entityStates = new List<EntityState>();
|
||||
|
||||
foreach (var (entityUid, visiblity) in visibleEnts)
|
||||
foreach (var (uid, visiblity) in visibleEnts)
|
||||
{
|
||||
if (sessionData.RequestedFull)
|
||||
{
|
||||
entityStates.Add(GetFullEntityState(session, uid, mQuery.GetComponent(uid)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (visiblity == PVSEntityVisiblity.StayedUnchanged)
|
||||
continue;
|
||||
|
||||
var @new = visiblity == PVSEntityVisiblity.Entered;
|
||||
var state = GetEntityState(session, entityUid, @new ? GameTick.Zero : fromTick, mQuery.GetComponent(entityUid).Flags);
|
||||
var entered = visiblity == PVSEntityVisiblity.Entered;
|
||||
var entFromTick = entered ? lastSeen.GetValueOrDefault(uid) : fromTick;
|
||||
var state = GetEntityState(session, uid, entFromTick, mQuery.GetComponent(uid));
|
||||
|
||||
//this entity is not new & nothing changed
|
||||
if(!@new && state.Empty) continue;
|
||||
|
||||
entityStates.Add(state);
|
||||
if (entered || !state.Empty)
|
||||
entityStates.Add(state);
|
||||
}
|
||||
|
||||
if(playerVisibleSet != null)
|
||||
// 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))
|
||||
{
|
||||
foreach (var (entityUid, _) in playerVisibleSet)
|
||||
if (oldEntry.Value.Key > fromTick && sessionData.Overflow == null)
|
||||
{
|
||||
// it was deleted, so we dont need to exit pvs
|
||||
if (deletions.Contains(entityUid)) continue;
|
||||
// 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.
|
||||
|
||||
//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));
|
||||
// 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.");
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
private void RecursivelyAddTreeNode(in EntityUid nodeIndex,
|
||||
private bool RecursivelyAddTreeNode(in EntityUid nodeIndex,
|
||||
RobustTree<EntityUid> tree,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
|
||||
GameTick fromTick,
|
||||
ref int totalEnteredEntities,
|
||||
@@ -683,12 +811,12 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
if (nodeIndex.IsValid() && !toSend.ContainsKey(nodeIndex))
|
||||
{
|
||||
//are we new?
|
||||
var (entered, budgetFail) = ProcessEntry(in nodeIndex, previousVisibleEnts,
|
||||
var (entered, budgetFull) = ProcessEntry(in nodeIndex, lastAcked, lastSent,
|
||||
ref totalEnteredEntities, in enteredEntityBudget);
|
||||
|
||||
if (budgetFail) return;
|
||||
|
||||
AddToSendSet(in nodeIndex, metaDataCache[nodeIndex], toSend, fromTick, entered);
|
||||
|
||||
if (budgetFull) return true;
|
||||
}
|
||||
|
||||
var node = tree[nodeIndex];
|
||||
@@ -697,15 +825,19 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
{
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
RecursivelyAddTreeNode(in child, tree, previousVisibleEnts, toSend, fromTick,
|
||||
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget);
|
||||
if (RecursivelyAddTreeNode(in child, tree, lastAcked, lastSent, toSend, fromTick,
|
||||
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool RecursivelyAddOverride(
|
||||
in EntityUid uid,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
|
||||
GameTick fromTick,
|
||||
ref int totalEnteredEntities,
|
||||
@@ -721,29 +853,40 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
if (toSend.ContainsKey(uid)) return true;
|
||||
|
||||
var parent = transQuery.GetComponent(uid).ParentUid;
|
||||
if (parent.IsValid() && !RecursivelyAddOverride(in parent, previousVisibleEnts, toSend, fromTick,
|
||||
if (parent.IsValid() && !RecursivelyAddOverride(in parent, lastAcked, lastSent, toSend, fromTick,
|
||||
ref totalEnteredEntities, metaQuery, transQuery, in enteredEntityBudget))
|
||||
return false;
|
||||
|
||||
var (entered, _) = ProcessEntry(in uid, previousVisibleEnts,
|
||||
var (entered, _) = ProcessEntry(in uid, lastAcked, lastSent,
|
||||
ref totalEnteredEntities, in enteredEntityBudget);
|
||||
|
||||
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, entered);
|
||||
return true;
|
||||
}
|
||||
|
||||
private (bool entered, bool budgetFail) ProcessEntry(in EntityUid uid,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
|
||||
private (bool entering, bool budgetFull) ProcessEntry(in EntityUid uid,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
|
||||
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
|
||||
ref int totalEnteredEntities, in int enteredEntityBudget)
|
||||
{
|
||||
var entered = previousVisibleEnts?.Remove(uid) == false;
|
||||
var enteredSinceLastSent = lastSent == null || !lastSent.ContainsKey(uid);
|
||||
|
||||
if (entered)
|
||||
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 (totalEnteredEntities >= enteredEntityBudget)
|
||||
// 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)
|
||||
return (entered, true);
|
||||
|
||||
totalEnteredEntities++;
|
||||
}
|
||||
|
||||
return (entered, false);
|
||||
@@ -757,7 +900,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);
|
||||
@@ -771,7 +914,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>? updates, List<EntityUid>? deletions) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
|
||||
public (List<EntityState>?, List<EntityUid>?, List<EntityUid>?, GameTick fromTick) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
|
||||
{
|
||||
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
|
||||
// no point sending an empty collection
|
||||
@@ -791,10 +934,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.Flags));
|
||||
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md));
|
||||
}
|
||||
|
||||
return (stateEntities.Count == 0 ? default : stateEntities, deletions);
|
||||
return (stateEntities.Count == 0 ? default : stateEntities, deletions, null, fromTick);
|
||||
}
|
||||
|
||||
// Just get the relevant entities that have been dirtied
|
||||
@@ -819,8 +962,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.Flags));
|
||||
if (md.EntityLastModifiedTick > fromTick)
|
||||
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md));
|
||||
}
|
||||
|
||||
foreach (var uid in dirty)
|
||||
@@ -832,8 +975,8 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
|
||||
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
|
||||
|
||||
if (md.EntityLastModifiedTick >= fromTick)
|
||||
stateEntities.Add(GetEntityState(player, uid, fromTick, md.Flags));
|
||||
if (md.EntityLastModifiedTick > fromTick)
|
||||
stateEntities.Add(GetEntityState(player, uid, fromTick, md));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -842,7 +985,7 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
{
|
||||
if (stateEntities.Count == 0) stateEntities = default;
|
||||
|
||||
return (stateEntities, deletions);
|
||||
return (stateEntities, deletions, null, fromTick);
|
||||
}
|
||||
|
||||
stateEntities = new List<EntityState>(EntityManager.EntityCount);
|
||||
@@ -853,13 +996,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.Flags));
|
||||
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md));
|
||||
}
|
||||
|
||||
// no point sending an empty collection
|
||||
if (stateEntities.Count == 0) stateEntities = default;
|
||||
|
||||
return (stateEntities, deletions);
|
||||
return (stateEntities, deletions, null, fromTick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -868,20 +1011,27 @@ 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="flags">Any applicable metadata flags</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>
|
||||
/// <returns>New entity State for the given entity.</returns>
|
||||
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataFlags flags)
|
||||
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
|
||||
{
|
||||
var bus = EntityManager.EventBus;
|
||||
var changed = new List<ComponentChange>();
|
||||
// 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;
|
||||
|
||||
// Whether this entity has any component states that should only be sent to specific sessions.
|
||||
var entitySpecific = (meta.Flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
|
||||
|
||||
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
|
||||
{
|
||||
DebugTools.Assert(component.Initialized);
|
||||
if (!component.NetSyncEnabled)
|
||||
continue;
|
||||
|
||||
if (component.Deleted || !component.Initialized)
|
||||
{
|
||||
_sawmill.Error("Entity manager returned deleted or uninitialized components while sending entity data");
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
|
||||
// "not different from entity creation".
|
||||
@@ -892,37 +1042,20 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
|
||||
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
|
||||
|
||||
var addState = false;
|
||||
var changeState = false;
|
||||
var addState = component.CreationTick != GameTick.Zero && component.CreationTick > fromTick;
|
||||
var changedState = component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick > fromTick;
|
||||
|
||||
// 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)
|
||||
if (!(addState || changedState))
|
||||
continue;
|
||||
|
||||
if (specificStates && !EntityManager.CanGetComponentState(bus, component, player))
|
||||
if (component.SendOnlyToOwner && player.AttachedEntity != component.Owner)
|
||||
continue;
|
||||
|
||||
if (addState)
|
||||
{
|
||||
ComponentState? state = null;
|
||||
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero &&
|
||||
component.LastModifiedTick >= fromTick)
|
||||
state = EntityManager.GetComponentState(bus, component);
|
||||
if (entitySpecific && !EntityManager.CanGetComponentState(bus, component, player))
|
||||
continue;
|
||||
|
||||
// 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)));
|
||||
}
|
||||
var state = changedState ? EntityManager.GetComponentState(bus, component) : null;
|
||||
changed.Add(ComponentChange.Added(netId, state, component.LastModifiedTick));
|
||||
}
|
||||
|
||||
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, fromTick))
|
||||
@@ -930,7 +1063,38 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
changed.Add(ComponentChange.Removed(netId));
|
||||
}
|
||||
|
||||
return new EntityState(entityUid, changed.ToArray());
|
||||
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);
|
||||
}
|
||||
|
||||
private EntityUid[] GetSessionViewers(ICommonSession session)
|
||||
@@ -1030,6 +1194,39 @@ 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]
|
||||
|
||||
@@ -22,6 +22,7 @@ using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using SharpZstd.Interop;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
@@ -51,6 +52,9 @@ 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");
|
||||
@@ -60,7 +64,9 @@ 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;
|
||||
@@ -118,10 +124,7 @@ namespace Robust.Server.GameStates
|
||||
|
||||
private void HandleClientConnected(object? sender, NetChannelArgs e)
|
||||
{
|
||||
if (!_ackedStates.ContainsKey(e.Channel.ConnectionId))
|
||||
_ackedStates.Add(e.Channel.ConnectionId, GameTick.Zero);
|
||||
else
|
||||
_ackedStates[e.Channel.ConnectionId] = GameTick.Zero;
|
||||
_ackedStates[e.Channel.ConnectionId] = GameTick.Zero;
|
||||
}
|
||||
|
||||
private void HandleClientDisconnect(object? sender, NetChannelArgs e)
|
||||
@@ -129,38 +132,36 @@ namespace Robust.Server.GameStates
|
||||
_ackedStates.Remove(e.Channel.ConnectionId);
|
||||
}
|
||||
|
||||
private void HandleStateAck(MsgStateAck msg)
|
||||
private void HandleFullStateRequest(MsgStateRequestFull msg)
|
||||
{
|
||||
Ack(msg.MsgChannel.ConnectionId, msg.Sequence);
|
||||
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 Ack(long uniqueIdentifier, GameTick stateAcked)
|
||||
private void HandleStateAck(MsgStateAck msg)
|
||||
{
|
||||
DebugTools.Assert(_networkManager.IsServer);
|
||||
if (_playerManager.TryGetSessionById(msg.MsgChannel.UserId, out var session))
|
||||
Ack(msg.MsgChannel.ConnectionId, msg.Sequence, session);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
private void Ack(long uniqueIdentifier, GameTick stateAcked, IPlayerSession playerSession)
|
||||
{
|
||||
if (!_ackedStates.TryGetValue(uniqueIdentifier, out var lastAck) || stateAcked <= lastAck)
|
||||
return;
|
||||
|
||||
//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?");
|
||||
ClientAck?.Invoke(playerSession, stateAcked, lastAck);
|
||||
_ackedStates[uniqueIdentifier] = stateAcked;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendGameStateUpdate()
|
||||
{
|
||||
DebugTools.Assert(_networkManager.IsServer);
|
||||
|
||||
if (!_networkManager.IsConnected)
|
||||
{
|
||||
// Prevent deletions piling up if we have no clients.
|
||||
@@ -257,7 +258,7 @@ namespace Robust.Server.GameStates
|
||||
DebugTools.Assert("Why does this channel not have an entry?");
|
||||
}
|
||||
|
||||
var (entStates, deletions) = _pvs.CullingEnabled
|
||||
var (entStates, deletions, leftPvs, fromTick) = _pvs.CullingEnabled
|
||||
? _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick, chunkCache,
|
||||
playerChunks[sessionIndex], metadataQuery, transformQuery, viewerEntities[sessionIndex])
|
||||
: _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
|
||||
@@ -267,7 +268,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(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
|
||||
var state = new GameState(fromTick, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
|
||||
entStates, playerStates, deletions, mapData);
|
||||
|
||||
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
|
||||
@@ -277,6 +278,8 @@ 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).
|
||||
@@ -286,11 +289,15 @@ 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)
|
||||
{
|
||||
_ackedStates[channel.ConnectionId] = _gameTiming.CurTick;
|
||||
Ack(channel.ConnectionId, _gameTiming.CurTick, session);
|
||||
}
|
||||
}
|
||||
|
||||
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (_pvs.CullingEnabled)
|
||||
|
||||
@@ -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);
|
||||
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The target number of game states to keep buffered up to smooth out against network inconsistency.
|
||||
/// The target number of game states to keep buffered up to smooth out network inconsistency.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> NetInterpRatio =
|
||||
CVarDef.Create("net.interp_ratio", 0, CVar.ARCHIVE);
|
||||
public static readonly CVarDef<int> NetBufferSize =
|
||||
CVarDef.Create("net.buffer_size", 0, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Enable verbose game state/networking logging.
|
||||
@@ -137,6 +137,12 @@ 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>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Collections;
|
||||
|
||||
@@ -76,6 +74,36 @@ 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.
|
||||
|
||||
@@ -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.LastRealTick)
|
||||
if (msg.Tick > _timing.CurTick)
|
||||
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.LastRealTick)
|
||||
_sawmill.Warning($"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.LastRealTick} ).");
|
||||
if(msg.Tick != default && msg.Tick < _timing.CurTick)
|
||||
_sawmill.Warning($"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.CurTick} ).");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; set; } = true;
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables]
|
||||
@@ -34,6 +34,13 @@ 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.
|
||||
|
||||
@@ -60,6 +60,12 @@ 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>
|
||||
@@ -185,13 +191,21 @@ namespace Robust.Shared.GameObjects
|
||||
public enum MetaDataFlags : byte
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity has states specific to a particular player.
|
||||
/// Whether the entity has states specific to particular players. This will cause many state-attempt events to
|
||||
/// be raised, and is generally somewhat expensive.
|
||||
/// </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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,6 +1123,7 @@ 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);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -27,6 +28,7 @@ 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
|
||||
@@ -250,19 +252,17 @@ namespace Robust.Shared.GameObjects
|
||||
/// <remarks>
|
||||
/// Calling Dirty on a component will call this directly.
|
||||
/// </remarks>
|
||||
public void Dirty(EntityUid uid)
|
||||
public virtual 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 == currentTick) return;
|
||||
if (metadata.EntityLastModifiedTick == _gameTiming.CurTick) return;
|
||||
|
||||
metadata.EntityLastModifiedTick = currentTick;
|
||||
metadata.EntityLastModifiedTick = _gameTiming.CurTick;
|
||||
|
||||
if (metadata.EntityLifeStage > EntityLifeStage.Initializing)
|
||||
{
|
||||
@@ -270,7 +270,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public void Dirty(Component component)
|
||||
public virtual void Dirty(Component component)
|
||||
{
|
||||
var owner = component.Owner;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Robust.Shared.Serialization;
|
||||
using System;
|
||||
using NetSerializer;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
@@ -13,10 +14,13 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public bool Empty => ComponentChanges.Value is null or { Count: 0 };
|
||||
|
||||
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents, bool hide = false)
|
||||
public readonly GameTick EntityLastModified;
|
||||
|
||||
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents, GameTick lastModified)
|
||||
{
|
||||
Uid = uid;
|
||||
ComponentChanges = changedComponents;
|
||||
EntityLastModified = lastModified;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +49,15 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
public readonly ushort NetID;
|
||||
|
||||
public ComponentChange(ushort netId, bool created, bool deleted, ComponentState? state)
|
||||
public readonly GameTick LastModifiedTick;
|
||||
|
||||
public ComponentChange(ushort netId, bool created, bool deleted, ComponentState? state, GameTick lastModifiedTick)
|
||||
{
|
||||
Deleted = deleted;
|
||||
State = state;
|
||||
NetID = netId;
|
||||
Created = created;
|
||||
LastModifiedTick = lastModifiedTick;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@@ -58,19 +65,19 @@ namespace Robust.Shared.GameObjects
|
||||
return $"{(Deleted ? "D" : "C")} {NetID} {State?.GetType().Name}";
|
||||
}
|
||||
|
||||
public static ComponentChange Added(ushort netId, ComponentState? state)
|
||||
public static ComponentChange Added(ushort netId, ComponentState? state, GameTick lastModifiedTick)
|
||||
{
|
||||
return new(netId, true, false, state);
|
||||
return new(netId, true, false, state, lastModifiedTick);
|
||||
}
|
||||
|
||||
public static ComponentChange Changed(ushort netId, ComponentState state)
|
||||
public static ComponentChange Changed(ushort netId, ComponentState state, GameTick lastModifiedTick)
|
||||
{
|
||||
return new(netId, false, false, state);
|
||||
return new(netId, false, false, state, lastModifiedTick);
|
||||
}
|
||||
|
||||
public static ComponentChange Removed(ushort netId)
|
||||
{
|
||||
return new(netId, false, true, null);
|
||||
return new(netId, false, true, null, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ public abstract partial class SharedTransformSystem
|
||||
DebugTools.Assert(!xform.Anchored);
|
||||
}
|
||||
|
||||
public void DetachParentToNull(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery)
|
||||
public void DetachParentToNull(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery, TransformComponent? oldConcrete = null)
|
||||
{
|
||||
var oldParent = xform._parent;
|
||||
|
||||
@@ -763,7 +763,7 @@ public abstract partial class SharedTransformSystem
|
||||
RaiseLocalEvent(xform.Owner, ref anchorStateChangedEvent, true);
|
||||
}
|
||||
|
||||
var oldConcrete = xformQuery.GetComponent(oldParent);
|
||||
oldConcrete ??= xformQuery.GetComponent(oldParent);
|
||||
oldConcrete._children.Remove(xform.Owner);
|
||||
|
||||
xform._parent = EntityUid.Invalid;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
@@ -11,13 +11,6 @@ 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
private bool _hasWritten;
|
||||
internal 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,8 +94,7 @@ namespace Robust.Shared.Network.Messages
|
||||
buffer.Write(stateStream.AsSpan());
|
||||
}
|
||||
|
||||
|
||||
_hasWritten = false;
|
||||
_hasWritten = true;
|
||||
MsgSize = buffer.LengthBytes;
|
||||
}
|
||||
|
||||
@@ -106,13 +105,7 @@ namespace Robust.Shared.Network.Messages
|
||||
/// <returns></returns>
|
||||
public bool ShouldSendReliably()
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
|
||||
return MsgSize > ReliableThreshold;
|
||||
}
|
||||
|
||||
|
||||
47
Robust.Shared/Network/Messages/MsgStateLeavePvs.cs
Normal file
47
Robust.Shared/Network/Messages/MsgStateLeavePvs.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
25
Robust.Shared/Network/Messages/MsgStateRequestFull.cs
Normal file
25
Robust.Shared/Network/Messages/MsgStateRequestFull.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ namespace Robust.Shared.Physics
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FixturesComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<FixturesComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<FixturesComponent, ComponentHandleState>(OnHandleState);
|
||||
@@ -288,8 +289,7 @@ namespace Robust.Shared.Physics
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? physics))
|
||||
{
|
||||
DebugTools.Assert(false);
|
||||
Logger.ErrorS("physics", $"Tried to apply fixture state for {uid} which has name {nameof(PhysicsComponent)}");
|
||||
Logger.ErrorS("physics", $"Tried to apply fixture state for an entity without physics: {ToPrettyString(uid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Exceptions;
|
||||
@@ -177,12 +177,13 @@ namespace Robust.Shared.Timing
|
||||
}
|
||||
#endif
|
||||
_timing.InSimulation = true;
|
||||
var tickPeriod = CalcTickPeriod();
|
||||
var tickPeriod = _timing.CalcAdjustedTickPeriod();
|
||||
|
||||
using (_prof.Group("Ticks"))
|
||||
{
|
||||
var countTicksRan = 0;
|
||||
// run the simulation for every accumulated tick
|
||||
|
||||
while (accumulator >= tickPeriod)
|
||||
{
|
||||
accumulator -= tickPeriod;
|
||||
@@ -192,6 +193,7 @@ namespace Robust.Shared.Timing
|
||||
if (_timing.Paused)
|
||||
continue;
|
||||
|
||||
_timing.TickRemainder = accumulator;
|
||||
countTicksRan += 1;
|
||||
|
||||
// update the simulation
|
||||
@@ -237,7 +239,7 @@ namespace Robust.Shared.Timing
|
||||
}
|
||||
#endif
|
||||
_timing.CurTick = new GameTick(_timing.CurTick.Value + 1);
|
||||
tickPeriod = CalcTickPeriod();
|
||||
tickPeriod = _timing.CalcAdjustedTickPeriod();
|
||||
|
||||
if (SingleStep)
|
||||
_timing.Paused = true;
|
||||
@@ -304,14 +306,6 @@ 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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Timing
|
||||
@@ -153,6 +154,14 @@ 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.
|
||||
@@ -251,14 +260,11 @@ namespace Robust.Shared.Timing
|
||||
public bool IsFirstTimePredicted { get; protected set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InPrediction => !ApplyingState && CurTick > LastRealTick;
|
||||
public virtual bool InPrediction => false;
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -102,6 +102,8 @@ 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>
|
||||
@@ -148,11 +150,6 @@ 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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Moq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -12,27 +13,27 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
[Test]
|
||||
public void FillBufferBlocksProcessing()
|
||||
{
|
||||
var timingMock = new Mock<IGameTiming>();
|
||||
var timingMock = new Mock<IClientGameTiming>();
|
||||
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.CurTick = new GameTick(3);
|
||||
var result = processor.ProcessTickStates(new GameTick(1), out _, out _);
|
||||
timing.LastProcessedTick = new GameTick(0);
|
||||
var result = processor.TryGetServerState(out _, out _);
|
||||
|
||||
Assert.That(result, Is.False);
|
||||
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FillBufferAndCalculateFirstState()
|
||||
{
|
||||
var timingMock = new Mock<IGameTiming>();
|
||||
var timingMock = new Mock<IClientGameTiming>();
|
||||
timingMock.SetupProperty(p => p.CurTick);
|
||||
|
||||
var timing = timingMock.Object;
|
||||
@@ -43,12 +44,11 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
|
||||
|
||||
// calculate states for first tick
|
||||
timing.CurTick = new GameTick(1);
|
||||
var result = processor.ProcessTickStates(new GameTick(1), out var curState, out var nextState);
|
||||
timing.LastProcessedTick = new GameTick(0);
|
||||
var result = processor.TryGetServerState(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<IGameTiming>();
|
||||
var timingMock = new Mock<IClientGameTiming>();
|
||||
timingMock.SetupProperty(p => p.CurTick);
|
||||
|
||||
var timing = timingMock.Object;
|
||||
@@ -71,10 +71,11 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
|
||||
|
||||
// calculate states for first tick
|
||||
timing.CurTick = new GameTick(3);
|
||||
processor.ProcessTickStates(timing.CurTick, out _, out _);
|
||||
timing.LastProcessedTick = new GameTick(2);
|
||||
processor.TryGetServerState(out var state, out _);
|
||||
|
||||
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
|
||||
Assert.NotNull(state);
|
||||
Assert.That(state.ToSequence.Value, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -82,12 +83,10 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
{
|
||||
var (timing, processor) = SetupProcessorFactory();
|
||||
|
||||
processor.Extrapolation = false;
|
||||
|
||||
// a few moments later...
|
||||
timing.CurTick = new GameTick(5); // current clock is ahead of server
|
||||
timing.LastProcessedTick = new GameTick(4); // current clock is ahead of server
|
||||
processor.AddNewState(GameStateFactory(3, 4)); // received a late state
|
||||
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
|
||||
var result = processor.TryGetServerState(out _, out _);
|
||||
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
@@ -101,57 +100,13 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
{
|
||||
var (timing, processor) = SetupProcessorFactory();
|
||||
|
||||
processor.Extrapolation = false;
|
||||
|
||||
// a few moments later...
|
||||
timing.CurTick = new GameTick(5); // current clock is ahead of server
|
||||
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
|
||||
timing.LastProcessedTick = new GameTick(4); // current clock is ahead of server
|
||||
var result = processor.TryGetServerState(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.
|
||||
@@ -162,11 +117,10 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
var (timing, processor) = SetupProcessorFactory();
|
||||
|
||||
processor.AddNewState(GameStateFactory(4, 5));
|
||||
processor.LastProcessedRealState = new GameTick(3);
|
||||
timing.LastRealTick = new GameTick(3);
|
||||
timing.LastProcessedTick = new GameTick(3);
|
||||
|
||||
timing.CurTick = new GameTick(4);
|
||||
|
||||
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
|
||||
var result = processor.TryGetServerState(out _, out _);
|
||||
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
@@ -182,52 +136,24 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
|
||||
processor.Interpolation = true;
|
||||
|
||||
timing.CurTick = new GameTick(4);
|
||||
timing.LastProcessedTick = new GameTick(3);
|
||||
|
||||
processor.LastProcessedRealState = new GameTick(3);
|
||||
timing.LastRealTick = new GameTick(3);
|
||||
processor.AddNewState(GameStateFactory(3, 5));
|
||||
|
||||
// We're missing the state for this tick so go into extrap.
|
||||
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out _);
|
||||
var result = processor.TryGetServerState(out var curState, out _);
|
||||
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(curState, Is.Not.Null);
|
||||
Assert.That(curState!.Extrapolated, Is.True);
|
||||
Assert.That(curState, Is.Null);
|
||||
|
||||
timing.CurTick = new GameTick(5);
|
||||
timing.LastProcessedTick = new GameTick(4);
|
||||
|
||||
// But we DO have the state for the tick after so apply away!
|
||||
result = processor.ProcessTickStates(timing.CurTick, out curState, out _);
|
||||
result = processor.TryGetServerState(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>
|
||||
@@ -242,10 +168,12 @@ 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 (IGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
|
||||
private static (IClientGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
|
||||
{
|
||||
var timingMock = new Mock<IGameTiming>();
|
||||
var timingMock = new Mock<IClientGameTiming>();
|
||||
timingMock.SetupProperty(p => p.CurTick);
|
||||
timingMock.SetupProperty(p => p.LastProcessedTick);
|
||||
timingMock.SetupProperty(p => p.LastRealTick);
|
||||
timingMock.SetupProperty(p => p.TickTimingAdjustment);
|
||||
|
||||
var timing = timingMock.Object;
|
||||
@@ -255,9 +183,8 @@ namespace Robust.UnitTesting.Client.GameStates
|
||||
processor.AddNewState(GameStateFactory(1, 2));
|
||||
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
|
||||
|
||||
// calculate states for first tick
|
||||
timing.CurTick = new GameTick(1);
|
||||
processor.ProcessTickStates(timing.CurTick, out _, out _);
|
||||
processor.LastFullStateRequested = null;
|
||||
timing.LastProcessedTick = timing.LastRealTick = new GameTick(1);
|
||||
|
||||
return (timing, processor);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
|
||||
@@ -252,6 +253,10 @@ 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));
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
|
||||
new EntityUid(512),
|
||||
new []
|
||||
{
|
||||
new ComponentChange(0, true, false, new MapGridComponentState(new GridId(0), 16))
|
||||
});
|
||||
new ComponentChange(0, true, false, new MapGridComponentState(new GridId(0), 16), default)
|
||||
}, default);
|
||||
|
||||
serializer.Serialize(stream, payload);
|
||||
array = stream.ToArray();
|
||||
|
||||
Reference in New Issue
Block a user