first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Unity;
using ProtoBufNet;
namespace NitroxServer.GameLogic.Entities;
[DataContract]
public class EntityData
{
[DataMember(Order = 1)]
public List<Entity> Entities = [];
[ProtoAfterDeserialization]
private void ProtoAfterDeserialization()
{
// After deserialization, we want to assign all of the
// children to their respective parent entities.
Dictionary<NitroxId, Entity> entitiesById = Entities.ToDictionary(entity => entity.Id);
foreach (Entity entity in Entities)
{
if (entity is WorldEntity we)
{
NitroxVector3 pos = we.Transform.LocalPosition;
if (float.IsNaN(pos.X) || float.IsNaN(pos.Y) || float.IsNaN(pos.Z) ||
float.IsInfinity(pos.X) || float.IsInfinity(pos.Y) || float.IsInfinity(pos.Z))
{
Log.Error("Found WorldEntity with NaN or infinite position. Teleporting it to world origin.");
we.Transform.LocalPosition = NitroxVector3.Zero;
}
NitroxQuaternion rot = we.Transform.LocalRotation;
if (float.IsNaN(rot.X) || float.IsNaN(rot.Y) || float.IsNaN(rot.Z) || float.IsNaN(rot.W) ||
float.IsInfinity(rot.X) || float.IsInfinity(rot.Y) || float.IsInfinity(rot.Z) || float.IsInfinity(rot.W))
{
Log.Error("Found WorldEntity with NaN or infinite rotation. Resetting rotation.");
we.Transform.LocalRotation = NitroxQuaternion.Identity;
}
}
// We will re-build the child hierarchy below and want to avoid duplicates.
// TODO: Rework system to no longer persist children entities because they are duplicates.
entity.ChildEntities.Clear();
if (entity.ParentId == null)
{
continue;
}
if (entitiesById.TryGetValue(entity.ParentId, out Entity parent))
{
parent.ChildEntities.Add(entity);
if (entity is WorldEntity we2 && parent is WorldEntity weParent)
{
we2.Transform.SetParent(weParent.Transform, false);
}
}
}
}
[OnDeserialized]
private void JsonAfterDeserialization(StreamingContext context)
{
ProtoAfterDeserialization();
}
public static EntityData From(List<Entity> entities)
{
return new EntityData { Entities = entities };
}
}

View File

@@ -0,0 +1,246 @@
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<NitroxId, Entity> entitiesById = new();
public Optional<T> GetEntityById<T>(NitroxId id) where T : Entity
{
TryGetEntityById(id, out T entity);
return Optional.OfNullable(entity);
}
public Optional<Entity> GetEntityById(NitroxId id)
{
return GetEntityById<Entity>(id);
}
public bool TryGetEntityById<T>(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<Entity> GetAllEntities(bool exceptGlobalRoot = false)
{
if (exceptGlobalRoot)
{
return new(entitiesById.Values.Where(entity => entity is not GlobalRootEntity));
}
return new List<Entity>(entitiesById.Values);
}
public List<Entity> GetEntities(List<NitroxId> ids)
{
return entitiesById.Join(ids,
entity => entity.Value.Id,
id => id,
(entity, id) => entity.Value)
.ToList();
}
public List<T> GetEntities<T>()
{
return entitiesById.Values.OfType<T>().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}");
}
}
/// <summary>
/// Registers or updates an entity and its children.
/// </summary>
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<Entity> entities)
{
foreach(Entity entity in entities)
{
AddEntity(entity);
}
}
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// Updates entities if they already exist
/// </remarks>
public void AddEntitiesIgnoringDuplicate(IEnumerable<Entity> 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<Entity> 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<Entity> 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);
}
}
}
/// <summary>
/// Removes all children from <paramref name="entity"/>
/// </summary>
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<Entity, bool> 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<Entity, bool> filter = null)
{
List<Entity> 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);
}
}
}
}

View File

@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Packets;
namespace NitroxServer.GameLogic.Entities;
public class EntitySimulation
{
private const SimulationLockType DEFAULT_ENTITY_SIMULATION_LOCKTYPE = SimulationLockType.TRANSIENT;
private readonly EntityRegistry entityRegistry;
private readonly WorldEntityManager worldEntityManager;
private readonly PlayerManager playerManager;
private readonly ISimulationWhitelist simulationWhitelist;
private readonly SimulationOwnershipData simulationOwnershipData;
public EntitySimulation(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, ISimulationWhitelist simulationWhitelist)
{
this.entityRegistry = entityRegistry;
this.worldEntityManager = worldEntityManager;
this.simulationOwnershipData = simulationOwnershipData;
this.playerManager = playerManager;
this.simulationWhitelist = simulationWhitelist;
}
public List<SimulatedEntity> GetSimulationChangesForCell(Player player, AbsoluteEntityCell cell)
{
List<WorldEntity> entities = worldEntityManager.GetEntities(cell);
List<WorldEntity> addedEntities = FilterSimulatableEntities(player, entities);
List<SimulatedEntity> ownershipChanges = new();
foreach (WorldEntity entity in addedEntities)
{
bool doesEntityMove = ShouldSimulateEntityMovement(entity);
ownershipChanges.Add(new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE));
}
return ownershipChanges;
}
public void FillWithRemovedCells(Player player, AbsoluteEntityCell removedCell, List<SimulatedEntity> ownershipChanges)
{
List<WorldEntity> entities = worldEntityManager.GetEntities(removedCell);
IEnumerable<WorldEntity> revokedEntities = entities.Where(entity => !player.CanSee(entity) && simulationOwnershipData.RevokeIfOwner(entity.Id, player));
AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges);
}
public void BroadcastSimulationChanges(List<SimulatedEntity> ownershipChanges)
{
if (ownershipChanges.Count > 0)
{
SimulationOwnershipChange ownershipChange = new(ownershipChanges);
playerManager.SendPacketToAllPlayers(ownershipChange);
}
}
public List<SimulatedEntity> CalculateSimulationChangesFromPlayerDisconnect(Player player)
{
List<SimulatedEntity> ownershipChanges = new();
List<NitroxId> revokedEntityIds = simulationOwnershipData.RevokeAllForOwner(player);
List<Entity> revokedEntities = entityRegistry.GetEntities(revokedEntityIds);
AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges);
return ownershipChanges;
}
public SimulatedEntity AssignNewEntityToPlayer(Entity entity, Player player, bool shouldEntityMove = true)
{
if (simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE))
{
bool doesEntityMove = shouldEntityMove && entity is WorldEntity worldEntity && ShouldSimulateEntityMovement(worldEntity);
return new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE);
}
throw new Exception($"New entity was already being simulated by someone else: {entity.Id}");
}
public List<SimulatedEntity> AssignGlobalRootEntitiesAndGetData(Player player)
{
List<SimulatedEntity> simulatedEntities = new();
foreach (GlobalRootEntity entity in worldEntityManager.GetGlobalRootEntities())
{
simulationOwnershipData.TryToAcquire(entity.Id, player, SimulationLockType.TRANSIENT);
if (!simulationOwnershipData.TryGetLock(entity.Id, out SimulationOwnershipData.PlayerLock playerLock))
{
continue;
}
bool doesEntityMove = ShouldSimulateEntityMovement(entity);
SimulatedEntity simulatedEntity = new(entity.Id, playerLock.Player.Id, doesEntityMove, playerLock.LockType);
simulatedEntities.Add(simulatedEntity);
}
return simulatedEntities;
}
private void AssignEntitiesToOtherPlayers(Player oldPlayer, IEnumerable<Entity> entities, List<SimulatedEntity> ownershipChanges)
{
List<Player> otherPlayers = playerManager.GetConnectedPlayersExcept(oldPlayer);
foreach (Entity entity in entities)
{
if (TryAssignEntityToPlayers(otherPlayers, entity, out SimulatedEntity simulatedEntity))
{
ownershipChanges.Add(simulatedEntity);
}
}
}
public bool TryAssignEntityToPlayers(List<Player> players, Entity entity, out SimulatedEntity simulatedEntity)
{
NitroxId id = entity.Id;
foreach (Player player in players)
{
if (player.CanSee(entity) && simulationOwnershipData.TryToAcquire(id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE))
{
bool doesEntityMove = entity is WorldEntity worldEntity && ShouldSimulateEntityMovement(worldEntity);
Log.Verbose($"Player {player.Name} has taken over simulating {id}");
simulatedEntity = new(id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE);
return true;
}
}
simulatedEntity = null;
return false;
}
private List<WorldEntity> FilterSimulatableEntities(Player player, List<WorldEntity> entities)
{
return entities.Where(entity => {
bool isEligibleForSimulation = player.CanSee(entity) && ShouldSimulateEntity(entity);
return isEligibleForSimulation && simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE);
}).ToList();
}
public bool ShouldSimulateEntity(WorldEntity entity)
{
return simulationWhitelist.UtilityWhitelist.Contains(entity.TechType) || ShouldSimulateEntityMovement(entity);
}
public bool ShouldSimulateEntityMovement(WorldEntity entity)
{
return !entity.SpawnedByServer || simulationWhitelist.MovementWhitelist.Contains(entity.TechType);
}
public bool ShouldSimulateEntityMovement(NitroxId entityId)
{
return entityRegistry.TryGetEntityById(entityId, out WorldEntity worldEntity) && ShouldSimulateEntityMovement(worldEntity);
}
public void EntityDestroyed(NitroxId id)
{
simulationOwnershipData.RevokeOwnerOfId(id);
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using ProtoBufNet;
namespace NitroxServer.GameLogic.Entities;
[DataContract]
public class GlobalRootData
{
[DataMember(Order = 1)]
public List<GlobalRootEntity> Entities = new();
[ProtoAfterDeserialization]
private void ProtoAfterDeserialization()
{
foreach (GlobalRootEntity entity in Entities)
{
EnsureChildrenTransformAreParented(entity);
}
}
[OnDeserialized]
private void JsonAfterDeserialization(StreamingContext context)
{
ProtoAfterDeserialization();
}
private static void EnsureChildrenTransformAreParented(WorldEntity entity)
{
if (entity.Transform == null)
{
return;
}
foreach (Entity child in entity.ChildEntities)
{
if (child is WorldEntity childWE && childWE.Transform != null)
{
childWE.Transform.SetParent(entity.Transform, false);
EnsureChildrenTransformAreParented(childWE);
}
}
}
public static GlobalRootData From(List<GlobalRootEntity> entities)
{
return new GlobalRootData { Entities = entities };
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
namespace NitroxServer.GameLogic.Entities;
public interface ISimulationWhitelist
{
/// <summary>
/// We don't want to give out simulation to all entities that the server sent out because there is a lot of stationary items and junk (TechType.None).
/// It is easier to maintain a list of items we should simulate than try to blacklist items. This list should not be checked for non-server spawned items
/// as they were probably dropped by the player and are mostly guaranteed to move.
/// </summary>
HashSet<NitroxTechType> MovementWhitelist { get; }
/// <summary>
/// We differentiate the entities which should be simulated because of one of their behaviour (ie for utility)
/// from those are simulated for their movements.
/// </summary>
HashSet<NitroxTechType> UtilityWhitelist { get; }
}

View File

@@ -0,0 +1,7 @@
using System;
using System.Collections.Generic;
namespace NitroxServer.GameLogic.Entities;
[Serializable]
public record struct NitroxEntitySlot(string BiomeType, List<string> AllowedTypes);

View File

@@ -0,0 +1,458 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Unity;
using NitroxServer.GameLogic.Unlockables;
using NitroxServer.Helper;
using NitroxServer.Resources;
using NitroxServer.Serialization;
namespace NitroxServer.GameLogic.Entities.Spawning;
public class BatchEntitySpawner : IEntitySpawner
{
private readonly BatchCellsParser batchCellsParser;
private readonly HashSet<NitroxInt3> emptyBatches = [];
private readonly Dictionary<string, PrefabPlaceholdersGroupAsset> placeholdersGroupsByClassId;
private readonly RandomSpawnSpoofer randomSpawnSpoofer;
private readonly IUwePrefabFactory prefabFactory;
private readonly IEntityBootstrapperManager entityBootstrapperManager;
private readonly PDAStateData pdaStateData;
private readonly string seed;
private readonly IUweWorldEntityFactory worldEntityFactory;
private readonly Lock parsedBatchesLock = new();
private readonly Lock emptyBatchesLock = new();
private HashSet<NitroxInt3> parsedBatches;
public List<NitroxInt3> SerializableParsedBatches
{
get
{
List<NitroxInt3> parsed;
List<NitroxInt3> empty;
lock (parsedBatchesLock)
{
parsed = [.. parsedBatches];
}
lock (emptyBatchesLock)
{
empty = [.. emptyBatches];
}
return [.. parsed.Except(empty)];
}
set
{
lock (parsedBatchesLock)
{
parsedBatches = [.. value];
}
}
}
private static readonly NitroxQuaternion prefabZUpRotation = NitroxQuaternion.FromEuler(new(-90f, 0f, 0f));
public BatchEntitySpawner(
EntitySpawnPointFactory entitySpawnPointFactory,
IUweWorldEntityFactory worldEntityFactory,
IUwePrefabFactory prefabFactory,
List<NitroxInt3> loadedPreviousParsed,
ServerProtoBufSerializer serializer,
IEntityBootstrapperManager entityBootstrapperManager,
Dictionary<string, PrefabPlaceholdersGroupAsset> placeholdersGroupsByClassId,
PDAStateData pdaStateData,
RandomSpawnSpoofer randomSpawnSpoofer,
string seed
)
{
parsedBatches = [.. loadedPreviousParsed];
this.worldEntityFactory = worldEntityFactory;
this.prefabFactory = prefabFactory;
this.entityBootstrapperManager = entityBootstrapperManager;
this.placeholdersGroupsByClassId = placeholdersGroupsByClassId;
this.pdaStateData = pdaStateData;
batchCellsParser = new BatchCellsParser(entitySpawnPointFactory, serializer);
this.randomSpawnSpoofer = randomSpawnSpoofer;
this.seed = seed;
}
public bool IsBatchSpawned(NitroxInt3 batchId)
{
lock (parsedBatches)
{
return parsedBatches.Contains(batchId);
}
}
public List<Entity> LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCreation = false)
{
lock (parsedBatches)
{
if (parsedBatches.Contains(batchId))
{
return [];
}
parsedBatches.Add(batchId);
}
DeterministicGenerator deterministicBatchGenerator = new(seed, batchId);
List<EntitySpawnPoint> spawnPoints = batchCellsParser.ParseBatchData(batchId);
List<Entity> entities = SpawnEntities(spawnPoints, deterministicBatchGenerator);
if (entities.Count == 0)
{
lock (emptyBatchesLock)
{
emptyBatches.Add(batchId);
}
}
else if (!fullCacheCreation)
{
Log.Info($"Spawning {entities.Count} entities from {spawnPoints.Count} spawn points in batch {batchId}");
}
for (int x = 0; x < entities.Count; x++) // Throws on duplicate Entities already but nice to know which ones
{
for (int y = 0; y < entities.Count; y++)
{
if (entities[x] == entities[y] && x != y)
{
Log.Error($"Duplicate Entity detected! {entities[x]}");
}
}
}
return entities;
}
/// <inheritdoc cref="CreateEntityWithChildren" />
private IEnumerable<Entity> SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entitySpawnPoint, List<UwePrefab> prefabs, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null)
{
// See CSVEntitySpawner.GetPrefabForSlot for reference
List<UwePrefab> allowedPrefabs = FilterAllowedPrefabs(prefabs, entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability);
bool areFragmentProbabilitiesNonNull = fragmentProbability > 0f && completeFragmentProbability > 0f;
float probabilityMultiplier = areFragmentProbabilitiesNonNull ? (completeFragmentProbability + fragmentProbability) / fragmentProbability : 1f;
float weightedFragmentProbability = 0f;
for (int i = 0; i < allowedPrefabs.Count; i++)
{
UwePrefab prefab = allowedPrefabs[i];
if (areFragmentProbabilitiesNonNull && prefab.IsFragment)
{
prefab = prefab with { Probability = prefab.Probability * probabilityMultiplier };
allowedPrefabs[i] = prefab;
}
weightedFragmentProbability += prefab.Probability;
}
UwePrefab chosenPrefab = default;
if (weightedFragmentProbability > 0f)
{
float probabilityThreshold = XORRandom.NextFloat();
if (weightedFragmentProbability > 1f)
{
probabilityThreshold *= weightedFragmentProbability;
}
float currentProbability = 0f;
foreach (UwePrefab prefab in allowedPrefabs)
{
currentProbability += prefab.Probability;
if (currentProbability >= probabilityThreshold)
{
chosenPrefab = prefab;
break;
}
}
}
if (chosenPrefab.Count == 0)
{
yield break;
}
if (worldEntityFactory.TryFind(chosenPrefab.ClassId, out UweWorldEntity uweWorldEntity))
{
for (int i = 0; i < chosenPrefab.Count; i++)
{
// Random position in sphere is only possible after first spawn, see EntitySlot.Spawn
IEnumerable<Entity> entities = CreateEntityWithChildren(entitySpawnPoint,
chosenPrefab.ClassId,
uweWorldEntity.TechType,
uweWorldEntity.PrefabZUp,
uweWorldEntity.CellLevel,
uweWorldEntity.LocalScale,
deterministicBatchGenerator,
parentEntity,
i > 0);
foreach (Entity entity in entities)
{
yield return entity;
}
}
}
}
private List<UwePrefab> FilterAllowedPrefabs(List<UwePrefab> prefabs, EntitySpawnPoint entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability)
{
List<UwePrefab> allowedPrefabs = [];
fragmentProbability = 0;
completeFragmentProbability = 0;
for (int i = 0; i < prefabs.Count; i++)
{
UwePrefab prefab = prefabs[i];
// Adapted code from the while loop in CSVEntitySpawner.GetPrefabForSlot
if (prefab.ClassId != "None" && worldEntityFactory.TryFind(prefab.ClassId, out UweWorldEntity uweWorldEntity) &&
entitySpawnPoint.AllowedTypes.Contains(uweWorldEntity.SlotType))
{
float weightedProbability = prefab.Probability / entitySpawnPoint.Density;
if (weightedProbability > 0)
{
if (prefab.IsFragment)
{
if (pdaStateData.ScannerComplete.Contains(uweWorldEntity.TechType))
{
completeFragmentProbability += weightedProbability;
continue;
}
else
{
fragmentProbability += weightedProbability;
}
}
prefab = prefab with { Probability = weightedProbability };
allowedPrefabs.Add(prefab);
}
}
}
return allowedPrefabs;
}
/// <summary>
/// Spawns the regular (can be children of PrefabPlaceholdersGroup) which are always the same thus context independent.
/// </summary>
/// <inheritdoc cref="CreateEntityWithChildren" />
private IEnumerable<Entity> SpawnEntitiesStaticly(EntitySpawnPoint entitySpawnPoint, DeterministicGenerator deterministicBatchGenerator, WorldEntity parentEntity = null)
{
if (worldEntityFactory.TryFind(entitySpawnPoint.ClassId, out UweWorldEntity uweWorldEntity))
{
// prefabZUp should not be taken into account for statically spawned entities
IEnumerable<Entity> entities = CreateEntityWithChildren(entitySpawnPoint,
entitySpawnPoint.ClassId,
uweWorldEntity.TechType,
false,
uweWorldEntity.CellLevel,
entitySpawnPoint.Scale,
deterministicBatchGenerator,
parentEntity);
foreach (Entity entity in entities)
{
yield return entity;
}
}
}
/// <returns>The first entity is a <see cref="WorldEntity"/> and the following are its children</returns>
private IEnumerable<Entity> CreateEntityWithChildren(EntitySpawnPoint entitySpawnPoint, string classId, NitroxTechType techType, bool prefabZUp, int cellLevel, NitroxVector3 localScale, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null, bool randomPosition = false)
{
WorldEntity spawnedEntity;
NitroxVector3 position = entitySpawnPoint.LocalPosition;
NitroxQuaternion rotation = entitySpawnPoint.LocalRotation;
if (prefabZUp)
{
// See EntitySlot.SpawnVirtualEntities use of WorldEntityInfo.prefabZUp
rotation *= prefabZUpRotation;
}
if (randomPosition)
{
position += XORRandom.NextInsideSphere(4f);
}
if (classId == CellRootEntity.CLASS_ID)
{
spawnedEntity = new CellRootEntity(position,
rotation,
localScale,
techType,
cellLevel,
classId,
true,
deterministicBatchGenerator.NextId());
}
else
{
randomSpawnSpoofer.PickRandomClassIdIfRequired(ref classId);
spawnedEntity = new WorldEntity(position,
rotation,
localScale,
techType,
cellLevel,
classId,
true,
deterministicBatchGenerator.NextId(),
parentEntity);
}
// See EntitySlotsPlaceholder.Spawn
if (!TryCreatePrefabPlaceholdersGroupWithChildren(ref spawnedEntity, classId, deterministicBatchGenerator))
{
spawnedEntity.ChildEntities = SpawnEntities(entitySpawnPoint.Children, deterministicBatchGenerator, spawnedEntity);
}
entityBootstrapperManager.PrepareEntityIfRequired(ref spawnedEntity, deterministicBatchGenerator);
yield return spawnedEntity;
if (parentEntity == null) // Ensures children are only returned at the top level
{
// Children are yielded as well so they can be indexed at the top level (for use by simulation
// ownership and various other consumers). The parent should always be yielded before the children
foreach (Entity childEntity in AllChildren(spawnedEntity))
{
yield return childEntity;
}
}
}
private IEnumerable<Entity> AllChildren(Entity entity)
{
foreach (Entity child in entity.ChildEntities)
{
yield return child;
if (child.ChildEntities.Count > 0)
{
foreach (Entity childOfChild in AllChildren(child))
{
yield return childOfChild;
}
}
}
}
private List<Entity> SpawnEntities(List<EntitySpawnPoint> entitySpawnPoints, DeterministicGenerator deterministicBatchGenerator, WorldEntity parentEntity = null)
{
List<Entity> entities = [];
foreach (EntitySpawnPoint esp in entitySpawnPoints)
{
if (esp is SerializedEntitySpawnPoint serializedEsp)
{
// We add the cell's coordinate because this entity isn't parented so it needs to know about its global position
NitroxTransform transform = new(serializedEsp.LocalPosition + serializedEsp.AbsoluteEntityCell.Position, serializedEsp.LocalRotation, serializedEsp.Scale);
SerializedWorldEntity entity = new(serializedEsp.SerializedComponents, serializedEsp.Layer, transform, deterministicBatchGenerator.NextId(), parentEntity?.Id, serializedEsp.AbsoluteEntityCell);
entities.Add(entity);
continue;
}
if (esp.Density > 0)
{
if (prefabFactory.TryGetPossiblePrefabs(esp.BiomeType, out List<UwePrefab> prefabs) && prefabs.Count > 0)
{
entities.AddRange(SpawnEntitiesUsingRandomDistribution(esp, prefabs, deterministicBatchGenerator, parentEntity));
}
else if (!string.IsNullOrEmpty(esp.ClassId))
{
entities.AddRange(SpawnEntitiesStaticly(esp, deterministicBatchGenerator, parentEntity));
}
}
}
return entities;
}
/// <summary>
/// Check to see if this entity is a PrefabPlaceholderGroup.
/// If it is, we want to add the PrefabPlaceholders that would be spawned here.
/// This is suppressed on the client so we don't get virtual entities that the server doesn't know about.
/// </summary>
/// <returns>If this Entity is a PrefabPlaceholdersGroup</returns>
private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity, string classId, DeterministicGenerator deterministicBatchGenerator)
{
if (!placeholdersGroupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset groupAsset))
{
return false;
}
entity = new PlaceholderGroupWorldEntity(entity);
// Adapted from PrefabPlaceholdersGroup.Spawn
for (int i = 0; i < groupAsset.PrefabAssets.Length; i++)
{
// Fix positioning of children
IPrefabAsset prefabAsset = groupAsset.PrefabAssets[i];
// Two cases, either the PrefabPlaceholder holds a visible GameObject or an EntitySlot (a MB which has a chance of spawning a prefab)
if (prefabAsset is PrefabPlaceholderAsset placeholderAsset && placeholderAsset.EntitySlot.HasValue)
{
WorldEntity spawnedEntity = SpawnPrefabAssetInEntitySlot(placeholderAsset.Transform, placeholderAsset.EntitySlot.Value, deterministicBatchGenerator, entity.AbsoluteEntityCell, entity);
if (spawnedEntity != null)
{
// Spawned child will not be of the same type as the current prefabAsset
if (placeholdersGroupsByClassId.ContainsKey(spawnedEntity.ClassId))
{
spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i);
}
else
{
spawnedEntity = new PrefabPlaceholderEntity(spawnedEntity, i);
}
entity.ChildEntities.Add(spawnedEntity);
}
}
else
{
// Regular visible GameObject
string prefabClassId = prefabAsset.ClassId;
if (prefabAsset is PrefabPlaceholderRandomAsset randomAsset && randomAsset.ClassIds.Count > 0)
{
int randomIndex = XORRandom.NextIntRange(0, randomAsset.ClassIds.Count);
prefabClassId = randomAsset.ClassIds[randomIndex];
}
EntitySpawnPoint esp = new(entity.AbsoluteEntityCell, prefabAsset.Transform.LocalPosition, prefabAsset.Transform.LocalRotation, prefabAsset.Transform.LocalScale, prefabClassId);
WorldEntity spawnedEntity = (WorldEntity)SpawnEntitiesStaticly(esp, deterministicBatchGenerator, entity).First();
if (prefabAsset is PrefabPlaceholdersGroupAsset)
{
spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i);
}
else
{
spawnedEntity = new PrefabPlaceholderEntity(spawnedEntity, i);
}
entity.ChildEntities.Add(spawnedEntity);
}
}
return true;
}
private WorldEntity SpawnPrefabAssetInEntitySlot(NitroxTransform transform, NitroxEntitySlot entitySlot, DeterministicGenerator deterministicBatchGenerator, AbsoluteEntityCell cell, Entity parentEntity)
{
if (!prefabFactory.TryGetPossiblePrefabs(entitySlot.BiomeType, out List<UwePrefab> prefabs) || prefabs.Count == 0)
{
return null;
}
List<Entity> entities = [];
EntitySpawnPoint entitySpawnPoint = new(cell, transform.LocalPosition, transform.LocalRotation, entitySlot.AllowedTypes.ToList(), 1f, entitySlot.BiomeType);
entities.AddRange(SpawnEntitiesUsingRandomDistribution(entitySpawnPoint, prefabs, deterministicBatchGenerator, parentEntity));
if (entities.Count > 0)
{
return (WorldEntity)entities[0];
}
return null;
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Unity;
namespace NitroxServer.GameLogic.Entities.Spawning;
public class EntitySpawnPoint
{
// Fields from EntitySlotData
public string BiomeType { get; }
public List<string> AllowedTypes { get; }
public float Density { get; }
public NitroxVector3 LocalPosition { get; set; }
public NitroxQuaternion LocalRotation { get; set; }
public readonly List<EntitySpawnPoint> Children = new List<EntitySpawnPoint>();
public AbsoluteEntityCell AbsoluteEntityCell { get; }
public NitroxVector3 Scale { get; protected set; }
public string ClassId { get; }
public bool CanSpawnCreature { get; private set; }
public EntitySpawnPoint Parent { get; set; }
public EntitySpawnPoint(AbsoluteEntityCell absoluteEntityCell, NitroxVector3 localPosition, NitroxQuaternion localRotation, List<string> allowedTypes, float density, string biomeType)
{
AbsoluteEntityCell = absoluteEntityCell;
LocalPosition = localPosition;
LocalRotation = localRotation;
BiomeType = biomeType;
Density = density;
AllowedTypes = allowedTypes;
}
public EntitySpawnPoint(AbsoluteEntityCell absoluteEntityCell, NitroxVector3 localPosition, NitroxQuaternion localRotation, NitroxVector3 scale, string classId)
{
AbsoluteEntityCell = absoluteEntityCell;
ClassId = classId;
Density = 1;
LocalPosition = localPosition;
Scale = scale;
LocalRotation = localRotation;
}
public override string ToString() => $"[EntitySpawnPoint - {AbsoluteEntityCell}, Local Position: {LocalPosition}, Local Rotation: {LocalRotation}, Scale: {Scale}, Class Id: {ClassId}, Biome Type: {BiomeType}, Density: {Density}, Can Spawn Creature: {CanSpawnCreature}]";
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Unity;
using NitroxServer.UnityStubs;
namespace NitroxServer.GameLogic.Entities.Spawning;
public abstract class EntitySpawnPointFactory
{
public abstract List<EntitySpawnPoint> From(AbsoluteEntityCell absoluteEntityCell, NitroxTransform transform, GameObject gameObject);
}

View File

@@ -0,0 +1,10 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxServer.Helper;
namespace NitroxServer.GameLogic.Entities.Spawning;
public interface IEntityBootstrapper
{
public void Prepare(ref WorldEntity spawnedEntity, DeterministicGenerator generator);
}

View File

@@ -0,0 +1,9 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxServer.Helper;
namespace NitroxServer.GameLogic.Entities.Spawning;
public interface IEntityBootstrapperManager
{
public void PrepareEntityIfRequired(ref WorldEntity spawnedEntity, DeterministicGenerator generator);
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
namespace NitroxServer.GameLogic.Entities.Spawning
{
public interface IEntitySpawner
{
List<Entity> LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCreation = false);
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Unity;
namespace NitroxServer.GameLogic.Entities.Spawning;
/// <summary>
/// Specific type of <see cref="EntitySpawnPoint"/> for spawning <see cref="SerializedWorldEntity"/>
/// </summary>
public class SerializedEntitySpawnPoint : EntitySpawnPoint
{
public List<SerializedComponent> SerializedComponents { get; }
public int Layer { get; }
public SerializedEntitySpawnPoint(List<SerializedComponent> serializedComponents, int layer, AbsoluteEntityCell absoluteEntityCell, NitroxTransform transform) : base(absoluteEntityCell, transform.LocalPosition, transform.LocalRotation, null, 1, null)
{
SerializedComponents = serializedComponents;
Layer = layer;
Scale = transform.LocalScale;
}
}

View File

@@ -0,0 +1,381 @@
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;
}
}