first commit
This commit is contained in:
77
NitroxServer/GameLogic/Entities/EntityData.cs
Normal file
77
NitroxServer/GameLogic/Entities/EntityData.cs
Normal 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 };
|
||||
}
|
||||
}
|
246
NitroxServer/GameLogic/Entities/EntityRegistry.cs
Normal file
246
NitroxServer/GameLogic/Entities/EntityRegistry.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
162
NitroxServer/GameLogic/Entities/EntitySimulation.cs
Normal file
162
NitroxServer/GameLogic/Entities/EntitySimulation.cs
Normal 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);
|
||||
}
|
||||
}
|
50
NitroxServer/GameLogic/Entities/GlobalRootData.cs
Normal file
50
NitroxServer/GameLogic/Entities/GlobalRootData.cs
Normal 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 };
|
||||
}
|
||||
}
|
20
NitroxServer/GameLogic/Entities/ISimulationWhitelist.cs
Normal file
20
NitroxServer/GameLogic/Entities/ISimulationWhitelist.cs
Normal 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; }
|
||||
}
|
7
NitroxServer/GameLogic/Entities/NitroxEntitySlot.cs
Normal file
7
NitroxServer/GameLogic/Entities/NitroxEntitySlot.cs
Normal 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);
|
458
NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs
Normal file
458
NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs
Normal 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;
|
||||
}
|
||||
}
|
44
NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPoint.cs
Normal file
44
NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPoint.cs
Normal 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}]";
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
11
NitroxServer/GameLogic/Entities/Spawning/IEntitySpawner.cs
Normal file
11
NitroxServer/GameLogic/Entities/Spawning/IEntitySpawner.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
381
NitroxServer/GameLogic/Entities/WorldEntityManager.cs
Normal file
381
NitroxServer/GameLogic/Entities/WorldEntityManager.cs
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user