using System; using System.Collections.Generic; using System.Linq; using Prometheus; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; namespace Robust.Shared.GameObjects { public delegate void EntityUidQueryCallback(EntityUid uid); /// [Virtual] public partial class EntityManager : IEntityManager { #region Dependencies [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ISerializationManager _serManager = default!; #endregion Dependencies /// public GameTick CurrentTick => _gameTiming.CurTick; IComponentFactory IEntityManager.ComponentFactory => ComponentFactory; /// public IEntitySystemManager EntitySysManager => _entitySystemManager; /// public virtual IEntityNetworkManager? EntityNetManager => null; protected readonly Queue QueuedDeletions = new(); protected readonly HashSet QueuedDeletionsSet = new(); /// /// All entities currently stored in the manager. /// protected readonly HashSet Entities = new(); private EntityEventBus _eventBus = null!; protected virtual int NextEntityUid { get; set; } = (int) EntityUid.FirstUid; /// public IEventBus EventBus => _eventBus; public event EventHandler? EntityAdded; public event EventHandler? EntityInitialized; public event EventHandler? EntityStarted; public event EventHandler? EntityDeleted; public event EventHandler? EntityDirtied; // only raised after initialization public bool Started { get; protected set; } public bool Initialized { get; protected set; } /// /// Constructs a new instance of . /// public EntityManager() { } public virtual void Initialize() { if (Initialized) throw new InvalidOperationException("Initialize() called multiple times"); _eventBus = new EntityEventBus(this); InitializeComponents(); Initialized = true; } public virtual void Startup() { if (Started) throw new InvalidOperationException("Startup() called multiple times"); // TODO: Probably better to call this on its own given it's so infrequent. _entitySystemManager.Initialize(); Started = true; } public virtual void Shutdown() { FlushEntities(); _eventBus.ClearEventTables(); _entitySystemManager.Shutdown(); ClearComponents(); Initialized = false; Started = false; } public void Cleanup() { QueuedDeletions.Clear(); QueuedDeletionsSet.Clear(); _entitySystemManager.Clear(); Entities.Clear(); _eventBus.Dispose(); _eventBus = null!; ClearComponents(); Initialized = false; Started = false; } public virtual void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { using (histogram?.WithLabels("EntitySystems").NewTimer()) { _entitySystemManager.TickUpdate(frameTime, noPredictions); } using (histogram?.WithLabels("EntityEventBus").NewTimer()) { _eventBus.ProcessEventQueue(); } using (histogram?.WithLabels("QueuedDeletion").NewTimer()) { while (QueuedDeletions.TryDequeue(out var uid)) { DeleteEntity(uid); } QueuedDeletionsSet.Clear(); } using (histogram?.WithLabels("ComponentCull").NewTimer()) { CullRemovedComponents(); } } public virtual void FrameUpdate(float frameTime) { _entitySystemManager.FrameUpdate(frameTime); } #region Entity Management public EntityUid CreateEntityUninitialized(string? prototypeName, EntityUid euid) { return CreateEntity(prototypeName, euid); } /// public virtual EntityUid CreateEntityUninitialized(string? prototypeName) { return CreateEntity(prototypeName); } /// public virtual EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates) { var newEntity = CreateEntity(prototypeName); if (coordinates.IsValid(this)) { GetComponent(newEntity).Coordinates = coordinates; } return newEntity; } /// public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates) { var newEntity = CreateEntity(prototypeName); var transform = GetComponent(newEntity); transform.AttachParent(_mapManager.GetMapEntityId(coordinates.MapId)); // TODO: Look at this bullshit. Please code a way to force-move an entity regardless of anchoring. var oldAnchored = transform.Anchored; transform.Anchored = false; transform.WorldPosition = coordinates.Position; transform.Anchored = oldAnchored; return newEntity; } /// public virtual EntityUid SpawnEntity(string? protoName, EntityCoordinates coordinates) { if (!coordinates.IsValid(this)) throw new InvalidOperationException($"Tried to spawn entity {protoName} on invalid coordinates {coordinates}."); var entity = CreateEntityUninitialized(protoName, coordinates); InitializeAndStartEntity(entity, coordinates.GetMapId(this)); return entity; } /// public virtual EntityUid SpawnEntity(string? protoName, MapCoordinates coordinates) { var entity = CreateEntityUninitialized(protoName, coordinates); InitializeAndStartEntity(entity, coordinates.MapId); return entity; } /// public int EntityCount => Entities.Count; /// public IEnumerable GetEntities() => Entities; /// /// Marks this entity as dirty so that it will be updated over the network. /// /// /// Calling Dirty on a component will call this directly. /// public void Dirty(EntityUid uid) { var currentTick = CurrentTick; // We want to retrieve MetaDataComponent even if its Deleted flag is set. if (!_entTraitArray[ArrayIndexFor()].TryGetValue(uid, out var component)) throw new KeyNotFoundException($"Entity {uid} does not exist, cannot dirty it."); var metadata = (MetaDataComponent)component; if (metadata.EntityLastModifiedTick == currentTick) return; metadata.EntityLastModifiedTick = currentTick; if (metadata.EntityLifeStage > EntityLifeStage.Initializing) { EntityDirtied?.Invoke(this, uid); } } public void Dirty(Component component) { var owner = component.Owner; // Deserialization will cause this to be true. // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (!owner.IsValid() || component.LifeStage >= ComponentLifeStage.Removing) return; if (!component.NetSyncEnabled) return; Dirty(owner); component.LastModifiedTick = CurrentTick; } /// /// Shuts-down and removes given Entity. This is also broadcast to all clients. /// /// Entity to remove public virtual void DeleteEntity(EntityUid e) { // Networking blindly spams entities at this function, they can already be // deleted from being a child of a previously deleted entity // TODO: Why does networking need to send deletes for child entities? if (!_entTraitArray[ArrayIndexFor()].TryGetValue(e, out var comp) || comp is not MetaDataComponent meta || meta.EntityDeleted) return; if (meta.EntityLifeStage == EntityLifeStage.Terminating) #if !EXCEPTION_TOLERANCE throw new InvalidOperationException("Called Delete on an entity already being deleted."); #else return; #endif RecursiveDeleteEntity(e); } private void RecursiveDeleteEntity(EntityUid uid) { if (!TryGetComponent(uid, out MetaDataComponent metadata) || metadata.EntityDeleted) return; //TODO: Why was this still a child if it was already deleted? var transform = GetComponent(uid); metadata.EntityLifeStage = EntityLifeStage.Terminating; var ev = new EntityTerminatingEvent(uid); EventBus.RaiseLocalEvent(uid, ref ev, false); // DeleteEntity modifies our _children collection, we must cache the collection to iterate properly foreach (var child in transform._children.ToArray()) { // Recursion Alert RecursiveDeleteEntity(child); } // Shut down all components. foreach (var component in InSafeOrder(_entCompIndex[uid])) { if(component.Running) component.LifeShutdown(this); } // map does not have a parent node, everything else needs to be detached if (transform.ParentUid != EntityUid.Invalid) { // Detach from my parent, if any transform.DetachParentToNull(); } // Dispose all my components, in a safe order so transform is available DisposeComponents(uid); metadata.EntityLifeStage = EntityLifeStage.Deleted; EntityDeleted?.Invoke(this, uid); EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(uid)); Entities.Remove(uid); } public void QueueDeleteEntity(EntityUid uid) { if(QueuedDeletionsSet.Add(uid)) QueuedDeletions.Enqueue(uid); } public bool IsQueuedForDeletion(EntityUid uid) => QueuedDeletionsSet.Contains(uid); public bool EntityExists(EntityUid uid) { return _entTraitArray[ArrayIndexFor()].ContainsKey(uid); } public bool EntityExists(EntityUid? uid) { return uid.HasValue && EntityExists(uid.Value); } public bool Deleted(EntityUid uid) { return !_entTraitArray[ArrayIndexFor()].TryGetValue(uid, out var comp) || ((MetaDataComponent) comp).EntityDeleted; } public bool Deleted(EntityUid? uid) { return !uid.HasValue || !_entTraitArray[ArrayIndexFor()].TryGetValue(uid.Value, out var comp) || ((MetaDataComponent) comp).EntityDeleted; } /// /// Disposes all entities and clears all lists. /// public void FlushEntities() { foreach (var e in GetEntities()) { DeleteEntity(e); } } /// /// Allocates an entity and stores it but does not load components or do initialization. /// private protected EntityUid AllocEntity(string? prototypeName, EntityUid uid = default) { EntityPrototype? prototype = null; if (!string.IsNullOrWhiteSpace(prototypeName)) { // If the prototype doesn't exist then we throw BEFORE we allocate the entity. prototype = PrototypeManager.Index(prototypeName); } var entity = AllocEntity(uid); GetComponent(entity).EntityPrototype = prototype; return entity; } /// /// Allocates an entity and stores it but does not load components or do initialization. /// private protected EntityUid AllocEntity(EntityUid uid = default) { if (uid == default) { uid = GenerateEntityUid(); } if (EntityExists(uid)) { throw new InvalidOperationException($"UID already taken: {uid}"); } // we want this called before adding components EntityAdded?.Invoke(this, uid); var metadata = new MetaDataComponent { Owner = uid }; Entities.Add(uid); // add the required MetaDataComponent directly. AddComponentInternal(uid, metadata, false, false); // allocate the required TransformComponent AddComponent(uid); return uid; } /// /// Allocates an entity and loads components but does not do initialization. /// private protected virtual EntityUid CreateEntity(string? prototypeName, EntityUid uid = default) { if (prototypeName == null) return AllocEntity(uid); var entity = AllocEntity(prototypeName, uid); try { EntityPrototype.LoadEntity(GetComponent(entity).EntityPrototype, entity, ComponentFactory, this, _serManager, null); return entity; } catch (Exception e) { // Exception during entity loading. // Need to delete the entity to avoid corrupt state causing crashes later. DeleteEntity(entity); throw new EntityCreationException($"Exception inside CreateEntity with prototype {prototypeName}", e); } } private protected void LoadEntity(EntityUid entity, IEntityLoadContext? context) { EntityPrototype.LoadEntity(GetComponent(entity).EntityPrototype, entity, ComponentFactory, this, _serManager, context); } private void InitializeAndStartEntity(EntityUid entity, MapId mapId) { try { InitializeEntity(entity); StartEntity(entity); // If the map we're initializing the entity on is initialized, run map init on it. if (_mapManager.IsMapInitialized(mapId)) entity.RunMapInit(); } catch (Exception e) { DeleteEntity(entity); throw new EntityCreationException("Exception inside InitializeAndStartEntity", e); } } protected void InitializeEntity(EntityUid entity) { InitializeComponents(entity); EntityInitialized?.Invoke(this, entity); } protected void StartEntity(EntityUid entity) { StartComponents(entity); EntityStarted?.Invoke(this, entity); } /// public virtual EntityStringRepresentation ToPrettyString(EntityUid uid) { // We want to retrieve the MetaData component even if it is deleted. if (!_entTraitArray[ArrayIndexFor()].TryGetValue(uid, out var component)) return new EntityStringRepresentation(uid, true); var metadata = (MetaDataComponent) component; return new EntityStringRepresentation(uid, metadata.EntityDeleted, metadata.EntityName, metadata.EntityPrototype?.ID); } #endregion Entity Management /// /// Factory for generating a new EntityUid for an entity currently being created. /// /// protected virtual EntityUid GenerateEntityUid() { return new(NextEntityUid++); } } public enum EntityMessageType : byte { Error = 0, SystemMessage } }