Files
Nitrox/NitroxClient/GameLogic/Entities.cs
2025-07-06 00:23:46 +02:00

371 lines
16 KiB
C#

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<NitroxId, Type> spawnedAsType = new();
private readonly Dictionary<NitroxId, List<Entity>> pendingParentEntitiesByParentId = new Dictionary<NitroxId, List<Entity>>();
private readonly Dictionary<Type, IEntitySpawner> entitySpawnersByType = new Dictionary<Type, IEntitySpawner>();
public List<Entity> EntitiesToSpawn { get; private init; }
private bool spawningEntities;
private readonly HashSet<NitroxId> deletedEntitiesIds = new();
private readonly List<SimulatedEntity> 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<EntityMetadata> 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<EntityMetadataUpdate>.IsSuppressed)
{
return;
}
Optional<EntityMetadata> 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<Entity> entitiesToEnqueue)
{
EntitiesToSpawn.InsertRange(0, entitiesToEnqueue);
if (!spawningEntities)
{
spawningEntities = true;
CoroutineHost.StartCoroutine(SpawnNewEntities());
}
}
/// <remarks>
/// 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
/// </remarks>
/// <param name="forceRespawn">Should children be spawned even if already marked as spawned</param>
public IEnumerator SpawnBatchAsync(List<Entity> 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<Optional<GameObject>> entityResult = new();
TaskResult<Exception> 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<NitroxId> childrenIds = entity.ChildEntities.Select(entity => entity.Id).ToList();
if (pendingParentEntitiesByParentId.TryGetValue(entity.Id, out List<Entity> pendingEntities))
{
IEnumerable<Entity> 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<Entity> dirtyEntities)
{
foreach (Entity entity in dirtyEntities)
{
RemoveEntityHierarchy(entity);
Optional<GameObject> gameObject = NitroxEntity.GetObjectFrom(entity.Id);
if (gameObject.HasValue)
{
DestroyObject(gameObject.Value);
}
}
}
/// <summary>
/// Either perform a special operation (e.g. for plants) or a simple <see cref="UnityEngine.Object.Destroy"/>
/// </summary>
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<Entity> pendingEntities))
{
pendingEntities = new List<Entity>();
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);
}
/// <summary>
/// Allows the ability to respawn an entity and its entire hierarchy. Callers are responsible for ensuring the
/// entity is no longer in the world.
/// </summary>
public void RemoveEntityHierarchy(Entity entity)
{
RemoveEntity(entity.Id);
foreach (Entity child in entity.ChildEntities)
{
RemoveEntityHierarchy(child);
}
}
}
}