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,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;
}
}