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 { /// /// Manager for entities -- controls things like template loading and instantiation /// 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); } /// 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}"); } /// public override void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null) { Dirty(new Entity(uid, component), meta); } /// public override void Dirty(Entity 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 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(eventArgs, msg)); } #region IEntityNetworkManager impl public override IEntityNetworkManager EntityNetManager => this; /// public event EventHandler? ReceivedSystemMessage; private readonly PriorityQueue<(uint seq, MsgEntity msg)> _queue = new(new MessageTickComparer()); private uint _incomingMsgSequence = 0; /// public void SetupNetworking() { _networkManager.RegisterNetMessage(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); } /// 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); } /// 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 } }