mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
359 lines
14 KiB
C#
359 lines
14 KiB
C#
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
|
|
{
|
|
/// <inheritdoc />
|
|
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;
|
|
[Dependency] private readonly IClientNetManager _network;
|
|
[Dependency] private readonly IBaseClient _client;
|
|
[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 />
|
|
public int MinBufferSize => _processor.MinBufferSize;
|
|
|
|
/// <inheritdoc />
|
|
public int TargetBufferSize => _processor.TargetBufferSize;
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
|
|
/// <inheritdoc />
|
|
public void Initialize()
|
|
{
|
|
_processor = new GameStateProcessor(_timing);
|
|
|
|
_network.RegisterNetMessage<MsgState>(MsgState.NAME, HandleStateMessage);
|
|
_network.RegisterNetMessage<MsgStateAck>(MsgStateAck.NAME);
|
|
_client.RunLevelChanged += RunLevelChanged;
|
|
|
|
_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)
|
|
{
|
|
if (args.NewLevel == ClientRunLevel.Initialize)
|
|
{
|
|
// We JUST left a server or the client started up, Reset everything.
|
|
Reset();
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
_timing.CurTick = _lastProcessedTick + 1;
|
|
|
|
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)
|
|
{
|
|
var msg = _network.CreateNetMessage<MsgStateAck>();
|
|
msg.Sequence = sequence;
|
|
_network.ClientSendMessage(msg);
|
|
}
|
|
|
|
private List<EntityUid> ApplyGameState(GameState curState, GameState nextState)
|
|
{
|
|
_mapManager.ApplyGameStatePre(curState.MapData);
|
|
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;
|
|
}
|
|
}
|
|
|
|
public class GameStateAppliedArgs : EventArgs
|
|
{
|
|
public GameState AppliedState { get; }
|
|
|
|
public GameStateAppliedArgs(GameState appliedState)
|
|
{
|
|
AppliedState = appliedState;
|
|
}
|
|
}
|
|
}
|