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 emptyBatches = []; private readonly Dictionary 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 parsedBatches; public List SerializableParsedBatches { get { List parsed; List 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 loadedPreviousParsed, ServerProtoBufSerializer serializer, IEntityBootstrapperManager entityBootstrapperManager, Dictionary 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 LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCreation = false) { lock (parsedBatches) { if (parsedBatches.Contains(batchId)) { return []; } parsedBatches.Add(batchId); } DeterministicGenerator deterministicBatchGenerator = new(seed, batchId); List spawnPoints = batchCellsParser.ParseBatchData(batchId); List 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; } /// private IEnumerable SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entitySpawnPoint, List prefabs, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null) { // See CSVEntitySpawner.GetPrefabForSlot for reference List 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 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 FilterAllowedPrefabs(List prefabs, EntitySpawnPoint entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability) { List 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; } /// /// Spawns the regular (can be children of PrefabPlaceholdersGroup) which are always the same thus context independent. /// /// private IEnumerable 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 entities = CreateEntityWithChildren(entitySpawnPoint, entitySpawnPoint.ClassId, uweWorldEntity.TechType, false, uweWorldEntity.CellLevel, entitySpawnPoint.Scale, deterministicBatchGenerator, parentEntity); foreach (Entity entity in entities) { yield return entity; } } } /// The first entity is a and the following are its children private IEnumerable 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 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 SpawnEntities(List entitySpawnPoints, DeterministicGenerator deterministicBatchGenerator, WorldEntity parentEntity = null) { List 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 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; } /// /// 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. /// /// If this Entity is a PrefabPlaceholdersGroup 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 prefabs) || prefabs.Count == 0) { return null; } List 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; } }