using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.Util; namespace NitroxServer.GameLogic.Entities { public class EntityRegistry { private readonly ConcurrentDictionary entitiesById = new(); public Optional GetEntityById(NitroxId id) where T : Entity { TryGetEntityById(id, out T entity); return Optional.OfNullable(entity); } public Optional GetEntityById(NitroxId id) { return GetEntityById(id); } public bool TryGetEntityById(NitroxId id, out T entity) where T : Entity { if (entitiesById.TryGetValue(id, out Entity _entity) && _entity is T typedEntity) { entity = typedEntity; return true; } entity = null; return false; } public List GetAllEntities(bool exceptGlobalRoot = false) { if (exceptGlobalRoot) { return new(entitiesById.Values.Where(entity => entity is not GlobalRootEntity)); } return new List(entitiesById.Values); } public List GetEntities(List ids) { return entitiesById.Join(ids, entity => entity.Value.Id, id => id, (entity, id) => entity.Value) .ToList(); } public List GetEntities() { return entitiesById.Values.OfType().ToList(); } public void AddEntity(Entity entity) { if (!entitiesById.TryAdd(entity.Id, entity)) { // Log an error to show stack trace but don't halt execution. Log.Error(new InvalidOperationException(), $"Trying to add duplicate entity {entity.Id}"); } } /// /// Registers or updates an entity and its children. /// public void AddOrUpdate(Entity entity) { if (!entitiesById.TryAdd(entity.Id, entity)) { Entity current = entitiesById[entity.Id]; RemoveFromParent(current); entitiesById.TryUpdate(entity.Id, entity, current); } AddToParent(entity); AddEntitiesIgnoringDuplicate(entity.ChildEntities); } public void AddEntities(IEnumerable entities) { foreach(Entity entity in entities) { AddEntity(entity); } } /// /// Used for situations when some children may be new but others may not be. For /// example a dropped InventoryEntity turns into a WorldEntity but keeps its /// battery inside (already known). /// /// /// Updates entities if they already exist /// public void AddEntitiesIgnoringDuplicate(IEnumerable entities) { foreach (Entity entity in entities) { if (entitiesById.TryGetValue(entity.Id, out Entity currentEntity)) { entitiesById.TryUpdate(entity.Id, entity, currentEntity); } else { entitiesById.TryAdd(entity.Id, entity); } AddEntitiesIgnoringDuplicate(entity.ChildEntities); } } public Optional RemoveEntity(NitroxId id) { if (entitiesById.TryRemove(id, out Entity entity)) { RemoveFromParent(entity); foreach (Entity child in entity.ChildEntities) { RemoveEntity(child.Id); } } return Optional.OfNullable(entity); } public void AddToParent(Entity entity) { if (entity.ParentId != null) { Optional parent = GetEntityById(entity.ParentId); if (parent.HasValue) { parent.Value.ChildEntities.Add(entity); } } } public void RemoveFromParent(Entity entity) { if (entity.ParentId != null && TryGetEntityById(entity.ParentId, out Entity parentEntity)) { parentEntity.ChildEntities.RemoveAll(childEntity => childEntity.Id.Equals(entity.Id)); entity.ParentId = null; if (entity is WorldEntity worldEntity && worldEntity.Transform != null) { worldEntity.Transform.SetParent(null, true); } } } /// /// Removes all children from /// public void CleanChildren(Entity entity) { for (int i = entity.ChildEntities.Count - 1; i >= 0; i--) { RemoveEntity(entity.ChildEntities[i].Id); } } public void ReparentEntity(NitroxId entityId, NitroxId newParentId) { if (entityId == null || !TryGetEntityById(entityId, out Entity entity)) { Log.Error($"Could not find entity to reparent: {entityId}"); return; } ReparentEntity(entity, newParentId); } public void ReparentEntity(NitroxId entityId, Entity newParent) { if (entityId == null || !TryGetEntityById(entityId, out Entity entity)) { Log.Error($"Could not find entity to reparent: {entityId}"); return; } ReparentEntity(entity, newParent); } public void ReparentEntity(Entity entity, NitroxId newParentId) { Entity parentEntity = newParentId != null ? GetEntityById(newParentId).Value : null; ReparentEntity(entity, parentEntity); } public void ReparentEntity(Entity entity, Entity newParent) { RemoveFromParent(entity); if (newParent == null) { return; } if (entity is WorldEntity worldEntity && worldEntity.Transform != null && newParent is WorldEntity parentWorldEntity && parentWorldEntity.Transform != null) { worldEntity.Transform.SetParent(parentWorldEntity.Transform, true); } entity.ParentId = newParent.Id; newParent.ChildEntities.Add(entity); } public void TransferChildren(NitroxId parentId, NitroxId newParentId, Func filter = null) { if (!TryGetEntityById(parentId, out Entity parentEntity)) { Log.Error($"[{nameof(EntityRegistry.TransferChildren)}] Couldn't find origin parent entity for {parentId}"); return; } if (!TryGetEntityById(newParentId, out Entity newParentEntity)) { Log.Error($"[{nameof(EntityRegistry.TransferChildren)}] Couldn't find new parent entity for {newParentId}"); return; } TransferChildren(parentEntity, newParentEntity, filter); } public void TransferChildren(Entity parent, Entity newParent, Func filter = null) { List childrenToMove = filter != null ? [.. parent.ChildEntities.Where(filter)] : parent.ChildEntities; // In case parent == newParent (which is actually a case used) we need removal to happen before adding the entities back parent.ChildEntities.RemoveAll(entity => filter(entity)); foreach (Entity childEntity in childrenToMove) { childEntity.ParentId = newParent.Id; newParent.ChildEntities.Add(childEntity); } } } }