Prediction (#1027)

This commit is contained in:
Pieter-Jan Briers
2020-04-18 17:35:54 +02:00
committed by GitHub
parent 2da1640ab7
commit 0c8f869cb4
47 changed files with 1110 additions and 137 deletions

View File

@@ -65,6 +65,7 @@ namespace Robust.Client
public void Initialize()
{
_net.RegisterNetMessage<MsgServerInfo>(MsgServerInfo.NAME, HandleServerInfo);
_net.RegisterNetMessage<MsgSetTickRate>(MsgSetTickRate.NAME, HandleSetTickRate);
_net.Connected += OnConnected;
_net.ConnectFailed += OnConnectFailed;
_net.Disconnect += OnNetDisconnect;
@@ -120,7 +121,7 @@ namespace Robust.Client
/// receiving states when they join the lobby.
/// </summary>
/// <param name="session">Session of the player.</param>
private void OnPlayerJoinedServer(PlayerSession session)
private void OnPlayerJoinedServer(IPlayerSession session)
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connected);
OnRunLevelChanged(ClientRunLevel.Connected);
@@ -135,7 +136,7 @@ namespace Robust.Client
/// Player is joining the game
/// </summary>
/// <param name="session">Session of the player.</param>
private void OnPlayerJoinedGame(PlayerSession session)
private void OnPlayerJoinedGame(IPlayerSession session)
{
DebugTools.Assert(RunLevel >= ClientRunLevel.Connected);
OnRunLevelChanged(ClientRunLevel.InGame);
@@ -193,6 +194,11 @@ namespace Robust.Client
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
}
private void HandleSetTickRate(MsgSetTickRate message)
{
_timing.TickRate = message.NewTickRate;
}
private void OnLocalStatusChanged(object obj, StatusEventArgs eventArgs)
{
// player finished fully connecting to the server.
@@ -253,12 +259,12 @@ namespace Robust.Client
/// <summary>
/// The session that triggered the event.
/// </summary>
private PlayerSession Session { get; }
private IPlayerSession Session { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public PlayerEventArgs(PlayerSession session)
public PlayerEventArgs(IPlayerSession session)
{
Session = session;
}

View File

@@ -267,13 +267,13 @@ namespace Robust.Client
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
_userInterfaceManager.Update(frameEventArgs);
_stateManager.Update(frameEventArgs);
if (_client.RunLevel >= ClientRunLevel.Connected)
{
_gameStateManager.ApplyGameState();
}
_stateManager.Update(frameEventArgs);
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
}

View File

@@ -41,11 +41,12 @@ namespace Robust.Client.GameObjects
Started = true;
}
public void ApplyEntityStates(List<EntityState> curEntStates, IEnumerable<EntityUid> deletions,
public List<EntityUid> ApplyEntityStates(List<EntityState> curEntStates, IEnumerable<EntityUid> deletions,
List<EntityState> nextEntStates)
{
var toApply = new Dictionary<IEntity, (EntityState, EntityState)>();
var toInitialize = new List<Entity>();
var created = new List<EntityUid>();
deletions ??= new EntityUid[0];
if (curEntStates != null && curEntStates.Count != 0)
@@ -64,6 +65,7 @@ namespace Robust.Client.GameObjects
var newEntity = CreateEntity(metaState.PrototypeId, es.Uid);
toApply.Add(newEntity, (es, null));
toInitialize.Add(newEntity);
created.Add(newEntity.Uid);
}
}
}
@@ -160,6 +162,8 @@ namespace Robust.Client.GameObjects
entity.Delete();
}
#endif
return created;
}
public void Dispose()

View File

@@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using Robust.Client.Interfaces.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
@@ -14,13 +18,17 @@ namespace Robust.Client.GameObjects
{
#pragma warning disable 649
[Dependency] private readonly IClientNetManager _networkManager;
[Dependency] private readonly IClientGameStateManager _gameStateManager;
[Dependency] private readonly IGameTiming _gameTiming;
#pragma warning restore 649
/// <inheritdoc />
public event EventHandler<NetworkComponentMessage> ReceivedComponentMessage;
/// <inheritdoc />
public event EventHandler<EntitySystemMessage> ReceivedSystemMessage;
public event EventHandler<object> ReceivedSystemMessage;
private readonly PriorityQueue<MsgEntity> _queue = new PriorityQueue<MsgEntity>(new MessageTickComparer());
/// <inheritdoc />
public void SetupNetworking()
@@ -28,12 +36,28 @@ namespace Robust.Client.GameObjects
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
}
public void Update()
{
while (_queue.Count != 0 && _queue.Peek().SourceTick <= _gameStateManager.CurServerTick)
{
DispatchMsgEntity(_queue.Take());
}
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntitySystemMessage message)
{
SendSystemNetworkMessage(message, default(uint));
}
public void SendSystemNetworkMessage(EntitySystemMessage message, uint sequence)
{
var msg = _networkManager.CreateNetMessage<MsgEntity>();
msg.Type = EntityMessageType.SystemMessage;
msg.SystemMessage = message;
msg.SourceTick = _gameTiming.CurTick;
msg.Sequence = sequence;
_networkManager.ClientSendMessage(msg);
}
@@ -54,10 +78,23 @@ namespace Robust.Client.GameObjects
msg.EntityUid = entity.Uid;
msg.NetId = component.NetID.Value;
msg.ComponentMessage = message;
msg.SourceTick = _gameTiming.CurTick;
_networkManager.ClientSendMessage(msg);
}
private void HandleEntityNetworkMessage(MsgEntity message)
{
if (message.SourceTick <= _gameStateManager.CurServerTick)
{
DispatchMsgEntity(message);
return;
}
_queue.Add(message);
}
private void DispatchMsgEntity(MsgEntity message)
{
switch (message.Type)
{
@@ -70,5 +107,16 @@ namespace Robust.Client.GameObjects
return;
}
}
private sealed class MessageTickComparer : IComparer<MsgEntity>
{
public int Compare(MsgEntity x, MsgEntity y)
{
DebugTools.AssertNotNull(x);
DebugTools.AssertNotNull(y);
return y.SourceTick.CompareTo(x.SourceTick);
}
}
}
}

View File

@@ -0,0 +1,23 @@
using Robust.Client.Interfaces.GameStates;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.GameObjects
{
public static class EntityManagerExt
{
public static void RaisePredictiveEvent<T>(this IEntityManager entityManager, T msg)
where T : EntitySystemMessage
{
var sequence = IoCManager.Resolve<IClientGameStateManager>().SystemMessageDispatched(msg);
entityManager.EntityNetManager.SendSystemNetworkMessage(msg, sequence);
var eventArgs = new EntitySessionEventArgs(IoCManager.Resolve<IPlayerManager>().LocalPlayer.Session);
entityManager.EventBus.RaiseEvent(EventSource.Local, msg);
entityManager.EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Robust.Client.GameObjects.Components;
using Robust.Client.Interfaces.GameStates;
using Robust.Client.Interfaces.Input;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
@@ -21,6 +23,7 @@ namespace Robust.Client.GameObjects.EntitySystems
#pragma warning disable 649
[Dependency] private readonly IInputManager _inputManager;
[Dependency] private readonly IPlayerManager _playerManager;
[Dependency] private readonly IClientGameStateManager _stateManager;
#pragma warning restore 649
private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates();
@@ -36,6 +39,11 @@ namespace Robust.Client.GameObjects.EntitySystems
/// </summary>
public ICommandBindMapping BindMap => _bindMap;
/// <summary>
/// If the input system is currently predicting input.
/// </summary>
public bool Predicted { get; private set; }
/// <summary>
/// Inserts an Input Command into the simulation.
/// </summary>
@@ -52,6 +60,11 @@ namespace Robust.Client.GameObjects.EntitySystems
#endif
// set state, state change is updated regardless if it is locally bound
if (_cmdStates.GetState(function) == message.State)
{
return;
}
_cmdStates.SetState(function, message.State);
// handle local binds before sending off
@@ -62,6 +75,32 @@ namespace Robust.Client.GameObjects.EntitySystems
return;
}
// send it off to the client
DispatchInputCommand(message);
}
/// <summary>
/// Handle a predicted input command.
/// </summary>
/// <param name="inputCmd">Input command to handle as predicted.</param>
public void PredictInputCommand(FullInputCmdMessage inputCmd)
{
var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId);
if (!_bindMap.TryGetHandler(keyFunc, out var handler))
return;
Predicted = true;
var session = _playerManager.LocalPlayer.Session;
handler.HandleCmdMessage(session, inputCmd);
Predicted = false;
}
private void DispatchInputCommand(FullInputCmdMessage message)
{
_stateManager.InputCommandDispatched(message);
RaiseNetworkEvent(message);
}

View File

@@ -1,17 +1,25 @@
using System;
using System.Collections.Generic;
using Robust.Client.GameObjects.EntitySystems;
using Robust.Client.Interfaces;
using Robust.Client.Interfaces.GameObjects;
using Robust.Client.Interfaces.GameStates;
using Robust.Client.Interfaces.Input;
using Robust.Shared.GameStates;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -19,7 +27,14 @@ namespace Robust.Client.GameStates
public class ClientGameStateManager : IClientGameStateManager
{
private GameStateProcessor _processor;
private uint _nextInputCmdSeq = 1;
private readonly Queue<FullInputCmdMessage> _pendingInputs = new Queue<FullInputCmdMessage>();
private readonly Queue<(uint sequence, GameTick sourceTick, EntitySystemMessage msg, object sessionMsg)>
_pendingSystemMessages
= new Queue<(uint, GameTick, EntitySystemMessage, object)>();
#pragma warning disable 649
[Dependency] private readonly IClientEntityManager _entities;
[Dependency] private readonly IPlayerManager _players;
@@ -28,6 +43,8 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IGameTiming _timing;
[Dependency] private readonly IConfigurationManager _config;
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
[Dependency] private readonly IComponentManager _componentManager;
#pragma warning restore 649
/// <inheritdoc />
@@ -37,7 +54,16 @@ namespace Robust.Client.GameStates
public int TargetBufferSize => _processor.TargetBufferSize;
/// <inheritdoc />
public int CurrentBufferSize => _processor.CurrentBufferSize;
public int CurrentBufferSize => _processor.CalculateBufferSize(CurServerTick);
public bool Predicting { get; private set; }
public int PredictSize { get; private set; }
private uint _lastProcessedSeq;
private GameTick _lastProcessedTick = GameTick.Zero;
public GameTick CurServerTick => _lastProcessedTick;
/// <inheritdoc />
public event Action<GameStateAppliedArgs> GameStateApplied;
@@ -51,24 +77,26 @@ namespace Robust.Client.GameStates
_network.RegisterNetMessage<MsgStateAck>(MsgStateAck.NAME);
_client.RunLevelChanged += RunLevelChanged;
if(!_config.IsCVarRegistered("net.interp"))
_config.RegisterCVar("net.interp", false, CVar.ARCHIVE, b => _processor.Interpolation = b);
if (!_config.IsCVarRegistered("net.interp_ratio"))
_config.RegisterCVar("net.interp_ratio", 0, CVar.ARCHIVE, i => _processor.InterpRatio = i);
if (!_config.IsCVarRegistered("net.logging"))
_config.RegisterCVar("net.logging", false, CVar.ARCHIVE, b => _processor.Logging = b);
_config.RegisterCVar("net.interp", false, CVar.ARCHIVE, b => _processor.Interpolation = b);
_config.RegisterCVar("net.interp_ratio", 0, CVar.ARCHIVE, i => _processor.InterpRatio = i);
_config.RegisterCVar("net.logging", false, CVar.ARCHIVE, b => _processor.Logging = b);
_config.RegisterCVar("net.predict", true, CVar.ARCHIVE, b => Predicting = b);
_config.RegisterCVar("net.predict_size", 2, CVar.ARCHIVE, i => PredictSize = i);
_processor.Interpolation = _config.GetCVar<bool>("net.interp");
_processor.InterpRatio = _config.GetCVar<int>("net.interp_ratio");
_processor.Logging = _config.GetCVar<bool>("net.logging");
Predicting = _config.GetCVar<bool>("net.predict");
PredictSize = _config.GetCVar<int>("net.predict_size");
}
/// <inheritdoc />
public void Reset()
{
_processor.Reset();
_lastProcessedTick = GameTick.Zero;
_lastProcessedSeq = 0;
}
private void RunLevelChanged(object sender, RunLevelChangedEventArgs args)
@@ -80,23 +108,222 @@ namespace Robust.Client.GameStates
}
}
public void InputCommandDispatched(FullInputCmdMessage message)
{
if (!Predicting)
{
return;
}
message.InputSequence = _nextInputCmdSeq;
_pendingInputs.Enqueue(message);
var inputMan = IoCManager.Resolve<IInputManager>();
inputMan.NetworkBindMap.TryGetKeyFunction(message.InputFunctionId, out var boundFunc);
Logger.DebugS("State",
$"CL> SENT tick={_timing.CurTick}, seq={_nextInputCmdSeq}, func={boundFunc.FunctionName}, state={message.State}");
_nextInputCmdSeq++;
}
public uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage
{
if (!Predicting)
{
return default;
}
var evArgs = new EntitySessionEventArgs(_players.LocalPlayer.Session);
_pendingSystemMessages.Enqueue((_nextInputCmdSeq, _timing.CurTick, message,
new EntitySessionMessage<T>(evArgs, message)));
return _nextInputCmdSeq++;
}
private void HandleStateMessage(MsgState message)
{
var state = message.State;
var lastCurTick = _timing.CurTick;
_timing.CurTick = _lastProcessedTick + 1;
_processor.AddNewState(state);
// we always ack everything we receive, even if it is late
AckGameState(state.ToSequence);
_timing.CurTick = lastCurTick;
}
/// <inheritdoc />
public void ApplyGameState()
{
if (!_processor.ProcessTickStates(_timing.CurTick, out var curState, out var nextState))
return;
_timing.CurTick = _lastProcessedTick + 1;
ApplyGameState(curState, nextState);
if (!_processor.ProcessTickStates(_timing.CurTick, out var curState, out var nextState))
{
return;
}
if (Predicting)
{
// Disable IsFirstTimePredicted while re-running HandleComponentState here.
// Helps with debugging.
using var _ = _timing.StartPastPredictionArea();
ResetPredictedEntities(_timing.CurTick);
}
// apply current state
var createdEntities = ApplyGameState(curState, nextState);
MergeImplicitData(createdEntities);
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
var inputMan = IoCManager.Resolve<IInputManager>();
var input = sysMan.GetEntitySystem<InputSystem>();
if (_lastProcessedSeq < curState.LastProcessedInput)
{
Logger.DebugS("State", $"SV> RCV tick={_timing.CurTick}, seq={_lastProcessedSeq}");
_lastProcessedSeq = curState.LastProcessedInput;
}
// remove old pending inputs
while (_pendingInputs.Count > 0 && _pendingInputs.Peek().InputSequence <= _lastProcessedSeq)
{
var inCmd = _pendingInputs.Dequeue();
inputMan.NetworkBindMap.TryGetKeyFunction(inCmd.InputFunctionId, out var boundFunc);
Logger.DebugS("State",
$"SV> seq={inCmd.InputSequence}, func={boundFunc.FunctionName}, state={inCmd.State}");
}
while (_pendingSystemMessages.Count > 0 && _pendingSystemMessages.Peek().sequence <= _lastProcessedSeq)
{
_pendingSystemMessages.Dequeue();
}
DebugTools.Assert(_timing.InSimulation);
_lastProcessedTick = _timing.CurTick;
if (Predicting)
{
using var _ = _timing.StartPastPredictionArea();
/*
if (_pendingInputs.Count > 0)
{
Logger.DebugS("State", "CL> Predicted:");
}
*/
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
var ping = _network.ServerChannel.Ping / 1000f; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize + _timing.TickRate * ping +
PredictSize;
//Logger.DebugS("State", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value; t < targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
inputMan.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
/*
Logger.DebugS("State",
$" seq={inputCmd.InputSequence}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
*/
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
Logger.DebugS("State", "doot");
}
_entitySystemManager.Update((float) _timing.TickPeriod.TotalSeconds);
}
}
}
private void ResetPredictedEntities(GameTick curTick)
{
foreach (var entity in _entities.GetEntities())
{
// TODO: 99% there's an off-by-one here.
if (entity.Uid.IsClientSide() || entity.LastModifiedTick < curTick)
{
continue;
}
Logger.DebugS("State", $"Entity {entity.Uid} was made dirty.");
var last = _processor.GetLastServerStates(entity.Uid);
// TODO: handle component deletions/creations.
foreach (var comp in _componentManager.GetNetComponents(entity.Uid))
{
DebugTools.AssertNotNull(comp.NetID);
if (comp.LastModifiedTick < curTick || !last.TryGetValue(comp.NetID.Value, out var compState))
{
continue;
}
Logger.DebugS("State", $" And also its component {comp.Name}");
// TODO: Handle interpolation.
comp.HandleComponentState(compState, null);
}
}
}
private void MergeImplicitData(List<EntityUid> createdEntities)
{
// The server doesn't send data that the server can replicate itself on entity creation.
// As such, GameStateProcessor doesn't have that data either.
// We have to feed it back this data by calling GetComponentState() and such,
// so that we can later roll back to it (if necessary).
var outputData = new Dictionary<EntityUid, Dictionary<uint, ComponentState>>();
foreach (var createdEntity in createdEntities)
{
var compData = new Dictionary<uint, ComponentState>();
outputData.Add(createdEntity, compData);
foreach (var component in _componentManager.GetNetComponents(createdEntity))
{
var state = component.GetComponentState();
if (state.GetType() == typeof(ComponentState))
{
continue;
}
compData.Add(state.NetID, state);
}
}
_processor.MergeImplicitData(outputData);
}
private void AckGameState(GameTick sequence)
@@ -106,14 +333,16 @@ namespace Robust.Client.GameStates
_network.ClientSendMessage(msg);
}
private void ApplyGameState(GameState curState, GameState nextState)
private List<EntityUid> ApplyGameState(GameState curState, GameState nextState)
{
_mapManager.ApplyGameStatePre(curState.MapData);
_entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions, nextState?.EntityStates);
var createdEntities = _entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
nextState?.EntityStates);
_players.ApplyPlayerStates(curState.PlayerStates);
_mapManager.ApplyGameStatePost(curState.MapData);
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
return createdEntities;
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Log;
@@ -20,6 +21,9 @@ namespace Robust.Client.GameStates
private GameTick _lastProcessedRealState;
private GameTick _highestFromSequence;
private readonly Dictionary<EntityUid, Dictionary<uint, ComponentState>> _lastStateFullRep
= new Dictionary<EntityUid, Dictionary<uint, ComponentState>>();
/// <inheritdoc />
public int MinBufferSize => Interpolation ? 3 : 2;
@@ -27,7 +31,7 @@ namespace Robust.Client.GameStates
public int TargetBufferSize => MinBufferSize + InterpRatio;
/// <inheritdoc />
public int CurrentBufferSize => _stateBuffer.Count(s => s.ToSequence >= _timing.CurTick);
public int CurrentBufferSize => CalculateBufferSize(_timing.CurTick);
/// <inheritdoc />
public bool Interpolation { get; set; }
@@ -119,7 +123,7 @@ namespace Robust.Client.GameStates
if (applyNextState && !curState.Extrapolated)
_lastProcessedRealState = curState.ToSequence;
if (!_waitingForFull)
{
if (!applyNextState)
@@ -135,12 +139,72 @@ namespace Robust.Client.GameStates
_timing.TickTimingAdjustment = 0f;
}
if (Logging && applyNextState)
Logger.DebugS("net.state", $"Applying State: ext={curState.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
if (applyNextState)
{
if (Logging)
{
Logger.DebugS("net.state", $"Applying State: ext={curState.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
if (!curState.Extrapolated)
{
UpdateFullRep(curState);
}
}
return applyNextState;
}
private void UpdateFullRep(GameState state)
{
if (state.FromSequence == GameTick.Zero)
{
// Full state.
_lastStateFullRep.Clear();
}
else
{
if (state.EntityDeletions != null)
{
foreach (var deletion in state.EntityDeletions)
{
_lastStateFullRep.Remove(deletion);
}
}
}
if (state.EntityStates != null)
{
foreach (var entityState in state.EntityStates)
{
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
{
compData = new Dictionary<uint, ComponentState>();
_lastStateFullRep.Add(entityState.Uid, compData);
}
if (entityState.ComponentChanges != null)
{
foreach (var change in entityState.ComponentChanges)
{
if (change.Deleted)
{
compData.Remove(change.NetID);
}
}
}
if (entityState.ComponentStates != null)
{
foreach (var compState in entityState.ComponentStates)
{
compData[compState.NetID] = compState;
}
}
}
}
}
private bool CalculateFullState(out GameState curState, out GameState nextState, int targetBufferSize)
{
if (_lastFullState != null)
@@ -206,6 +270,7 @@ namespace Robust.Client.GameStates
nextState = null;
GameState futureState = null;
uint lastStateInput = 0;
for (var i = 0; i < _stateBuffer.Count; i++)
{
@@ -225,6 +290,10 @@ namespace Robust.Client.GameStates
{
futureState = state;
}
else if (state.ToSequence == lastTick)
{
lastStateInput = state.LastProcessedInput;
}
else if (state.ToSequence < _highestFromSequence) // remove any old states we find to keep the buffer clean
{
_stateBuffer.RemoveSwap(i);
@@ -236,7 +305,7 @@ namespace Robust.Client.GameStates
if (!Extrapolation && curState == null && futureState != null)
{
//this is not actually extrapolation
curState = ExtrapolateState(_highestFromSequence, curTick);
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
return true; // keep moving, we have a future state
}
@@ -253,12 +322,12 @@ namespace Robust.Client.GameStates
if (curState == null)
{
curState = ExtrapolateState(_highestFromSequence, curTick);
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
}
if (nextState == null && Interpolation)
{
nextState = ExtrapolateState(_highestFromSequence, nextTick);
nextState = ExtrapolateState(_highestFromSequence, nextTick, lastStateInput);
}
return true;
@@ -267,9 +336,9 @@ namespace Robust.Client.GameStates
/// <summary>
/// Generates a completely fake GameState.
/// </summary>
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence)
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence, uint lastInput)
{
var state = new GameState(fromSequence, toSequence, null, null, null, null);
var state = new GameState(fromSequence, toSequence, lastInput, null, null, null, null);
state.Extrapolated = true;
return state;
}
@@ -281,5 +350,31 @@ namespace Robust.Client.GameStates
_lastFullState = null;
_waitingForFull = true;
}
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data)
{
foreach (var (uid, compData) in data)
{
var fullRep = _lastStateFullRep[uid];
foreach (var (netId, compState) in compData)
{
if (!fullRep.ContainsKey(netId))
{
fullRep.Add(netId, compState);
}
}
}
}
public Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity)
{
return _lastStateFullRep[entity];
}
public int CalculateBufferSize(GameTick fromTick)
{
return _stateBuffer.Count(s => s.ToSequence >= fromTick);
}
}
}

View File

@@ -1,10 +1,14 @@
using Robust.Shared.GameStates;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
{
/// <summary>
/// Holds a collection of game states and calculates which ones to apply at a given game tick.
/// It also stores a copy of all the last entity states from the server,
/// allowing the game to be reset to a server-like condition at any point.
/// </summary>
internal interface IGameStateProcessor
{
@@ -26,6 +30,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Number of game states currently in the state buffer.
/// </summary>
/// <seealso cref="CalculateBufferSize"/>
int CurrentBufferSize { get; }
/// <summary>
@@ -71,5 +76,31 @@ namespace Robust.Client.GameStates
/// Resets the processor back to its initial state.
/// </summary>
void Reset();
/// <summary>
/// Merges entity data into the full copy of the server states.
/// </summary>
/// <remarks>
/// This is necessary because the server does not send data
/// that can be inferred from entity creation on new entity states.
/// This data thus has to be re-constructed client-side and merged with this method.
/// </remarks>
/// <param name="data">
/// The data to merge.
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
/// </param>
void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data);
/// <summary>
/// Get the last state data from the server for an entity.
/// </summary>
/// <returns>Dictionary (net ID -> ComponentState)</returns>
Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity);
/// <summary>
/// Calculate the size of the game state buffer from a given tick.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int CalculateBufferSize(GameTick fromTick);
}
}

View File

@@ -215,16 +215,22 @@ namespace Robust.Client.Input
UIKeyBindStateChanged?.Invoke(eventArgs);
if (state == BoundKeyState.Up || !eventArgs.Handled)
{
KeyBindStateChanged?.Invoke(eventArgs);
var cmd = GetInputCommand(binding.Function);
if (state == BoundKeyState.Up)
// TODO: Allow input commands to still get forwarded to server if necessary.
if (cmd != null)
{
cmd?.Disabled(null);
if (state == BoundKeyState.Up)
{
cmd.Disabled(null);
}
else
{
cmd.Enabled(null);
}
}
else
{
cmd?.Enabled(null);
KeyBindStateChanged?.Invoke(eventArgs);
}
}

View File

@@ -6,6 +6,8 @@ namespace Robust.Client.Interfaces.GameObjects
{
public interface IClientEntityManager : IEntityManager
{
void ApplyEntityStates(List<EntityState> curEntStates, IEnumerable<EntityUid> deletions, List<EntityState> nextEntStates);
/// <returns>The list of new entities created.</returns>
List<EntityUid> ApplyEntityStates(List<EntityState> curEntStates, IEnumerable<EntityUid> deletions,
List<EntityState> nextEntStates);
}
}

View File

@@ -1,5 +1,8 @@
using System;
using Robust.Client.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Timing;
namespace Robust.Client.Interfaces.GameStates
{
@@ -28,6 +31,14 @@ namespace Robust.Client.Interfaces.GameStates
/// </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>
/// This is called after the game state has been applied for the current tick.
/// </summary>
@@ -47,5 +58,13 @@ namespace Robust.Client.Interfaces.GameStates
/// Applies the game state for this tick.
/// </summary>
void ApplyGameState();
/// <summary>
/// An input command has been dispatched.
/// </summary>
/// <param name="message">Message being dispatched.</param>
void InputCommandDispatched(FullInputCmdMessage message);
uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage;
}
}

View File

@@ -41,7 +41,10 @@ namespace Robust.Client.Player
/// <summary>
/// Session of the local client.
/// </summary>
[ViewVariables] public PlayerSession Session { get; set; }
[ViewVariables]
public IPlayerSession Session => InternalSession;
internal PlayerSession InternalSession { get; set; }
/// <summary>
/// OOC name of the local player.
@@ -74,6 +77,7 @@ namespace Robust.Client.Player
DetachEntity();
ControlledEntity = entity;
InternalSession.AttachedEntity = entity;
if (!ControlledEntity.TryGetComponent<EyeComponent>(out var eye))
{

View File

@@ -193,7 +193,7 @@ namespace Robust.Client.Player
_sessions.Add(state.SessionId, newSession);
if (state.SessionId == LocalPlayer.SessionId)
{
LocalPlayer.Session = newSession;
LocalPlayer.InternalSession = newSession;
// We just connected to the server, hurray!
LocalPlayer.SwitchState(SessionStatus.Connecting, newSession.Status);

View File

@@ -1,14 +1,17 @@
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Network;
namespace Robust.Client.Player
{
public class PlayerSession : IPlayerSession
internal sealed class PlayerSession : IPlayerSession
{
/// <inheritdoc />
public SessionStatus Status { get; set; } = SessionStatus.Connecting;
public IEntity AttachedEntity { get; set; }
/// <inheritdoc />
public NetSessionId SessionId { get; }

View File

@@ -1,4 +1,5 @@
using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.GameStates;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.Input;
using Robust.Client.Interfaces.State;
@@ -8,6 +9,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
namespace Robust.Client.UserInterface.CustomControls
{
@@ -54,10 +56,7 @@ namespace Robust.Client.UserInterface.CustomControls
_debugNetPanel = new DebugNetPanel(netManager, gameTiming1);
AddChild(_debugNetPanel);
_timeDebug = new DebugTimePanel(gameTiming1)
{
Visible = false,
};
_timeDebug = new DebugTimePanel(gameTiming1, IoCManager.Resolve<IClientGameStateManager>());
AddChild(_timeDebug);
_frameGraph = new FrameGraph(gameTiming1);

View File

@@ -1,35 +1,35 @@
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.ResourceManagement;
using Robust.Client.Interfaces.GameStates;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.CustomControls
{
public class DebugTimePanel : PanelContainer
{
private readonly IGameTiming _gameTiming;
private readonly IClientGameStateManager _gameState;
private Label _contents;
public DebugTimePanel(IGameTiming gameTiming)
public DebugTimePanel(IGameTiming gameTiming, IClientGameStateManager gameState)
{
_gameTiming = gameTiming;
_gameState = gameState;
_contents = new Label
{
FontColorShadowOverride = Color.Black,
/*MarginTop = 5,
MarginLeft = 5*/
};
AddChild(_contents);
PanelOverride = new StyleBoxFlat
{
BackgroundColor = new Color(67, 105, 255, 138),
BackgroundColor = new Color(35, 134, 37, 138),
ContentMarginLeftOverride = 5,
ContentMarginTopOverride = 5
};
MouseFilter = _contents.MouseFilter = MouseFilterMode.Ignore;
@@ -46,8 +46,9 @@ namespace Robust.Client.UserInterface.CustomControls
return;
}
_contents.Text = $@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick},
CurTime: {_gameTiming.CurTime}, RealTime: {_gameTiming.RealTime}, CurFrame: {_gameTiming.CurFrame}";
_contents.Text = $@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}, CurServerTick: {_gameState.CurServerTick}, Pred: {_gameTiming.CurTick.Value - _gameState.CurServerTick.Value}
CurTime: {_gameTiming.CurTime:hh\:mm\:ss\.ff}, RealTime: {_gameTiming.RealTime:hh\:mm\:ss\.ff}, CurFrame: {_gameTiming.CurFrame}
TickTimingAdjustment: {_gameTiming.TickTimingAdjustment}";
MinimumSizeChanged();
}

View File

@@ -34,6 +34,7 @@ using Robust.Shared.Localization;
using Robust.Server.Interfaces.Debugging;
using Robust.Server.ServerStatus;
using Robust.Shared;
using Robust.Shared.Network.Messages;
namespace Robust.Server
{
@@ -170,6 +171,7 @@ namespace Robust.Server
{
netMan.Initialize(true);
netMan.StartServer();
netMan.RegisterNetMessage<MsgSetTickRate>(MsgSetTickRate.NAME);
}
catch (Exception e)
{
@@ -325,7 +327,12 @@ namespace Robust.Server
{
var cfgMgr = IoCManager.Resolve<IConfigurationManager>();
cfgMgr.RegisterCVar("net.tickrate", 60, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
cfgMgr.RegisterCVar("net.tickrate", 60, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER, i =>
{
var b = (byte) i;
_time.TickRate = b;
SendTickRateUpdateToClients(b);
});
cfgMgr.RegisterCVar("game.hostname", "MyServer", CVar.ARCHIVE);
cfgMgr.RegisterCVar("game.maxplayers", 32, CVar.ARCHIVE);
@@ -338,6 +345,14 @@ namespace Robust.Server
Logger.InfoS("srv", $"Max players: {MaxPlayers}");
}
private void SendTickRateUpdateToClients(byte newTickRate)
{
var msg = _network.CreateNetMessage<MsgSetTickRate>();
msg.NewTickRate = newTickRate;
_network.ServerSendToAll(msg);
}
// called right before main loop returns, do all saving/cleanup in here
private void Cleanup()
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Input;
using Robust.Shared.IoC;
@@ -12,7 +13,7 @@ namespace Robust.Server.GameObjects.EntitySystems
/// <summary>
/// Server side processing of incoming user commands.
/// </summary>
public class InputSystem : EntitySystem
public class InputSystem : SharedInputSystem
{
#pragma warning disable 649
[Dependency] private readonly IPlayerManager _playerManager;
@@ -21,10 +22,12 @@ namespace Robust.Server.GameObjects.EntitySystems
private readonly Dictionary<IPlayerSession, IPlayerCommandStates> _playerInputs = new Dictionary<IPlayerSession, IPlayerCommandStates>();
private readonly CommandBindMapping _bindMap = new CommandBindMapping();
private readonly Dictionary<IPlayerSession, uint> _lastProcessedInputCmd = new Dictionary<IPlayerSession, uint>();
/// <summary>
/// Server side input command binds.
/// </summary>
public ICommandBindMapping BindMap => _bindMap;
public override ICommandBindMapping BindMap => _bindMap;
/// <inheritdoc />
public override void Initialize()
@@ -33,18 +36,19 @@ namespace Robust.Server.GameObjects.EntitySystems
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
private void HandleCommandMessage(IPlayerSession session, InputCmdHandler cmdHandler, FullInputCmdMessage msg)
{
cmdHandler.HandleCmdMessage(session, msg);
}
/// <inheritdoc />
public override void Shutdown()
{
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void InputMessageHandler(InputCmdMessage message)
private void InputMessageHandler(InputCmdMessage message, EntitySessionEventArgs eventArgs)
{
var channel = message.NetChannel;
if(channel == null)
return;
if (!(message is FullInputCmdMessage msg))
return;
@@ -52,21 +56,24 @@ namespace Robust.Server.GameObjects.EntitySystems
if (!_playerManager.KeyMap.TryGetKeyFunction(msg.InputFunctionId, out var function))
return;
var session = _playerManager.GetSessionByChannel(channel);
//Client Sanitization: bad enum key state value
if (!Enum.IsDefined(typeof(BoundKeyState), msg.State))
return;
var session = (IPlayerSession) eventArgs.SenderSession;
if (_lastProcessedInputCmd[session] < msg.InputSequence)
_lastProcessedInputCmd[session] = msg.InputSequence;
// route the cmdMessage to the proper bind
//Client Sanitization: unbound command, just ignore
if (_bindMap.TryGetHandler(function, out var command))
if (_bindMap.TryGetHandler(function, out var cmdHandler))
{
// set state, only bound key functions get state changes
var states = GetInputStates(session);
states.SetState(function, msg.State);
command.HandleCmdMessage(session, msg);
HandleCommandMessage(session, cmdHandler, msg);
}
}
@@ -75,16 +82,23 @@ namespace Robust.Server.GameObjects.EntitySystems
return _playerInputs[session];
}
public uint GetLastInputCommand(IPlayerSession session)
{
return _lastProcessedInputCmd[session];
}
private void OnPlayerStatusChanged(object sender, SessionStatusEventArgs args)
{
switch (args.NewStatus)
{
case SessionStatus.Connected:
_playerInputs.Add(args.Session, new PlayerCommandStates());
_lastProcessedInputCmd.Add(args.Session, 0);
break;
case SessionStatus.Disconnected:
_playerInputs.Remove(args.Session);
_lastProcessedInputCmd.Remove(args.Session);
break;
}
}

View File

@@ -1,35 +1,66 @@
using System;
using System.Collections.Generic;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
{
/// <summary>
/// The server implementation of the Entity Network Manager.
/// </summary>
public class ServerEntityNetworkManager : IEntityNetworkManager
public class ServerEntityNetworkManager : IServerEntityNetworkManager
{
#pragma warning disable 649
[Dependency] private readonly IServerNetManager _networkManager;
[Dependency] private readonly IGameTiming _gameTiming;
[Dependency] private readonly IPlayerManager _playerManager;
#pragma warning restore 649
/// <inheritdoc />
public event EventHandler<NetworkComponentMessage> ReceivedComponentMessage;
/// <inheritdoc />
public event EventHandler<EntitySystemMessage> ReceivedSystemMessage;
public event EventHandler<object> ReceivedSystemMessage;
private readonly PriorityQueue<MsgEntity> _queue = new PriorityQueue<MsgEntity>(new MessageSequenceComparer());
private readonly Dictionary<IPlayerSession, uint> _lastProcessedSequencesCmd =
new Dictionary<IPlayerSession, uint>();
/// <inheritdoc />
public void SetupNetworking()
{
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
public void Update()
{
while (_queue.Count != 0 && _queue.Peek().SourceTick <= _gameTiming.CurTick)
{
DispatchEntityNetworkMessage(_queue.Take());
}
}
public uint GetLastMessageSequence(IPlayerSession session)
{
return _lastProcessedSequencesCmd[session];
}
/// <inheritdoc />
public void SendComponentNetworkMessage(INetChannel channel, IEntity entity, IComponent component, ComponentMessage message)
public void SendComponentNetworkMessage(INetChannel channel, IEntity entity, IComponent component,
ComponentMessage message)
{
if (_networkManager.IsClient)
return;
@@ -42,6 +73,7 @@ namespace Robust.Server.GameObjects
msg.EntityUid = entity.Uid;
msg.NetId = component.NetID.Value;
msg.ComponentMessage = message;
msg.SourceTick = _gameTiming.CurTick;
//Send the message
if (channel == null)
@@ -56,6 +88,7 @@ namespace Robust.Server.GameObjects
var newMsg = _networkManager.CreateNetMessage<MsgEntity>();
newMsg.Type = EntityMessageType.SystemMessage;
newMsg.SystemMessage = message;
newMsg.SourceTick = _gameTiming.CurTick;
_networkManager.ServerSendToAll(newMsg);
}
@@ -66,12 +99,43 @@ namespace Robust.Server.GameObjects
var newMsg = _networkManager.CreateNetMessage<MsgEntity>();
newMsg.Type = EntityMessageType.SystemMessage;
newMsg.SystemMessage = message;
newMsg.SourceTick = _gameTiming.CurTick;
_networkManager.ServerSendMessage(newMsg, targetConnection);
}
private void HandleEntityNetworkMessage(MsgEntity message)
{
var msgT = message.SourceTick;
var cT = _gameTiming.CurTick;
Logger.Info($"{msgT}, {cT}");
if (msgT <= cT)
{
if (msgT < cT)
{
Logger.WarningS("net.ent", "Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}",
(int) msgT.Value - (int) cT.Value, message.MsgChannel.SessionId, msgT, cT);
}
DispatchEntityNetworkMessage(message);
return;
}
_queue.Add(message);
}
private void DispatchEntityNetworkMessage(MsgEntity message)
{
var player = _playerManager.GetSessionByChannel(message.MsgChannel);
if (message.Sequence != 0)
{
if (_lastProcessedSequencesCmd[player] < message.Sequence)
{
_lastProcessedSequencesCmd[player] = message.Sequence;
}
}
switch (message.Type)
{
case EntityMessageType.ComponentMessage:
@@ -79,9 +143,37 @@ namespace Robust.Server.GameObjects
return;
case EntityMessageType.SystemMessage:
ReceivedSystemMessage?.Invoke(this, message.SystemMessage);
var msg = message.SystemMessage;
var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType());
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(player), msg);
ReceivedSystemMessage?.Invoke(this, sessionMsg);
return;
}
}
private void OnPlayerStatusChanged(object sender, SessionStatusEventArgs args)
{
switch (args.NewStatus)
{
case SessionStatus.Connected:
_lastProcessedSequencesCmd.Add(args.Session, 0);
break;
case SessionStatus.Disconnected:
_lastProcessedSequencesCmd.Remove(args.Session);
break;
}
}
private sealed class MessageSequenceComparer : IComparer<MsgEntity>
{
public int Compare(MsgEntity x, MsgEntity y)
{
DebugTools.AssertNotNull(x);
DebugTools.AssertNotNull(y);
return y.Sequence.CompareTo(x.Sequence);
}
}
}
}

View File

@@ -1,10 +1,13 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.GameState;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Network;
@@ -30,6 +33,8 @@ namespace Robust.Server.GameStates
[Dependency] private readonly IServerNetManager _networkManager;
[Dependency] private readonly IPlayerManager _playerManager;
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IEntitySystemManager _systemManager;
[Dependency] private readonly IServerEntityNetworkManager _entityNetworkManager;
[Dependency] private readonly IConfigurationManager _configurationManager;
#pragma warning restore 649
@@ -106,6 +111,8 @@ namespace Robust.Server.GameStates
return;
}
var inputSystem = _systemManager.GetEntitySystem<InputSystem>();
var oldestAck = GameTick.MaxValue;
@@ -132,7 +139,10 @@ 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 state = new GameState(lastAck, _gameTiming.CurTick, entStates, playerStates, deletions, mapData);
var lastInputCommand = inputSystem.GetLastInputCommand(session);
var lastSystemMessage = _entityNetworkManager.GetLastMessageSequence(session);
var state = new GameState(lastAck, _gameTiming.CurTick,
Math.Max(lastInputCommand, lastSystemMessage), entStates, playerStates, deletions, mapData);
if (lastAck < oldestAck)
{
oldestAck = lastAck;
@@ -151,7 +161,6 @@ namespace Robust.Server.GameStates
{
_ackedStates[channel.ConnectionId] = _gameTiming.CurTick;
}
}
// keep the deletion history buffers clean
@@ -162,6 +171,5 @@ namespace Robust.Server.GameStates
_mapManager.CullDeletionHistory(oldestAck);
}
}
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
namespace Robust.Server.Interfaces.GameObjects
{
public interface IServerEntityNetworkManager : IEntityNetworkManager
{
uint GetLastMessageSequence(IPlayerSession session);
}
}

View File

@@ -9,7 +9,6 @@ namespace Robust.Server.Interfaces.Player
{
public interface IPlayerSession : ICommonSession
{
IEntity AttachedEntity { get; }
EntityUid? AttachedEntityUid { get; }
INetChannel ConnectedClient { get; }
DateTime ConnectedTime { get; }

View File

@@ -51,6 +51,7 @@ namespace Robust.Server
IoCManager.Register<IConsoleShell, ConsoleShell>();
IoCManager.Register<IEntityManager, ServerEntityManager>();
IoCManager.Register<IEntityNetworkManager, ServerEntityNetworkManager>();
IoCManager.Register<IServerEntityNetworkManager, ServerEntityNetworkManager>();
IoCManager.Register<IMapLoader, MapLoader>();
IoCManager.Register<IPauseManager, PauseManager>();
IoCManager.Register<IPlacementManager, PlacementManager>();

View File

@@ -235,6 +235,8 @@ namespace Robust.Shared.GameObjects.Components
public class CollisionEnabledEvent : EntitySystemMessage
{
public bool Value { get; }
public EntityUid Owner { get; }
public CollisionEnabledEvent(EntityUid uid, bool value)
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@@ -21,8 +22,8 @@ namespace Robust.Shared.GameObjects
/// <param name="source"></param>
/// <param name="subscriber">Subscriber that owns the handler.</param>
/// <param name="eventHandler">Delegate that handles the event.</param>
void SubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler<T> eventHandler)
where T : EntityEventArgs;
void SubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber,
EntityEventHandler<T> eventHandler);
/// <summary>
/// Unsubscribes all event handlers of a given type.
@@ -30,15 +31,16 @@ namespace Robust.Shared.GameObjects
/// <typeparam name="T">Event type being unsubscribed from.</typeparam>
/// <param name="source"></param>
/// <param name="subscriber">Subscriber that owns the handlers.</param>
void UnsubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber)
where T : EntityEventArgs;
void UnsubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber);
/// <summary>
/// Immediately raises an event onto the bus.
/// </summary>
/// <param name="source"></param>
/// <param name="toRaise">Event being raised.</param>
void RaiseEvent(EventSource source, EntityEventArgs toRaise);
void RaiseEvent(EventSource source, object toRaise);
void RaiseEvent<T>(EventSource source, T toRaise);
/// <summary>
/// Queues an event to be raised at a later time.
@@ -53,8 +55,7 @@ namespace Robust.Shared.GameObjects
/// <typeparam name="T">Event type being waited for.</typeparam>
/// <param name="source"></param>
/// <returns></returns>
Task<T> AwaitEvent<T>(EventSource source)
where T : EntityEventArgs;
Task<T> AwaitEvent<T>(EventSource source);
/// <summary>
/// Waits for an event to be raised. You do not have to subscribe to the event.
@@ -63,8 +64,7 @@ namespace Robust.Shared.GameObjects
/// <param name="source"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<T> AwaitEvent<T>(EventSource source, CancellationToken cancellationToken)
where T : EntityEventArgs;
Task<T> AwaitEvent<T>(EventSource source, CancellationToken cancellationToken);
/// <summary>
/// Unsubscribes all event handlers for a given subscriber.
@@ -95,19 +95,19 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
internal class EntityEventBus : IEntityEventBus
{
private delegate void EventHandler(EntityEventArgs ev);
private delegate void EventHandler(object ev);
private readonly Dictionary<Type, List<(EventSource mask, Delegate originalHandler, EventHandler callback)>> _eventSubscriptions
= new Dictionary<Type, List<(EventSource mask, Delegate originalHandler, EventHandler callback)>>();
private readonly Dictionary<Type, List<Registration>> _eventSubscriptions
= new Dictionary<Type, List<Registration>>();
private readonly Dictionary<IEntityEventSubscriber, Dictionary<Type, (EventSource source, Delegate originalHandler, EventHandler handler)>> _inverseEventSubscriptions
= new Dictionary<IEntityEventSubscriber, Dictionary<Type, (EventSource source, Delegate originalHandler, EventHandler handler)>>();
private readonly Dictionary<IEntityEventSubscriber, Dictionary<Type, Registration>> _inverseEventSubscriptions
= new Dictionary<IEntityEventSubscriber, Dictionary<Type, Registration>>();
private readonly Queue<(EventSource source, EntityEventArgs args)> _eventQueue = new Queue<(EventSource source, EntityEventArgs args)>();
private readonly Queue<(EventSource source, object args)> _eventQueue = new Queue<(EventSource source, object args)>();
private readonly Dictionary<Type, (EventSource, CancellationTokenRegistration, TaskCompletionSource<EntityEventArgs>)>
private readonly Dictionary<Type, (EventSource, CancellationTokenRegistration, TaskCompletionSource<object>)>
_awaitingMessages
= new Dictionary<Type, (EventSource, CancellationTokenRegistration, TaskCompletionSource<EntityEventArgs>)>();
= new Dictionary<Type, (EventSource, CancellationTokenRegistration, TaskCompletionSource<object>)>();
/// <inheritdoc />
public void UnsubscribeEvents(IEntityEventSubscriber subscriber)
@@ -137,7 +137,6 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public void SubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler<T> eventHandler)
where T : EntityEventArgs
{
if (source == EventSource.None)
throw new ArgumentOutOfRangeException(nameof(source));
@@ -149,15 +148,15 @@ namespace Robust.Shared.GameObjects
throw new ArgumentNullException(nameof(subscriber));
var eventType = typeof(T);
var subscriptionTuple = (source, eventHandler, (EventHandler) (ev => eventHandler((T) ev)));
var subscriptionTuple = new Registration(source, eventHandler, ev => eventHandler((T) ev), eventHandler);
if (!_eventSubscriptions.TryGetValue(eventType, out var subscriptions))
_eventSubscriptions.Add(eventType, new List<(EventSource, Delegate, EventHandler)> {subscriptionTuple});
else if (!subscriptions.Any(p => p.mask == source && p.originalHandler == (Delegate) eventHandler))
_eventSubscriptions.Add(eventType, new List<Registration> {subscriptionTuple});
else if (!subscriptions.Any(p => p.Mask == source && p.Original == (Delegate) eventHandler))
subscriptions.Add(subscriptionTuple);
if (!_inverseEventSubscriptions.TryGetValue(subscriber, out var inverseSubscription))
{
inverseSubscription = new Dictionary<Type, (EventSource, Delegate, EventHandler)>
inverseSubscription = new Dictionary<Type, Registration>
{
{eventType, subscriptionTuple}
};
@@ -176,7 +175,6 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public void UnsubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber)
where T : EntityEventArgs
{
if (source == EventSource.None)
throw new ArgumentOutOfRangeException(nameof(source));
@@ -188,11 +186,22 @@ namespace Robust.Shared.GameObjects
if (_inverseEventSubscriptions.TryGetValue(subscriber, out var inverse)
&& inverse.TryGetValue(eventType, out var tuple))
UnsubscribeEvent(source, eventType, tuple.originalHandler, tuple.handler, subscriber);
UnsubscribeEvent(source, eventType, tuple.Original, tuple.Handler, subscriber);
}
/// <inheritdoc />
public void RaiseEvent(EventSource source, EntityEventArgs toRaise)
public void RaiseEvent(EventSource source, object toRaise)
{
if (source == EventSource.None)
throw new ArgumentOutOfRangeException(nameof(source));
if (toRaise == null)
throw new ArgumentNullException(nameof(toRaise));
ProcessSingleEvent(source, toRaise);
}
public void RaiseEvent<T>(EventSource source, T toRaise)
{
if (source == EventSource.None)
throw new ArgumentOutOfRangeException(nameof(source));
@@ -217,14 +226,12 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public Task<T> AwaitEvent<T>(EventSource source)
where T : EntityEventArgs
{
return AwaitEvent<T>(source,default);
return AwaitEvent<T>(source, default);
}
/// <inheritdoc />
public Task<T> AwaitEvent<T>(EventSource source, CancellationToken cancellationToken)
where T : EntityEventArgs
{
if(source == EventSource.None)
throw new ArgumentOutOfRangeException(nameof(source));
@@ -235,7 +242,7 @@ namespace Robust.Shared.GameObjects
throw new InvalidOperationException("Cannot await the same message type twice at once.");
}
var tcs = new TaskCompletionSource<EntityEventArgs>();
var tcs = new TaskCompletionSource<object>();
CancellationTokenRegistration reg = default;
if (cancellationToken != default)
{
@@ -247,7 +254,7 @@ namespace Robust.Shared.GameObjects
}
// Tiny trick so we can return T while the tcs is passed an EntitySystemMessage.
async Task<T> DoCast(Task<EntityEventArgs> task)
async Task<T> DoCast(Task<object> task)
{
return (T)await task;
}
@@ -258,7 +265,7 @@ namespace Robust.Shared.GameObjects
private void UnsubscribeEvent(EventSource source, Type eventType, Delegate originalHandler, EventHandler handler, IEntityEventSubscriber subscriber)
{
var tuple = (source, originalHandler, evh: handler);
var tuple = new Registration(source, originalHandler, handler, originalHandler);
if (_eventSubscriptions.TryGetValue(eventType, out var subscriptions) && subscriptions.Contains(tuple))
subscriptions.Remove(tuple);
@@ -266,7 +273,7 @@ namespace Robust.Shared.GameObjects
inverse.Remove(eventType);
}
private void ProcessSingleEvent(EventSource source, EntityEventArgs eventArgs)
private void ProcessSingleEvent(EventSource source, object eventArgs)
{
var eventType = eventArgs.GetType();
@@ -274,8 +281,8 @@ namespace Robust.Shared.GameObjects
{
foreach (var handler in subs)
{
if((handler.mask & source) != 0)
handler.callback(eventArgs);
if((handler.Mask & source) != 0)
handler.Handler(eventArgs);
}
}
@@ -283,12 +290,92 @@ namespace Robust.Shared.GameObjects
{
var (mask, _, tcs) = awaiting;
if((source & mask) != 0)
if ((source & mask) != 0)
{
tcs.TrySetResult(eventArgs);
_awaitingMessages.Remove(eventType);
}
}
}
private void ProcessSingleEvent<T>(EventSource source, T eventArgs)
{
var eventType = typeof(T);
if (_eventSubscriptions.TryGetValue(eventType, out var subs))
{
foreach (var (mask, originalHandler, _) in subs)
{
if ((mask & source) != 0)
{
var foo = (EntityEventHandler<T>) originalHandler;
foo(eventArgs);
}
}
}
if (_awaitingMessages.TryGetValue(eventType, out var awaiting))
{
var (mask1, _, tcs) = awaiting;
if ((source & mask1) != 0)
{
tcs.TrySetResult(eventArgs);
_awaitingMessages.Remove(eventType);
}
}
}
private readonly struct Registration : IEquatable<Registration>
{
public readonly EventSource Mask;
public readonly object EqualityToken;
public readonly Delegate Original;
public readonly EventHandler Handler;
public Registration(EventSource mask, Delegate original, EventHandler handler, object equalityToken)
{
Mask = mask;
Original = original;
Handler = handler;
EqualityToken = equalityToken;
}
public bool Equals(Registration other)
{
return Mask == other.Mask && Equals(EqualityToken, other.EqualityToken);
}
public override bool Equals(object obj)
{
return obj is Registration other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return ((int) Mask * 397) ^ (EqualityToken != null ? EqualityToken.GetHashCode() : 0);
}
}
public static bool operator ==(Registration left, Registration right)
{
return left.Equals(right);
}
public static bool operator !=(Registration left, Registration right)
{
return !left.Equals(right);
}
public void Deconstruct(out EventSource mask, out Delegate originalHandler, out EventHandler handler)
{
mask = Mask;
originalHandler = Original;
handler = Handler;
}
}
}
}

View File

@@ -1,11 +1,41 @@
using System;
using Robust.Shared.Players;
namespace Robust.Shared.GameObjects
{
public interface IEntityEventSubscriber { }
public delegate void EntityEventHandler<in T>(T ev)
where T : EntityEventArgs;
public delegate void EntityEventHandler<in T>(T ev);
public delegate void EntitySessionEventHandler<in T>(T msg, EntitySessionEventArgs args);
public class EntityEventArgs : EventArgs { }
public readonly struct EntitySessionEventArgs
{
public EntitySessionEventArgs(ICommonSession senderSession)
{
SenderSession = senderSession;
}
public ICommonSession SenderSession { get; }
}
internal readonly struct EntitySessionMessage<T>
{
public EntitySessionMessage(EntitySessionEventArgs eventArgs, T message)
{
EventArgs = eventArgs;
Message = message;
}
public EntitySessionEventArgs EventArgs { get; }
public T Message { get; }
public void Deconstruct(out EntitySessionEventArgs eventArgs, out T message)
{
eventArgs = EventArgs;
message = Message;
}
}
}

View File

@@ -82,6 +82,7 @@ namespace Robust.Shared.GameObjects
public virtual void Update(float frameTime)
{
EntityNetworkManager.Update();
EntitySystemManager.Update(frameTime);
_eventBus.ProcessEventQueue();
CullDeletedEntities();

View File

@@ -1,6 +1,7 @@
using Robust.Shared.Serialization;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Robust.Shared.GameObjects
{
@@ -8,7 +9,9 @@ namespace Robust.Shared.GameObjects
public sealed class EntityState
{
public EntityUid Uid { get; }
[CanBeNull]
public List<ComponentChanged> ComponentChanges { get; }
[CanBeNull]
public List<ComponentState> ComponentStates { get; }
public EntityState(EntityUid uid, List<ComponentChanged> changedComponents, List<ComponentState> componentStates)

View File

@@ -1,5 +1,4 @@
using System;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects
@@ -7,16 +6,5 @@ namespace Robust.Shared.GameObjects
[Serializable, NetSerializable]
public class EntitySystemMessage : EntityEventArgs
{
/// <summary>
/// Remote network channel this message came from.
/// If this is null, the message was raised locally.
/// </summary>
[field: NonSerialized]
public INetChannel NetChannel { get; set; }
/// <summary>
/// Entity this message is raised for.
/// </summary>
public EntityUid Owner { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
namespace Robust.Shared.GameObjects
{
public static class EventBusExt
{
public static void SubscribeSessionEvent<T>(this IEventBus eventBus, EventSource source,
IEntityEventSubscriber subscriber, EntitySessionEventHandler<T> handler)
{
var wrapper = new HandlerWrapper<T>(handler);
eventBus.SubscribeEvent<EntitySessionMessage<T>>(source, subscriber, wrapper.Invoke);
}
private sealed class HandlerWrapper<T>
{
public HandlerWrapper(EntitySessionEventHandler<T> handler)
{
Handler = handler;
}
public EntitySessionEventHandler<T> Handler { get; }
public void Invoke(EntitySessionMessage<T> msg)
{
Handler(msg.Message, msg.EventArgs);
}
private bool Equals(HandlerWrapper<T> other)
{
return Handler.Equals(other.Handler);
}
public override bool Equals(object obj)
{
return ReferenceEquals(this, obj) || obj is HandlerWrapper<T> other && Equals(other);
}
public override int GetHashCode()
{
return Handler.GetHashCode();
}
}
}
}

View File

@@ -17,7 +17,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// This event is raised when a component message comes in from the network.
/// </summary>
event EventHandler<EntitySystemMessage> ReceivedSystemMessage;
event EventHandler<object> ReceivedSystemMessage;
/// <summary>
/// Initializes networking for this manager. This should only be called once.
@@ -47,6 +47,11 @@ namespace Robust.Shared.GameObjects
/// <param name="message">Message that should be sent.</param>
void SendSystemNetworkMessage(EntitySystemMessage message);
void SendSystemNetworkMessage(EntitySystemMessage message, uint sequence)
{
throw new NotSupportedException();
}
/// <summary>
/// Sends an Entity System Message to relevant System on a client.
/// Server: Sends the message to the relevant systems of the client on <paramref name="channel"/>
@@ -57,5 +62,10 @@ namespace Robust.Shared.GameObjects
/// Thrown if called on the client.
/// </exception>
void SendSystemNetworkMessage(EntitySystemMessage message, INetChannel channel);
/// <summary>
/// Sends out queued messages based on current tick.
/// </summary>
void Update();
}
}

View File

@@ -46,12 +46,24 @@ namespace Robust.Shared.GameObjects.Systems
EntityManager.EventBus.SubscribeEvent(EventSource.Network, this, handler);
}
protected void SubscribeNetworkEvent<T>(EntitySessionEventHandler<T> handler)
where T : EntitySystemMessage
{
EntityManager.EventBus.SubscribeSessionEvent(EventSource.Network, this, handler);
}
protected void SubscribeLocalEvent<T>(EntityEventHandler<T> handler)
where T : EntitySystemMessage
{
EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, handler);
}
protected void SubscribeLocalEvent<T>(EntitySessionEventHandler<T> handler)
where T : EntitySystemMessage
{
EntityManager.EventBus.SubscribeSessionEvent(EventSource.Local, this, handler);
}
protected void UnsubscribeNetworkEvent<T>()
where T : EntitySystemMessage
{

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Input;
namespace Robust.Shared.GameObjects.Systems
{
public abstract class SharedInputSystem : EntitySystem
{
public abstract ICommandBindMapping BindMap { get; }
}
}

View File

@@ -2,6 +2,7 @@
using Robust.Shared.Serialization;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Robust.Shared.GameStates
@@ -25,10 +26,11 @@ namespace Robust.Shared.GameStates
/// <summary>
/// Constructor!
/// </summary>
public GameState(GameTick fromSequence, GameTick toSequence, List<EntityState> entities, List<PlayerState> players, List<EntityUid> deletions, GameStateMapData mapData)
public GameState(GameTick fromSequence, GameTick toSequence, uint lastInput, List<EntityState> entities, List<PlayerState> players, List<EntityUid> deletions, GameStateMapData mapData)
{
FromSequence = fromSequence;
ToSequence = toSequence;
LastProcessedInput = lastInput;
EntityStates = entities;
PlayerStates = players;
EntityDeletions = deletions;
@@ -38,8 +40,12 @@ namespace Robust.Shared.GameStates
public readonly GameTick FromSequence;
public readonly GameTick ToSequence;
public readonly uint LastProcessedInput;
[CanBeNull]
public readonly List<EntityState> EntityStates;
public readonly List<PlayerState> PlayerStates;
[CanBeNull]
public readonly List<EntityUid> EntityDeletions;
public readonly GameStateMapData MapData;
}

View File

@@ -25,12 +25,13 @@ namespace Robust.Shared.Input
/// <param name="disabled">The delegate to be ran when this command is disabled.</param>
/// <returns>The new input command.</returns>
public static InputCmdHandler FromDelegate(StateInputCmdDelegate enabled = null,
StateInputCmdDelegate disabled = null)
StateInputCmdDelegate disabled = null, bool handle=true)
{
return new StateInputCmdHandler
{
EnabledDelegate = enabled,
DisabledDelegate = disabled,
Handle = handle,
};
}
@@ -38,6 +39,7 @@ namespace Robust.Shared.Input
{
public StateInputCmdDelegate EnabledDelegate;
public StateInputCmdDelegate DisabledDelegate;
public bool Handle { get; set; }
public override void Enabled(ICommonSession session)
{
@@ -58,10 +60,10 @@ namespace Robust.Shared.Input
{
case BoundKeyState.Up:
Disabled(session);
return true;
return Handle;
case BoundKeyState.Down:
Enabled(session);
return true;
return Handle;
}
//Client Sanitization: unknown key state, just ignore

View File

@@ -10,7 +10,7 @@ namespace Robust.Shared.Input
/// Abstract class that all Input Commands derive from.
/// </summary>
[Serializable, NetSerializable]
public abstract class InputCmdMessage : EntitySystemMessage
public abstract class InputCmdMessage : EntitySystemMessage, IComparable<InputCmdMessage>
{
/// <summary>
/// Client tick this was created.
@@ -22,6 +22,11 @@ namespace Robust.Shared.Input
/// </summary>
public KeyFunctionId InputFunctionId { get; }
/// <summary>
/// Sequence number of this input command.
/// </summary>
public uint InputSequence { get; set; }
/// <summary>
/// Creates an instance of <see cref="InputCmdMessage"/>.
/// </summary>
@@ -32,6 +37,13 @@ namespace Robust.Shared.Input
Tick = tick;
InputFunctionId = inputFunctionId;
}
public int CompareTo(InputCmdMessage other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return InputSequence.CompareTo(other.InputSequence);
}
}
/// <summary>
@@ -143,11 +155,12 @@ namespace Robust.Shared.Input
/// Creates an instance of <see cref="FullInputCmdMessage"/>.
/// </summary>
/// <param name="tick">Client tick this was created.</param>
/// <param name="inputSequence"></param>
/// <param name="inputFunctionId">Function this command is changing.</param>
/// <param name="state">New state of the Input Function.</param>
/// <param name="coordinates">Local Coordinates of the pointer when the command was created.</param>
/// <param name="screenCoordinates"></param>
public FullInputCmdMessage(GameTick tick, KeyFunctionId inputFunctionId, BoundKeyState state, GridCoordinates coordinates, ScreenCoordinates screenCoordinates)
public FullInputCmdMessage(GameTick tick, int inputSequence, KeyFunctionId inputFunctionId, BoundKeyState state, GridCoordinates coordinates, ScreenCoordinates screenCoordinates)
: this(tick, inputFunctionId, state, coordinates, screenCoordinates, EntityUid.Invalid) { }
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Robust.Shared.Interfaces.Timing
@@ -101,5 +102,18 @@ namespace Robust.Shared.Interfaces.Timing
/// Resets the real uptime of the server.
/// </summary>
void ResetRealTime();
bool IsFirstTimePredicted { get; }
void StartPastPrediction();
void EndPastPrediction();
[MustUseReturnValue]
PredictionGuard StartPastPredictionArea()
{
StartPastPrediction();
return new PredictionGuard(this);
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace Robust.Shared.Interfaces.Timing
{
public readonly struct PredictionGuard : IDisposable
{
private readonly IGameTiming _gameTiming;
public PredictionGuard(IGameTiming gameTiming)
{
_gameTiming = gameTiming;
}
public void Dispose()
{
_gameTiming.EndPastPrediction();
}
}
}

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using System.Collections.Generic;
using Robust.Shared.Enums;
using Robust.Shared.Timing;
namespace Robust.Shared.Network.Messages
{
@@ -25,10 +26,14 @@ namespace Robust.Shared.Network.Messages
public EntityUid EntityUid { get; set; }
public uint NetId { get; set; }
public uint Sequence { get; set; }
public GameTick SourceTick { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Type = (EntityMessageType)buffer.ReadByte();
SourceTick = buffer.ReadGameTick();
Sequence = buffer.ReadUInt32();
switch (Type)
{
@@ -39,7 +44,6 @@ namespace Robust.Shared.Network.Messages
using (var stream = new MemoryStream(buffer.ReadBytes(messageLength)))
{
SystemMessage = serializer.Deserialize<EntitySystemMessage>(stream);
SystemMessage.NetChannel = MsgChannel;
}
}
break;
@@ -63,6 +67,8 @@ namespace Robust.Shared.Network.Messages
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write((byte)Type);
buffer.Write(SourceTick);
buffer.Write(Sequence);
switch (Type)
{

View File

@@ -0,0 +1,26 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
namespace Robust.Shared.Network.Messages
{
public class MsgSetTickRate : NetMessage
{
#region REQUIRED
public static readonly MsgGroups GROUP = MsgGroups.Core;
public static readonly string NAME = nameof(MsgSetTickRate);
public MsgSetTickRate(INetChannel channel) : base(NAME, GROUP) { }
#endregion
public byte NewTickRate { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
NewTickRate = buffer.ReadByte();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(NewTickRate);
}
}
}

View File

@@ -170,6 +170,7 @@ namespace Robust.Shared.Network
_config.RegisterCVar("net.sendbuffersize", 131071, CVar.ARCHIVE);
_config.RegisterCVar("net.receivebuffersize", 131071, CVar.ARCHIVE);
_config.RegisterCVar("net.verbose", false, CVar.ARCHIVE, NetVerboseChanged);
if (!isServer)
{
@@ -196,6 +197,14 @@ namespace Robust.Shared.Network
_initialized = true;
}
private void NetVerboseChanged(bool on)
{
foreach (var peer in _netPeers)
{
peer.Configuration.SetMessageTypeEnabled(NetIncomingMessageType.VerboseDebugMessage, on);
}
}
public void StartServer()
{
DebugTools.Assert(IsServer);
@@ -348,13 +357,16 @@ namespace Robust.Shared.Network
{
var netConfig = new NetPeerConfiguration("SS14_NetTag");
// ping the client 4 times every second.
netConfig.PingInterval = 0.25f;
// ping the client once per second.
netConfig.PingInterval = 1f;
netConfig.SendBufferSize = _config.GetCVar<int>("net.sendbuffersize");
netConfig.ReceiveBufferSize = _config.GetCVar<int>("net.receivebuffersize");
netConfig.MaximumHandshakeAttempts = 1;
var verbose = _config.GetCVar<bool>("net.verbose");
netConfig.SetMessageTypeEnabled(NetIncomingMessageType.VerboseDebugMessage, verbose);
if (IsServer)
{
netConfig.MaximumConnections = _config.GetCVar<int>("game.maxplayers");

View File

@@ -2,6 +2,7 @@ using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Shared.Network
{
@@ -44,5 +45,15 @@ namespace Robust.Shared.Network
{
message.Write((int)entityUid);
}
public static GameTick ReadGameTick(this NetIncomingMessage message)
{
return new GameTick(message.ReadUInt32());
}
public static void Write(this NetOutgoingMessage message, GameTick tick)
{
message.Write(tick.Value);
}
}
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.GameObjects;
namespace Robust.Shared.Players
{
@@ -11,5 +12,7 @@ namespace Robust.Shared.Players
/// Status of the session.
/// </summary>
SessionStatus Status { get; set; }
IEntity AttachedEntity { get; }
}
}

View File

@@ -70,6 +70,16 @@ namespace Robust.Shared.Timing
public static bool operator <(GameTick a, GameTick b) => a.Value < b.Value;
public static bool operator <=(GameTick a, GameTick b) => a.Value <= b.Value;
public static GameTick operator +(GameTick a, uint b)
{
return new GameTick(a.Value + b);
}
public static GameTick operator -(GameTick a, uint b)
{
return new GameTick(a.Value - b);
}
/// <inheritdoc />
public override string ToString()
{

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Timing
{
@@ -154,6 +155,24 @@ namespace Robust.Shared.Timing
_lastRealTime = TimeSpan.Zero;
}
public bool IsFirstTimePredicted { get; private set; } = true;
public void StartPastPrediction()
{
// Don't allow recursive predictions.
// Not sure if it's necessary yet and if not, great!
DebugTools.Assert(IsFirstTimePredicted);
IsFirstTimePredicted = false;
}
public void EndPastPrediction()
{
DebugTools.Assert(!IsFirstTimePredicted);
IsFirstTimePredicted = true;
}
/// <summary>
/// Calculates the average FPS of the last 50 real frame times.
/// </summary>

View File

@@ -158,7 +158,7 @@ namespace Robust.UnitTesting.Client.GameStates
/// </summary>
private static GameState GameStateFactory(uint from, uint to)
{
return new GameState(new GameTick(from), new GameTick(to), null, null, null, null);
return new GameState(new GameTick(@from), new GameTick(to), 0, null, null, null, null);
}
/// <summary>