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.Player;
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 IPlayerManager _playerManager = default!;
[Dependency] private IClientNetManager _networkManager = default!;
[Dependency] private IClientGameTiming _gameTiming = default!;
[Dependency] private IClientGameStateManager _stateMan = default!;
[Dependency] private IBaseClient _client = default!;
[Dependency] private IReplayRecordingManager _replayRecording = default!;
internal event Action? AfterStartup;
internal event Action? AfterShutdown;
private readonly Queue _queuedPredictedDeletions = new();
private readonly HashSet _queuedPredictedDeletionsSet = new();
private readonly HashSet _predictedDetachedEntities = new();
private Histogram? _tickUpdateHistogram;
private Histogram.Child? _entityNetHistogram;
public override void Initialize()
{
SetupNetworking();
ReceivedSystemMessage += (_, systemMsg) => EventBus.RaiseEvent(EventSource.Network, systemMsg);
base.Initialize();
}
public override void Startup()
{
base.Startup();
AfterStartup?.Invoke();
}
public override void Shutdown()
{
base.Shutdown();
AfterShutdown?.Invoke();
}
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();
_queuedPredictedDeletions.Clear();
_queuedPredictedDeletionsSet.Clear();
_predictedDetachedEntities.Clear();
using var _ = _gameTiming.StartStateApplicationArea();
base.FlushEntities();
}
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
{
return base.CreateEntity(prototypeName, out metadata);
}
///
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 || uid == EntityUid.Invalid)
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 DeleteEntity(EntityUid? uid)
{
if (uid != null)
ClearPredictedDeletion(uid.Value);
base.DeleteEntity(uid);
}
///
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 void DirtyField(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
{
// TODO Prediction
// does the client actually need to dirty the field?
// I.e., can't it just dirty the whole component to trigger a reset?
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.DirtyField(uid, comp, fieldName, metadata);
}
public override void DirtyFields(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
{
// TODO Prediction
// does the client actually need to dirty the field?
// I.e., can't it just dirty the whole component to trigger a reset?
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.DirtyFields(uid, comp, meta, fields);
}
///
public override void Dirty(Entity ent, MetaDataComponent? meta = null)
{
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
///
public override void Dirty(Entity ent, MetaDataComponent? meta = null)
{
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
///
public override void Dirty(Entity ent, MetaDataComponent? meta = null)
{
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
public override void RaisePredictiveEvent(T msg)
{
var session = _playerManager.LocalSession;
DebugTools.AssertNotNull(session);
var sequence = _stateMan.SystemMessageDispatched(msg);
EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
if (!_stateMan.IsPredictionEnabled && _client.RunLevel != ClientRunLevel.SinglePlayerGame)
return;
DebugTools.Assert(_gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted || _client.RunLevel == ClientRunLevel.SinglePlayerGame);
var eventArgs = new EntitySessionEventArgs(session!);
EventBus.RaiseEvent(EventSource.Local, msg);
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage(eventArgs, msg));
}
///
public override void RaiseSharedEvent(T message, EntityUid? user = null)
{
if (user == null || user != _playerManager.LocalEntity || !_gameTiming.IsFirstTimePredicted)
return;
EventBus.RaiseEvent(EventSource.Local, ref message);
}
///
public override void RaiseSharedEvent(T message, ICommonSession? user = null)
{
if (user == null || user != _playerManager.LocalSession || !_gameTiming.IsFirstTimePredicted)
return;
EventBus.RaiseEvent(EventSource.Local, ref message);
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;
///
public event EventHandler