Files
Nitrox/NitroxServer/GameLogic/Entities/WorldEntityManager.cs
2025-07-06 00:23:46 +02:00

382 lines
14 KiB
C#

using System.Collections.Generic;
using System.Linq;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Unity;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxServer.GameLogic.Entities.Spawning;
namespace NitroxServer.GameLogic.Entities;
/// <remarks>
/// Regular <see cref="WorldEntity"/> are held in cells and should be registered in <see cref="worldEntitiesByBatchId"/> and <see cref="worldEntitiesByCell"/>.
/// But <see cref="GlobalRootEntity"/> are held in their own root object (GlobalRoot) so they should never be registered in cells (they're seeable at all times).
/// </remarks>
public class WorldEntityManager
{
private readonly EntityRegistry entityRegistry;
/// <summary>
/// World entities can disappear if you go out of range.
/// </summary>
private readonly Dictionary<AbsoluteEntityCell, Dictionary<NitroxId, WorldEntity>> worldEntitiesByCell;
/// <summary>
/// Global root entities that are always visible.
/// </summary>
private readonly Dictionary<NitroxId, GlobalRootEntity> globalRootEntitiesById;
private readonly BatchEntitySpawner batchEntitySpawner;
private readonly PlayerManager playerManager;
private readonly object worldEntitiesLock;
private readonly object globalRootEntitiesLock;
public WorldEntityManager(EntityRegistry entityRegistry, BatchEntitySpawner batchEntitySpawner, PlayerManager playerManager)
{
List<WorldEntity> worldEntities = entityRegistry.GetEntities<WorldEntity>();
globalRootEntitiesById = entityRegistry.GetEntities<GlobalRootEntity>().ToDictionary(entity => entity.Id);
worldEntitiesByCell = worldEntities.Where(entity => entity is not GlobalRootEntity)
.GroupBy(entity => entity.AbsoluteEntityCell)
.ToDictionary(group => group.Key, group => group.ToDictionary(entity => entity.Id, entity => entity));
this.entityRegistry = entityRegistry;
this.batchEntitySpawner = batchEntitySpawner;
this.playerManager = playerManager;
worldEntitiesLock = new();
globalRootEntitiesLock = new();
}
public List<GlobalRootEntity> GetGlobalRootEntities(bool rootOnly = false)
{
if (rootOnly)
{
return GetGlobalRootEntities<GlobalRootEntity>().Where(entity => entity.ParentId == null).ToList();
}
return GetGlobalRootEntities<GlobalRootEntity>();
}
public List<T> GetGlobalRootEntities<T>() where T : GlobalRootEntity
{
lock (globalRootEntitiesLock)
{
return new(globalRootEntitiesById.Values.OfType<T>());
}
}
public List<GlobalRootEntity> GetPersistentGlobalRootEntities()
{
// TODO: refactor if there are more entities that should not be persisted
return GetGlobalRootEntities(true).Where(entity =>
{
if (entity.Metadata is CyclopsMetadata cyclopsMetadata)
{
// Do not save cyclops wrecks
return !cyclopsMetadata.IsDestroyed;
}
return true;
}).ToList();
}
public List<WorldEntity> GetEntities(AbsoluteEntityCell cell)
{
lock (worldEntitiesLock)
{
if (worldEntitiesByCell.TryGetValue(cell, out Dictionary<NitroxId, WorldEntity> batchEntities))
{
return batchEntities.Values.ToList();
}
}
return [];
}
public bool TryUpdateEntityPosition(NitroxId id, NitroxVector3 position, NitroxQuaternion rotation, out AbsoluteEntityCell newCell, out WorldEntity worldEntity)
{
lock (worldEntitiesLock)
{
if (!entityRegistry.TryGetEntityById(id, out worldEntity))
{
Log.WarnOnce($"[{nameof(WorldEntityManager)}] Can't update entity position of {id} because it isn't registered");
newCell = null;
return false;
}
AbsoluteEntityCell oldCell = worldEntity.AbsoluteEntityCell;
worldEntity.Transform.Position = position;
worldEntity.Transform.Rotation = rotation;
newCell = worldEntity.AbsoluteEntityCell;
if (oldCell != newCell)
{
EntitySwitchedCells(worldEntity, oldCell, newCell);
}
return true;
}
}
public Optional<Entity> RemoveGlobalRootEntity(NitroxId entityId, bool removeFromRegistry = true)
{
Optional<Entity> removedEntity = Optional.Empty;
lock (globalRootEntitiesLock)
{
if (removeFromRegistry)
{
// In case there were player entities under the removed entity, we need to reparent them to the GlobalRoot
// to make sure that they won't be removed
if (entityRegistry.TryGetEntityById(entityId, out GlobalRootEntity globalRootEntity))
{
MovePlayerChildrenToRoot(globalRootEntity);
}
removedEntity = entityRegistry.RemoveEntity(entityId);
}
globalRootEntitiesById.Remove(entityId);
}
return removedEntity;
}
public void MovePlayerChildrenToRoot(GlobalRootEntity globalRootEntity)
{
List<PlayerWorldEntity> playerEntities = FindPlayerEntitiesInChildren(globalRootEntity);
foreach (PlayerWorldEntity childPlayerEntity in playerEntities)
{
// Reparent the entity on top of GlobalRoot
globalRootEntity.ChildEntities.Remove(childPlayerEntity);
childPlayerEntity.ParentId = null;
// Make sure the PlayerEntity is correctly registered
AddOrUpdateGlobalRootEntity(childPlayerEntity);
}
}
public void TrackEntityInTheWorld(WorldEntity entity)
{
if (entity is GlobalRootEntity globalRootEntity)
{
AddOrUpdateGlobalRootEntity(globalRootEntity, false);
return;
}
RegisterWorldEntity(entity);
}
/// <summary>
/// Automatically registers a WorldEntity in its AbsoluteEntityCell
/// </summary>
/// <remarks>
/// The provided should NOT be a GlobalRootEntity (they don't stand in cells)
/// </remarks>
public void RegisterWorldEntity(WorldEntity entity)
{
RegisterWorldEntityInCell(entity, entity.AbsoluteEntityCell);
}
public void RegisterWorldEntityInCell(WorldEntity entity, AbsoluteEntityCell cell)
{
lock (worldEntitiesLock)
{
if (!worldEntitiesByCell.TryGetValue(cell, out Dictionary<NitroxId, WorldEntity> worldEntitiesInCell))
{
worldEntitiesInCell = worldEntitiesByCell[cell] = [];
}
worldEntitiesInCell[entity.Id] = entity;
}
}
/// <summary>
/// Automatically unregisters a WorldEntity in its AbsoluteEntityCell
/// </summary>
public void UnregisterWorldEntity(WorldEntity entity)
{
UnregisterWorldEntityFromCell(entity.Id, entity.AbsoluteEntityCell);
}
public void UnregisterWorldEntityFromCell(NitroxId entityId, AbsoluteEntityCell cell)
{
lock (worldEntitiesLock)
{
if (worldEntitiesByCell.TryGetValue(cell, out Dictionary<NitroxId, WorldEntity> worldEntitiesInCell))
{
worldEntitiesInCell.Remove(entityId);
}
}
}
public void LoadAllUnspawnedEntities(System.Threading.CancellationToken token)
{
IMap map = NitroxServiceLocator.LocateService<IMap>();
int totalBatches = map.DimensionsInBatches.X * map.DimensionsInBatches.Y * map.DimensionsInBatches.Z;
int batchesLoaded = 0;
for (int x = 0; x < map.DimensionsInBatches.X; x++)
{
token.ThrowIfCancellationRequested();
for (int y = 0; y < map.DimensionsInBatches.Y; y++)
{
for (int z = 0; z < map.DimensionsInBatches.Z; z++)
{
int spawned = LoadUnspawnedEntities(new(x, y, z), true);
Log.Debug($"Loaded {spawned} entities from batch ({x}, {y}, {z})");
batchesLoaded++;
}
}
if (batchesLoaded > 0)
{
Log.Info($"Loading : {(int)(100f * batchesLoaded / totalBatches)}%");
}
}
}
public int LoadUnspawnedEntities(NitroxInt3 batchId, bool suppressLogs)
{
List<Entity> spawnedEntities = batchEntitySpawner.LoadUnspawnedEntities(batchId, suppressLogs);
List<WorldEntity> entitiesInCells = spawnedEntities.Where(entity => typeof(WorldEntity).IsAssignableFrom(entity.GetType()) &&
entity.GetType() != typeof(CellRootEntity) &&
entity.GetType() != typeof(GlobalRootEntity))
.Cast<WorldEntity>()
.ToList();
// UWE stores entities serialized with a handful of parent cell roots. These only represent a small fraction of all possible cell
// roots that could exist. There is no reason for the server to know about these and much easier to consider top-level world entities
// as positioned globally and not locally. Thus, we promote cell root children to top level and throw the cell roots away.
foreach (CellRootEntity cellRoot in spawnedEntities.OfType<CellRootEntity>())
{
foreach (WorldEntity worldEntity in cellRoot.ChildEntities.Cast<WorldEntity>())
{
worldEntity.ParentId = null;
worldEntity.Transform.SetParent(null, true);
entitiesInCells.Add(worldEntity);
}
cellRoot.ChildEntities = new List<Entity>();
}
// Specific type of entities which is not parented to a CellRootEntity
entitiesInCells.AddRange(spawnedEntities.OfType<SerializedWorldEntity>());
entityRegistry.AddEntitiesIgnoringDuplicate(entitiesInCells.OfType<Entity>().ToList());
foreach (WorldEntity entity in entitiesInCells)
{
RegisterWorldEntity(entity);
}
return entitiesInCells.Count;
}
private void EntitySwitchedCells(WorldEntity entity, AbsoluteEntityCell oldCell, AbsoluteEntityCell newCell)
{
if (entity is GlobalRootEntity)
{
return; // We don't care what cell a global root entity resides in. Only phasing entities.
}
if (oldCell != newCell)
{
lock (worldEntitiesLock)
{
// Specifically remove entity from oldCell
UnregisterWorldEntityFromCell(entity.Id, oldCell);
// Automatically add entity to its new cell
RegisterWorldEntityInCell(entity, newCell);
// It can happen for some players that the entity moves to a loaded cell of theirs, but that they hadn't spawned it in the first place
foreach (Player player in playerManager.ConnectedPlayers())
{
if (player.HasCellLoaded(newCell) && !player.HasCellLoaded(oldCell))
{
player.SendPacket(new SpawnEntities(entity));
}
}
}
}
}
public void StopTrackingEntity(WorldEntity entity)
{
if (entity is GlobalRootEntity)
{
RemoveGlobalRootEntity(entity.Id, false);
}
else
{
UnregisterWorldEntity(entity);
}
}
public bool TryDestroyEntity(NitroxId entityId, out Entity entity)
{
Optional<Entity> optEntity = entityRegistry.RemoveEntity(entityId);
if (!optEntity.HasValue)
{
entity = null;
return false;
}
entity = optEntity.Value;
if (entity is WorldEntity worldEntity)
{
StopTrackingEntity(worldEntity);
}
return true;
}
/// <summary>
/// To avoid risking not having the same entity in <see cref="globalRootEntitiesById"/> and in EntityRegistry, we update both at the same time.
/// </summary>
public void AddOrUpdateGlobalRootEntity(GlobalRootEntity entity, bool addOrUpdateRegistry = true)
{
lock (globalRootEntitiesLock)
{
if (addOrUpdateRegistry)
{
entityRegistry.AddOrUpdate(entity);
}
globalRootEntitiesById[entity.Id] = entity;
}
}
/// <summary>
/// Iterative breadth-first search which gets all children player entities in <paramref name="parentEntity"/>'s hierarchy.
/// </summary>
private List<PlayerWorldEntity> FindPlayerEntitiesInChildren(Entity parentEntity)
{
List<PlayerWorldEntity> playerWorldEntities = [];
List<Entity> entitiesToSearch = [parentEntity];
while (entitiesToSearch.Count > 0)
{
Entity currentEntity = entitiesToSearch[^1];
entitiesToSearch.RemoveAt(entitiesToSearch.Count - 1);
if (currentEntity is PlayerWorldEntity playerWorldEntity)
{
playerWorldEntities.Add(playerWorldEntity);
}
else
{
entitiesToSearch.InsertRange(0, currentEntity.ChildEntities);
}
}
return playerWorldEntities;
}
}