mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
232 lines
8.6 KiB
C#
232 lines
8.6 KiB
C#
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.Replays;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Client.GameObjects
|
|
{
|
|
/// <summary>
|
|
/// Manager for entities -- controls things like template loading and instantiation
|
|
/// </summary>
|
|
public sealed partial class ClientEntityManager : EntityManager, IClientEntityManagerInternal
|
|
{
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
|
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
|
[Dependency] private readonly IClientGameStateManager _stateMan = default!;
|
|
[Dependency] private readonly IBaseClient _client = default!;
|
|
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
SetupNetworking();
|
|
ReceivedSystemMessage += (_, systemMsg) => EventBus.RaiseEvent(EventSource.Network, systemMsg);
|
|
|
|
base.Initialize();
|
|
}
|
|
|
|
public override void FlushEntities()
|
|
{
|
|
// Server doesn't network deletions on client shutdown so we need to
|
|
// manually clear these out or risk stale data getting used.
|
|
PendingNetEntityStates.Clear();
|
|
using var _ = _gameTiming.StartStateApplicationArea();
|
|
base.FlushEntities();
|
|
}
|
|
|
|
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
|
|
{
|
|
return base.CreateEntity(prototypeName, out metadata);
|
|
}
|
|
|
|
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)
|
|
{
|
|
base.InitializeEntity(entity, meta);
|
|
}
|
|
|
|
void IClientEntityManagerInternal.StartEntity(EntityUid entity)
|
|
{
|
|
base.StartEntity(entity);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void DirtyEntity(EntityUid uid, MetaDataComponent? meta = null)
|
|
{
|
|
// Client only dirties during prediction
|
|
if (_gameTiming.InPrediction)
|
|
base.DirtyEntity(uid, meta);
|
|
}
|
|
|
|
public override void QueueDeleteEntity(EntityUid? uid)
|
|
{
|
|
if (uid == null)
|
|
return;
|
|
|
|
if (IsClientSide(uid.Value))
|
|
{
|
|
base.QueueDeleteEntity(uid);
|
|
return;
|
|
}
|
|
|
|
if (ShuttingDown)
|
|
return;
|
|
|
|
// Client-side entity deletion is not supported and will cause errors.
|
|
if (_client.RunLevel == ClientRunLevel.Connected || _client.RunLevel == ClientRunLevel.InGame)
|
|
LogManager.RootSawmill.Error($"Predicting the queued deletion of a networked entity: {ToPrettyString(uid.Value)}. Trace: {Environment.StackTrace}");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null)
|
|
{
|
|
Dirty(new Entity<IComponent>(uid, component), meta);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
|
|
{
|
|
// Client only dirties during prediction
|
|
if (_gameTiming.InPrediction)
|
|
base.Dirty(ent, meta);
|
|
}
|
|
|
|
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
|
|
{
|
|
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
|
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
|
|
|
|
return base.ToPrettyString(uid);
|
|
}
|
|
|
|
public override void RaisePredictiveEvent<T>(T msg)
|
|
{
|
|
var localPlayer = _playerManager.LocalPlayer;
|
|
DebugTools.AssertNotNull(localPlayer);
|
|
|
|
var sequence = _stateMan.SystemMessageDispatched(msg);
|
|
EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
|
|
|
|
DebugTools.Assert(!_stateMan.IsPredictionEnabled || _gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted || _client.RunLevel != ClientRunLevel.Connected);
|
|
|
|
var eventArgs = new EntitySessionEventArgs(localPlayer!.Session);
|
|
EventBus.RaiseEvent(EventSource.Local, msg);
|
|
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
|
|
}
|
|
|
|
#region IEntityNetworkManager impl
|
|
|
|
public override IEntityNetworkManager EntityNetManager => this;
|
|
|
|
/// <inheritdoc />
|
|
public event EventHandler<object>? ReceivedSystemMessage;
|
|
|
|
private readonly PriorityQueue<(uint seq, MsgEntity msg)> _queue = new(new MessageTickComparer());
|
|
private uint _incomingMsgSequence = 0;
|
|
|
|
/// <inheritdoc />
|
|
public void SetupNetworking()
|
|
{
|
|
_networkManager.RegisterNetMessage<MsgEntity>(HandleEntityNetworkMessage);
|
|
}
|
|
|
|
public override void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram)
|
|
{
|
|
using (histogram?.WithLabels("EntityNet").NewTimer())
|
|
{
|
|
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick)
|
|
{
|
|
var (_, msg) = _queue.Take();
|
|
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
|
|
DispatchReceivedNetworkMsg(msg);
|
|
}
|
|
}
|
|
|
|
base.TickUpdate(frameTime, noPredictions, histogram);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SendSystemNetworkMessage(EntityEventArgs message, bool recordReplay = true)
|
|
{
|
|
SendSystemNetworkMessage(message, default(uint));
|
|
}
|
|
|
|
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
|
|
{
|
|
var msg = new MsgEntity();
|
|
msg.Type = EntityMessageType.SystemMessage;
|
|
msg.SystemMessage = message;
|
|
msg.SourceTick = _gameTiming.CurTick;
|
|
msg.Sequence = sequence;
|
|
|
|
_networkManager.ClientSendMessage(msg);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel? channel)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
private void HandleEntityNetworkMessage(MsgEntity message)
|
|
{
|
|
if (message.SourceTick <= _gameTiming.LastRealTick)
|
|
{
|
|
DispatchReceivedNetworkMsg(message);
|
|
return;
|
|
}
|
|
|
|
// MsgEntity is sent with ReliableOrdered so Lidgren guarantees ordering of incoming messages.
|
|
// We still need to store a sequence input number to ensure ordering remains consistent in
|
|
// the priority queue.
|
|
_queue.Add((++_incomingMsgSequence, message));
|
|
}
|
|
|
|
private void DispatchReceivedNetworkMsg(MsgEntity message)
|
|
{
|
|
switch (message.Type)
|
|
{
|
|
case EntityMessageType.SystemMessage:
|
|
|
|
// TODO REPLAYS handle late messages.
|
|
// If a message was received late, it will be recorded late here.
|
|
// Maybe process the replay to prevent late messages when playing back?
|
|
_replayRecording.RecordReplayMessage(message.SystemMessage);
|
|
|
|
DispatchReceivedNetworkMsg(message.SystemMessage);
|
|
return;
|
|
}
|
|
}
|
|
|
|
public void DispatchReceivedNetworkMsg(EntityEventArgs msg)
|
|
{
|
|
var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType());
|
|
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!;
|
|
ReceivedSystemMessage?.Invoke(this, msg);
|
|
ReceivedSystemMessage?.Invoke(this, sessionMsg);
|
|
}
|
|
|
|
private sealed class MessageTickComparer : IComparer<(uint seq, MsgEntity msg)>
|
|
{
|
|
public int Compare((uint seq, MsgEntity msg) x, (uint seq, MsgEntity msg) y)
|
|
{
|
|
var cmp = y.msg.SourceTick.CompareTo(x.msg.SourceTick);
|
|
if (cmp != 0)
|
|
{
|
|
return cmp;
|
|
}
|
|
|
|
return y.seq.CompareTo(x.seq);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|