using System; using System.Collections; using System.Collections.Generic; using System.Linq; using NitroxClient.Communication; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.Spawning; using NitroxClient.GameLogic.Spawning.Abstract; using NitroxClient.GameLogic.Spawning.Bases; using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.GameLogic.Entities.Bases; using NitroxModel.DataStructures.GameLogic.Entities.Metadata; using NitroxModel.DataStructures.Util; using NitroxModel.Packets; using UnityEngine; using UWE; namespace NitroxClient.GameLogic { public class Entities { private readonly IPacketSender packetSender; private readonly ThrottledPacketSender throttledPacketSender; private readonly EntityMetadataManager entityMetadataManager; private readonly SimulationOwnership simulationOwnership; private readonly Dictionary spawnedAsType = new(); private readonly Dictionary> pendingParentEntitiesByParentId = new Dictionary>(); private readonly Dictionary entitySpawnersByType = new Dictionary(); public List EntitiesToSpawn { get; private init; } private bool spawningEntities; private readonly HashSet deletedEntitiesIds = new(); private readonly List pendingSimulatedEntities = new(); public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacketSender, EntityMetadataManager entityMetadataManager, PlayerManager playerManager, LocalPlayer localPlayer, LiveMixinManager liveMixinManager, TimeManager timeManager, SimulationOwnership simulationOwnership) { this.packetSender = packetSender; this.throttledPacketSender = throttledPacketSender; this.entityMetadataManager = entityMetadataManager; this.simulationOwnership = simulationOwnership; EntitiesToSpawn = new(); entitySpawnersByType[typeof(PrefabChildEntity)] = new PrefabChildEntitySpawner(); entitySpawnersByType[typeof(PathBasedChildEntity)] = new PathBasedChildEntitySpawner(); entitySpawnersByType[typeof(InstalledModuleEntity)] = new InstalledModuleEntitySpawner(); entitySpawnersByType[typeof(InstalledBatteryEntity)] = new InstalledBatteryEntitySpawner(); entitySpawnersByType[typeof(InventoryEntity)] = new InventoryEntitySpawner(); entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(entityMetadataManager); entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, playerManager, localPlayer, this, simulationOwnership); entitySpawnersByType[typeof(PlaceholderGroupWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(PrefabPlaceholderEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(EscapePodWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(PlayerWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(VehicleWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(SerializedWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(GlobalRootEntity)] = new GlobalRootEntitySpawner(); entitySpawnersByType[typeof(BaseLeakEntity)] = new BaseLeakEntitySpawner(liveMixinManager); entitySpawnersByType[typeof(BuildEntity)] = new BuildEntitySpawner(this, (BaseLeakEntitySpawner)entitySpawnersByType[typeof(BaseLeakEntity)]); entitySpawnersByType[typeof(RadiationLeakEntity)] = new RadiationLeakEntitySpawner(timeManager); entitySpawnersByType[typeof(ModuleEntity)] = new ModuleEntitySpawner(this); entitySpawnersByType[typeof(GhostEntity)] = new GhostEntitySpawner(); entitySpawnersByType[typeof(OxygenPipeEntity)] = new OxygenPipeEntitySpawner(this, (WorldEntitySpawner)entitySpawnersByType[typeof(WorldEntity)]); entitySpawnersByType[typeof(PlacedWorldEntity)] = new PlacedWorldEntitySpawner((WorldEntitySpawner)entitySpawnersByType[typeof(WorldEntity)]); entitySpawnersByType[typeof(InteriorPieceEntity)] = new InteriorPieceEntitySpawner(this, entityMetadataManager); entitySpawnersByType[typeof(GeyserWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(ReefbackEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(ReefbackChildEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(CreatureRespawnEntity)] = entitySpawnersByType[typeof(WorldEntity)]; } public void EntityMetadataChanged(object o, NitroxId id) { Optional metadata = entityMetadataManager.Extract(o); if (metadata.HasValue) { BroadcastMetadataUpdate(id, metadata.Value); } } public void EntityMetadataChangedThrottled(object o, NitroxId id, float throttleTime = 0.2f) { // As throttled broadcasting is done after some time by a different function, this is where the packet sending should be interrupted if (PacketSuppressor.IsSuppressed) { return; } Optional metadata = entityMetadataManager.Extract(o); if (metadata.HasValue) { BroadcastMetadataUpdateThrottled(id, metadata.Value, throttleTime); } } public void BroadcastMetadataUpdate(NitroxId id, EntityMetadata metadata) { packetSender.Send(new EntityMetadataUpdate(id, metadata)); } public void BroadcastMetadataUpdateThrottled(NitroxId id, EntityMetadata metadata, float throttleTime = 0.2f) { throttledPacketSender.SendThrottled(new EntityMetadataUpdate(id, metadata), packet => packet.Id, throttleTime); } public void BroadcastEntitySpawnedByClient(Entity entity, bool requireRespawn = false) { packetSender.Send(new EntitySpawnedByClient(entity, requireRespawn)); } private IEnumerator SpawnNewEntities() { bool restarted = false; yield return SpawnBatchAsync(EntitiesToSpawn).OnYieldError(exception => { Log.Error(exception); if (EntitiesToSpawn.Count > 0) { restarted = true; // It's safe to run a new time because the processed entity is removed first so it won't infinitely throw errors CoroutineHost.StartCoroutine(SpawnNewEntities()); } }); spawningEntities = restarted; if (!spawningEntities) { entityMetadataManager.ClearNewerMetadata(); deletedEntitiesIds.Clear(); simulationOwnership.ClearNewerSimulations(); } } public void EnqueueEntitiesToSpawn(List entitiesToEnqueue) { EntitiesToSpawn.InsertRange(0, entitiesToEnqueue); if (!spawningEntities) { spawningEntities = true; CoroutineHost.StartCoroutine(SpawnNewEntities()); } } /// /// Yield returning takes too much time (at least once per IEnumerator branch) and it quickly gets out of hand with long function call hierarchies so /// we want to reduce the amount of yield operations and only skip to the next frame when required (to maintain the FPS). /// Also saves resources by using the IOut instances /// /// Should children be spawned even if already marked as spawned public IEnumerator SpawnBatchAsync(List batch, bool forceRespawn = false, bool skipFrames = true) { // we divide the FPS by 2.5 because we consider (time for 1 frame + spawning time without a frame + extra computing time) float allottedTimePerFrame = 0.4f / Application.targetFrameRate; float timeLimit = Time.realtimeSinceStartup + allottedTimePerFrame; TaskResult> entityResult = new(); TaskResult exception = new(); while (batch.Count > 0) { entityResult.Set(Optional.Empty); exception.Set(null); Entity entity = batch[^1]; batch.RemoveAt(batch.Count - 1); // Preconditions which may get the spawn process cancelled or postponed if (deletedEntitiesIds.Remove(entity.Id)) { continue; } if (WasAlreadySpawned(entity) && !forceRespawn) { UpdateEntity(entity); continue; } else if (entity.ParentId != null && !IsParentReady(entity.ParentId)) { AddPendingParentEntity(entity); continue; } // Executing the spawn instructions whether they're sync or async IEntitySpawner entitySpawner = entitySpawnersByType[entity.GetType()]; if (entitySpawner is not ISyncEntitySpawner syncEntitySpawner || (!syncEntitySpawner.SpawnSyncSafe(entity, entityResult, exception) && exception.Get() == null)) { IEnumerator coroutine = entitySpawner.SpawnAsync(entity, entityResult); if (coroutine != null) { yield return coroutine.OnYieldError(Log.Error); } } // Any error in there would make spawning children useless if (exception.Get() != null) { Log.Error(exception.Get()); continue; } else if (!entityResult.Get().Value) { continue; } entityMetadataManager.ApplyMetadata(entityResult.Get().Value, entity.Metadata); simulationOwnership.ApplyNewerSimulation(entity.Id); MarkAsSpawned(entity); // Finding out about all children (can be hidden in the object's hierarchy or in a pending list) if (!entitySpawner.SpawnsOwnChildren(entity)) { batch.AddRange(entity.ChildEntities); List childrenIds = entity.ChildEntities.Select(entity => entity.Id).ToList(); if (pendingParentEntitiesByParentId.TryGetValue(entity.Id, out List pendingEntities)) { IEnumerable childrenToAdd = pendingEntities.Where(e => !childrenIds.Contains(e.Id)); batch.AddRange(childrenToAdd); pendingParentEntitiesByParentId.Remove(entity.Id); } } // Skip a frame to maintain FPS if (Time.realtimeSinceStartup >= timeLimit && skipFrames) { yield return new WaitForEndOfFrame(); timeLimit = Time.realtimeSinceStartup + allottedTimePerFrame; } } } public IEnumerator SpawnEntityAsync(Entity entity, bool forceRespawn = false, bool skipFrames = false) { return SpawnBatchAsync(new() { entity }, forceRespawn, skipFrames); } public void CleanupExistingEntities(List dirtyEntities) { foreach (Entity entity in dirtyEntities) { RemoveEntityHierarchy(entity); Optional gameObject = NitroxEntity.GetObjectFrom(entity.Id); if (gameObject.HasValue) { DestroyObject(gameObject.Value); } } } /// /// Either perform a special operation (e.g. for plants) or a simple /// public static void DestroyObject(GameObject gameObject) { if (gameObject.TryGetComponent(out Plantable plantable)) { plantable.FreeSpot(); return; } if (gameObject.TryGetComponent(out GrownPlant grownPlant)) { grownPlant.seed.AliveOrNull()?.FreeSpot(); return; } UnityEngine.Object.Destroy(gameObject); } private void UpdateEntity(Entity entity) { if (!NitroxEntity.TryGetObjectFrom(entity.Id, out GameObject gameObject)) { #if DEBUG && ENTITY_LOG Log.Error($"Entity was already spawned but not found(is it in another chunk?) NitroxId: {entity.Id} TechType: {entity.TechType} ClassId: {entity.ClassId} Transform: {entity.Transform}"); #endif return; } entityMetadataManager.ApplyMetadata(gameObject, entity.Metadata); } private void AddPendingParentEntity(Entity entity) { if (!pendingParentEntitiesByParentId.TryGetValue(entity.ParentId, out List pendingEntities)) { pendingEntities = new List(); pendingParentEntitiesByParentId[entity.ParentId] = pendingEntities; } pendingEntities.Add(entity); } // Entites can sometimes be spawned as one thing but need to be respawned later as another. For example, a flare // spawned inside an Inventory as an InventoryItemEntity can later be dropped in the world as a WorldEntity. Another // example would be a base ghost that needs to be respawned a completed piece. public bool WasAlreadySpawned(Entity entity) { if (spawnedAsType.TryGetValue(entity.Id, out Type type)) { return type == entity.GetType() && NitroxEntity.TryGetObjectFrom(entity.Id, out _); } return false; } public bool IsKnownEntity(NitroxId id) { return spawnedAsType.ContainsKey(id); } public Type RequireEntityType(NitroxId id) { if (spawnedAsType.TryGetValue(id, out Type type)) { return type; } throw new InvalidOperationException($"Did not have a type for {id}"); } public bool IsParentReady(NitroxId id) { return WasParentSpawned(id) || NitroxEntity.TryGetObjectFrom(id, out GameObject _); } public bool WasParentSpawned(NitroxId id) { return spawnedAsType.ContainsKey(id); } public void MarkAsSpawned(Entity entity) { spawnedAsType[entity.Id] = entity.GetType(); } public void RemoveEntity(NitroxId id) => spawnedAsType.Remove(id); public void MarkForDeletion(NitroxId id) { deletedEntitiesIds.Add(id); } /// /// Allows the ability to respawn an entity and its entire hierarchy. Callers are responsible for ensuring the /// entity is no longer in the world. /// public void RemoveEntityHierarchy(Entity entity) { RemoveEntity(entity.Id); foreach (Entity child in entity.ChildEntities) { RemoveEntityHierarchy(child); } } } }