From 10d1535dd420917a72f742912c79ca6e640038f7 Mon Sep 17 00:00:00 2001 From: tornac1234 <24827220+tornac1234@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:19:59 +0100 Subject: [PATCH] Change world loading to only spawn entities in view range, and refactored of WorldEntityManager --- NitroxClient/Debuggers/NetworkDebugger.cs | 2 +- NitroxClient/GameLogic/Terrain.cs | 165 +++--- NitroxModel/Packets/BatchVisibilityChanged.cs | 19 - ...rldStreamer_LoadBatchTask_Execute_Patch.cs | 15 - .../LargeWorldStreamer_UnloadBatch_Patch.cs | 15 - .../BatchVisibilityChangedProcessor.cs | 43 -- .../CellVisibilityChangedProcessor.cs | 46 +- .../EntitySpawnedByClientProcessor.cs | 3 +- .../GameLogic/Bases/BuildingManager.cs | 18 +- .../GameLogic/Entities/EntitySimulation.cs | 47 +- .../GameLogic/Entities/WorldEntityManager.cs | 512 +++++++++--------- 11 files changed, 370 insertions(+), 515 deletions(-) delete mode 100644 NitroxModel/Packets/BatchVisibilityChanged.cs delete mode 100644 NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_LoadBatchTask_Execute_Patch.cs delete mode 100644 NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_UnloadBatch_Patch.cs delete mode 100644 NitroxServer/Communication/Packets/Processors/BatchVisibilityChangedProcessor.cs diff --git a/NitroxClient/Debuggers/NetworkDebugger.cs b/NitroxClient/Debuggers/NetworkDebugger.cs index 11a276a384..1b3dc9084d 100644 --- a/NitroxClient/Debuggers/NetworkDebugger.cs +++ b/NitroxClient/Debuggers/NetworkDebugger.cs @@ -19,7 +19,7 @@ public class NetworkDebugger : BaseDebugger, INetworkDebugger { nameof(PlayerMovement), nameof(EntityTransformUpdates), nameof(PlayerStats), nameof(SpawnEntities), nameof(VehicleMovement), nameof(PlayerCinematicControllerCall), nameof(PlayFMODAsset), nameof(PlayFMODCustomEmitter), nameof(PlayFMODStudioEmitter), nameof(PlayFMODCustomLoopingEmitter), nameof(SimulationOwnershipChange), - nameof(CellVisibilityChanged), nameof(BatchVisibilityChanged) + nameof(CellVisibilityChanged) }; private readonly List packets = new List(PACKET_STORED_COUNT); diff --git a/NitroxClient/GameLogic/Terrain.cs b/NitroxClient/GameLogic/Terrain.cs index 6366eca604..cce026d672 100644 --- a/NitroxClient/GameLogic/Terrain.cs +++ b/NitroxClient/GameLogic/Terrain.cs @@ -2,131 +2,96 @@ using System.Collections.Generic; using NitroxClient.Communication.Abstract; using NitroxClient.Map; -using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.Packets; using NitroxModel_Subnautica.DataStructures; using UnityEngine; using WorldStreaming; -namespace NitroxClient.GameLogic +namespace NitroxClient.GameLogic; + +public class Terrain { - public class Terrain + private readonly IMultiplayerSession multiplayerSession; + private readonly IPacketSender packetSender; + private readonly VisibleCells visibleCells; + private readonly VisibleBatches visibleBatches; + + private readonly List addedCells = []; + private readonly List removedCells = []; + + private bool cellsPendingSync; + private bool batchesPendingSync; + private float bufferedTime = 0f; + private const float TIME_BUFFER = 0.05f; + + public Terrain(IMultiplayerSession multiplayerSession, IPacketSender packetSender, VisibleCells visibleCells, VisibleBatches visibleBatches) { - private readonly IMultiplayerSession multiplayerSession; - private readonly IPacketSender packetSender; - private readonly VisibleCells visibleCells; - private readonly VisibleBatches visibleBatches; - - private bool cellsPendingSync; - private bool batchesPendingSync; - private float bufferedTime = 0f; - private float timeBuffer = 0.05f; - - private List addedCells = new List(); - private List removedCells = new List(); - private List addedBatches = new List(); - private List removedBatches = new List(); + this.multiplayerSession = multiplayerSession; + this.packetSender = packetSender; + this.visibleCells = visibleCells; + this.visibleBatches = visibleBatches; + } - public Terrain(IMultiplayerSession multiplayerSession, IPacketSender packetSender, VisibleCells visibleCells, VisibleBatches visibleBatches) - { - this.multiplayerSession = multiplayerSession; - this.packetSender = packetSender; - this.visibleCells = visibleCells; - this.visibleBatches = visibleBatches; - } + public void CellLoaded(Int3 batchId, Int3 cellId, int level) + { + AbsoluteEntityCell cell = new(batchId.ToDto(), cellId.ToDto(), level); - public void CellLoaded(Int3 batchId, Int3 cellId, int level) + if (!visibleCells.Contains(cell)) { - AbsoluteEntityCell cell = new AbsoluteEntityCell(batchId.ToDto(), cellId.ToDto(), level); - - if (!visibleCells.Contains(cell)) - { - visibleCells.Add(cell); - addedCells.Add(cell); - cellsPendingSync = true; - } + visibleCells.Add(cell); + addedCells.Add(cell); + cellsPendingSync = true; } + } - public void CellUnloaded(Int3 batchId, Int3 cellId, int level) - { - AbsoluteEntityCell cell = new AbsoluteEntityCell(batchId.ToDto(), cellId.ToDto(), level); - - if (visibleCells.Contains(cell)) - { - visibleCells.Remove(cell); - removedCells.Add(cell); - cellsPendingSync = true; - } - } - public void BatchLoaded(Int3 batchId) - { - NitroxInt3 nitroxBatchId = batchId.ToDto(); - if (!visibleBatches.Contains(nitroxBatchId)) - { - visibleBatches.Add(nitroxBatchId); - addedBatches.Add(nitroxBatchId); - batchesPendingSync = true; - } - } + public void CellUnloaded(Int3 batchId, Int3 cellId, int level) + { + AbsoluteEntityCell cell = new(batchId.ToDto(), cellId.ToDto(), level); - public void BatchUnloaded(Int3 batchId) + if (visibleCells.Contains(cell)) { - NitroxInt3 nitroxBatchId = batchId.ToDto(); - if (visibleBatches.Contains(nitroxBatchId)) - { - visibleBatches.Remove(nitroxBatchId); - removedBatches.Add(nitroxBatchId); - batchesPendingSync = true; - } + visibleCells.Remove(cell); + removedCells.Add(cell); + cellsPendingSync = true; } + } - public void UpdateVisibility() + public void UpdateVisibility() + { + bufferedTime += Time.deltaTime; + if (bufferedTime >= TIME_BUFFER) { - bufferedTime += Time.deltaTime; - if (bufferedTime > timeBuffer) + if (cellsPendingSync) { - if (cellsPendingSync) - { - CellVisibilityChanged cellsChanged = new CellVisibilityChanged(multiplayerSession.Reservation.PlayerId, addedCells.ToArray(), removedCells.ToArray()); - packetSender.Send(cellsChanged); - - addedCells.Clear(); - removedCells.Clear(); - - cellsPendingSync = false; - } - - if (batchesPendingSync) - { - BatchVisibilityChanged batchesChanged = new BatchVisibilityChanged(multiplayerSession.Reservation.PlayerId, addedBatches.ToArray(), removedBatches.ToArray()); - packetSender.Send(batchesChanged); + CellVisibilityChanged cellsChanged = new(multiplayerSession.Reservation.PlayerId, addedCells.ToArray(), removedCells.ToArray()); + packetSender.Send(cellsChanged); - addedBatches.Clear(); - removedBatches.Clear(); + addedCells.Clear(); + removedCells.Clear(); - batchesPendingSync = false; - } - bufferedTime = 0f; + cellsPendingSync = false; } + bufferedTime = 0f; } - /// - /// Forces world streamer's to load the terrain around the MainCamera and waits until it's done to unfreeze the player. - /// - public static IEnumerator WaitForWorldLoad() - { - // In WorldStreamer.CreateStreamers() three coroutines are created to constantly call UpdateCenter() on the streamers - // We force these updates so that the world streamer gets busy instantly - WorldStreamer streamerV2 = LargeWorldStreamer.main.streamerV2; - streamerV2.UpdateStreamingCenter(MainCamera.camera.transform.position); - streamerV2.octreesStreamer.UpdateCenter(streamerV2.streamingCenter); - streamerV2.lowDetailOctreesStreamer.UpdateCenter(streamerV2.streamingCenter); - streamerV2.clipmapStreamer.UpdateCenter(streamerV2.streamingCenter); + } - yield return new WaitUntil(() => LargeWorldStreamer.main.IsWorldSettled()); - Player.main.cinematicModeActive = false; - } + /// + /// Forces world streamer's to load the terrain around the MainCamera and waits until it's done to unfreeze the player. + /// + public static IEnumerator WaitForWorldLoad() + { + // In WorldStreamer.CreateStreamers() three coroutines are created to constantly call UpdateCenter() on the streamers + // We force these updates so that the world streamer gets busy instantly + WorldStreamer streamerV2 = LargeWorldStreamer.main.streamerV2; + streamerV2.UpdateStreamingCenter(MainCamera.camera.transform.position); + streamerV2.octreesStreamer.UpdateCenter(streamerV2.streamingCenter); + streamerV2.lowDetailOctreesStreamer.UpdateCenter(streamerV2.streamingCenter); + streamerV2.clipmapStreamer.UpdateCenter(streamerV2.streamingCenter); + + yield return new WaitUntil(() => LargeWorldStreamer.main.IsWorldSettled()); + Player.main.cinematicModeActive = false; } } diff --git a/NitroxModel/Packets/BatchVisibilityChanged.cs b/NitroxModel/Packets/BatchVisibilityChanged.cs deleted file mode 100644 index 946a7e1473..0000000000 --- a/NitroxModel/Packets/BatchVisibilityChanged.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using NitroxModel.DataStructures; - -namespace NitroxModel.Packets; - -[Serializable] -public class BatchVisibilityChanged : Packet -{ - public ushort PlayerId { get; } - public NitroxInt3[] Added { get; } - public NitroxInt3[] Removed { get; } - - public BatchVisibilityChanged(ushort playerId, NitroxInt3[] added, NitroxInt3[] removed) - { - PlayerId = playerId; - Added = added; - Removed = removed; - } -} diff --git a/NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_LoadBatchTask_Execute_Patch.cs b/NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_LoadBatchTask_Execute_Patch.cs deleted file mode 100644 index b23ac8b1c3..0000000000 --- a/NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_LoadBatchTask_Execute_Patch.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Reflection; -using NitroxClient.GameLogic; -using NitroxModel.Helper; - -namespace NitroxPatcher.Patches.Dynamic; - -public sealed partial class LargeWorldStreamer_LoadBatchTask_Execute_Patch : NitroxPatch, IDynamicPatch -{ - public static readonly MethodInfo TARGET_METHOD = Reflect.Method((LargeWorldStreamer.LoadBatchTask t) => t.Execute()); - - public static void Prefix(BatchCells ___batchCells) - { - Resolve().BatchLoaded(___batchCells.batch); - } -} diff --git a/NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_UnloadBatch_Patch.cs b/NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_UnloadBatch_Patch.cs deleted file mode 100644 index 19f51b820e..0000000000 --- a/NitroxPatcher/Patches/Dynamic/LargeWorldStreamer_UnloadBatch_Patch.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Reflection; -using NitroxClient.GameLogic; -using NitroxModel.Helper; - -namespace NitroxPatcher.Patches.Dynamic; - -public sealed partial class LargeWorldStreamer_UnloadBatch_Patch : NitroxPatch, IDynamicPatch -{ - public static readonly MethodInfo TARGET_METHOD = Reflect.Method((LargeWorldStreamer t) => t.UnloadBatch(default(Int3))); - - public static void Prefix(Int3 index) - { - Resolve().BatchUnloaded(index); - } -} diff --git a/NitroxServer/Communication/Packets/Processors/BatchVisibilityChangedProcessor.cs b/NitroxServer/Communication/Packets/Processors/BatchVisibilityChangedProcessor.cs deleted file mode 100644 index e15a7b9662..0000000000 --- a/NitroxServer/Communication/Packets/Processors/BatchVisibilityChangedProcessor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NitroxModel.DataStructures; -using NitroxModel.DataStructures.GameLogic; -using NitroxModel.DataStructures.GameLogic.Entities; -using NitroxModel.Packets; -using NitroxServer.Communication.Packets.Processors.Abstract; -using NitroxServer.GameLogic.Entities; - -namespace NitroxServer.Communication.Packets.Processors; - -public class BatchVisibilityChangedProcessor : AuthenticatedPacketProcessor -{ - private readonly EntitySimulation entitySimulation; - private readonly WorldEntityManager worldEntityManager; - - public BatchVisibilityChangedProcessor(EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) - { - this.entitySimulation = entitySimulation; - this.worldEntityManager = worldEntityManager; - } - - public override void Process(BatchVisibilityChanged packet, Player player) - { - foreach (NitroxInt3 batchId in packet.Added) - { - int count = worldEntityManager.LoadUnspawnedEntities(batchId, false); - - if (count > 0) - { - entitySimulation.BroadcastSimulationChangesForBatchAddition(player, batchId); - } - - List entitiesInBatch = worldEntityManager.GetEntities(batchId); - - if (entitiesInBatch.Count > 0) - { - SpawnEntities batchEntities = new(entitiesInBatch.Cast().ToList()); - player.SendPacket(batchEntities); - } - } - } -} diff --git a/NitroxServer/Communication/Packets/Processors/CellVisibilityChangedProcessor.cs b/NitroxServer/Communication/Packets/Processors/CellVisibilityChangedProcessor.cs index 07792185c0..f986bed9a9 100644 --- a/NitroxServer/Communication/Packets/Processors/CellVisibilityChangedProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/CellVisibilityChangedProcessor.cs @@ -1,24 +1,52 @@ +using System.Collections.Generic; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic; using NitroxModel.Packets; using NitroxServer.Communication.Packets.Processors.Abstract; using NitroxServer.GameLogic.Entities; -namespace NitroxServer.Communication.Packets.Processors +namespace NitroxServer.Communication.Packets.Processors; + +public class CellVisibilityChangedProcessor : AuthenticatedPacketProcessor { - class CellVisibilityChangedProcessor : AuthenticatedPacketProcessor + private readonly EntitySimulation entitySimulation; + private readonly WorldEntityManager worldEntityManager; + + public CellVisibilityChangedProcessor(EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) + { + this.entitySimulation = entitySimulation; + this.worldEntityManager = worldEntityManager; + } + + public override void Process(CellVisibilityChanged packet, Player player) { - private readonly EntitySimulation entitySimulation; + player.AddCells(packet.Added); + player.RemoveCells(packet.Removed); + + List totalEntities = []; + List totalSimulationChanges = []; - public CellVisibilityChangedProcessor(EntitySimulation entitySimulation) + foreach (AbsoluteEntityCell addedCell in packet.Added) { - this.entitySimulation = entitySimulation; + worldEntityManager.LoadUnspawnedEntities(addedCell.BatchId, false); + + totalSimulationChanges.AddRange(entitySimulation.GetSimulationChangesForCell(player, addedCell)); + totalEntities.AddRange(worldEntityManager.GetEntities(addedCell)); } - public override void Process(CellVisibilityChanged packet, Player player) + foreach (AbsoluteEntityCell removedCell in packet.Removed) { - player.AddCells(packet.Added); - player.RemoveCells(packet.Removed); + entitySimulation.FillWithRemovedCells(player, removedCell, totalSimulationChanges); + } - entitySimulation.BroadcastSimulationChangesForCellUpdates(player, packet.Added, packet.Removed); + if (totalEntities.Count > 0) + { + SpawnEntities batchEntities = new(totalEntities); + player.SendPacket(batchEntities); + } + if (totalSimulationChanges.Count > 0) + { + entitySimulation.BroadcastSimulationChanges(new(totalSimulationChanges)); } } } diff --git a/NitroxServer/Communication/Packets/Processors/EntitySpawnedByClientProcessor.cs b/NitroxServer/Communication/Packets/Processors/EntitySpawnedByClientProcessor.cs index debe18d96f..4cb38765dd 100644 --- a/NitroxServer/Communication/Packets/Processors/EntitySpawnedByClientProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/EntitySpawnedByClientProcessor.cs @@ -41,12 +41,13 @@ public override void Process(EntitySpawnedByClient packet, Player playerWhoSpawn playerManager.SendPacketToAllPlayers(ownershipChangePacket); } + SpawnEntities spawnEntities = new(entity, packet.RequireRespawn); foreach (Player player in playerManager.GetConnectedPlayers()) { bool isOtherPlayer = player != playerWhoSpawned; if (isOtherPlayer && player.CanSee(entity)) { - player.SendPacket(new SpawnEntities(entity, packet.RequireRespawn)); + player.SendPacket(spawnEntities); } } } diff --git a/NitroxServer/GameLogic/Bases/BuildingManager.cs b/NitroxServer/GameLogic/Bases/BuildingManager.cs index 7ed953dedf..962e5e47fc 100644 --- a/NitroxServer/GameLogic/Bases/BuildingManager.cs +++ b/NitroxServer/GameLogic/Bases/BuildingManager.cs @@ -36,7 +36,7 @@ public bool AddGhost(PlaceGhost placeGhost) return false; } - worldEntityManager.AddGlobalRootEntity(ghostEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(ghostEntity); return true; } @@ -57,7 +57,7 @@ public bool AddGhost(PlaceGhost placeGhost) } parentEntity.ChildEntities.Add(ghostEntity); - worldEntityManager.AddGlobalRootEntity(ghostEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(ghostEntity); return true; } @@ -72,7 +72,7 @@ public bool AddModule(PlaceModule placeModule) return false; } - worldEntityManager.AddGlobalRootEntity(moduleEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(moduleEntity); return true; } @@ -93,7 +93,7 @@ public bool AddModule(PlaceModule placeModule) } parentEntity.ChildEntities.Add(moduleEntity); - worldEntityManager.AddGlobalRootEntity(moduleEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(moduleEntity); return true; } @@ -141,7 +141,7 @@ public bool CreateBase(PlaceBase placeBase) } worldEntityManager.RemoveGlobalRootEntity(entity.Id); - worldEntityManager.AddGlobalRootEntity(placeBase.BuildEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(placeBase.BuildEntity); return true; } @@ -195,7 +195,7 @@ public bool UpdateBase(Player player, UpdateBase updateBase, out int operationId if (updateBase.BuiltPieceEntity != null && updateBase.BuiltPieceEntity is GlobalRootEntity builtPieceEntity) { - worldEntityManager.AddGlobalRootEntity(builtPieceEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(builtPieceEntity); buildEntity.ChildEntities.Add(builtPieceEntity); } @@ -231,7 +231,7 @@ public bool ReplaceBaseByGhost(BaseDeconstructed baseDeconstructed) } worldEntityManager.RemoveGlobalRootEntity(baseDeconstructed.FormerBaseId); - worldEntityManager.AddGlobalRootEntity(baseDeconstructed.ReplacerGhost); + worldEntityManager.AddOrUpdateGlobalRootEntity(baseDeconstructed.ReplacerGhost); return true; } @@ -265,7 +265,7 @@ public bool ReplacePieceByGhost(Player player, PieceDeconstructed pieceDeconstru removedEntity = worldEntityManager.RemoveGlobalRootEntity(pieceDeconstructed.PieceId).Value; GhostEntity ghostEntity = pieceDeconstructed.ReplacerGhost; - worldEntityManager.AddGlobalRootEntity(ghostEntity); + worldEntityManager.AddOrUpdateGlobalRootEntity(ghostEntity); buildEntity.ChildEntities.Add(ghostEntity); buildEntity.BaseData = pieceDeconstructed.BaseData; buildEntity.OperationId++; @@ -286,7 +286,7 @@ public bool CreateWaterParkPiece(WaterParkDeconstructed waterParkDeconstructed, return false; } InteriorPieceEntity newPiece = waterParkDeconstructed.NewWaterPark; - worldEntityManager.AddGlobalRootEntity(newPiece); + worldEntityManager.AddOrUpdateGlobalRootEntity(newPiece); buildEntity.ChildEntities.Add(newPiece); foreach (NitroxId childId in waterParkDeconstructed.MovedChildrenIds) diff --git a/NitroxServer/GameLogic/Entities/EntitySimulation.cs b/NitroxServer/GameLogic/Entities/EntitySimulation.cs index baa644fe5e..a8d3dd85d7 100644 --- a/NitroxServer/GameLogic/Entities/EntitySimulation.cs +++ b/NitroxServer/GameLogic/Entities/EntitySimulation.cs @@ -27,19 +27,9 @@ public EntitySimulation(EntityRegistry entityRegistry, WorldEntityManager worldE this.simulationWhitelist = simulationWhitelist; } - public void BroadcastSimulationChangesForCellUpdates(Player player, AbsoluteEntityCell[] added, AbsoluteEntityCell[] removed) + public List GetSimulationChangesForCell(Player player, AbsoluteEntityCell cell) { - List ownershipChanges = new(); - - AddCells(player, added, ownershipChanges); - RemoveCells(player, removed, ownershipChanges); - - BroadcastSimulationChanges(ownershipChanges); - } - - public void BroadcastSimulationChangesForBatchAddition(Player player, NitroxInt3 batchId) - { - List entities = worldEntityManager.GetEntities(batchId); + List entities = worldEntityManager.GetEntities(cell); List addedEntities = FilterSimulatableEntities(player, entities); List ownershipChanges = new(); @@ -50,40 +40,21 @@ public void BroadcastSimulationChangesForBatchAddition(Player player, NitroxInt3 ownershipChanges.Add(new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)); } - BroadcastSimulationChanges(ownershipChanges); - } - - private void RemoveCells(Player player, AbsoluteEntityCell[] removed, List ownershipChanges) - { - foreach (AbsoluteEntityCell cell in removed) - { - List entities = worldEntityManager.GetEntities(cell); - IEnumerable revokedEntities = entities.Where(entity => !player.CanSee(entity) && simulationOwnershipData.RevokeIfOwner(entity.Id, player)); - AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges); - } + return ownershipChanges; } - private void AddCells(Player player, AbsoluteEntityCell[] added, List ownershipChanges) + public void FillWithRemovedCells(Player player, AbsoluteEntityCell removedCell, List ownershipChanges) { - foreach (AbsoluteEntityCell cell in added) - { - List entities = worldEntityManager.GetEntities(cell); - List addedEntities = FilterSimulatableEntities(player, entities); - - foreach (WorldEntity entity in addedEntities) - { - bool doesEntityMove = ShouldSimulateEntityMovement(entity); - ownershipChanges.Add(new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)); - } - } + List entities = worldEntityManager.GetEntities(removedCell); + IEnumerable revokedEntities = entities.Where(entity => !player.CanSee(entity) && simulationOwnershipData.RevokeIfOwner(entity.Id, player)); + AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges); } - private void BroadcastSimulationChanges(List ownershipChanges) + public void BroadcastSimulationChanges(List ownershipChanges) { if (ownershipChanges.Count > 0) { - // TODO: This should be moved to `SimulationOwnership` - SimulationOwnershipChange ownershipChange = new SimulationOwnershipChange(ownershipChanges); + SimulationOwnershipChange ownershipChange = new(ownershipChanges); playerManager.SendPacketToAllPlayers(ownershipChange); } } diff --git a/NitroxServer/GameLogic/Entities/WorldEntityManager.cs b/NitroxServer/GameLogic/Entities/WorldEntityManager.cs index 3e7e4b7035..d4b3bda7e0 100644 --- a/NitroxServer/GameLogic/Entities/WorldEntityManager.cs +++ b/NitroxServer/GameLogic/Entities/WorldEntityManager.cs @@ -9,368 +9,350 @@ using NitroxModel.Helper; using NitroxServer.GameLogic.Entities.Spawning; -namespace NitroxServer.GameLogic.Entities +namespace NitroxServer.GameLogic.Entities; + +/// +/// Regular are held in cells and should be registered in and . +/// But are held in their own root object (GlobalRoot) so they should never be registered in cells (they're seeable at all times). +/// +public class WorldEntityManager { - public class WorldEntityManager - { - private readonly EntityRegistry entityRegistry; + private readonly EntityRegistry entityRegistry; - /// - /// Phasing entities can disappear if you go out of range. - /// - private readonly Dictionary> phasingEntitiesByBatchId; + /// + /// World entities can disappear if you go out of range. + /// + private readonly Dictionary> worldEntitiesByBatchId; - private readonly Dictionary> phasingEntitiesByCellId; + private readonly Dictionary> worldEntitiesByCell; - /// - /// Global root entities that are always visible. - /// - private readonly Dictionary globalRootEntitiesById; + /// + /// Global root entities that are always visible. + /// + private readonly Dictionary globalRootEntitiesById; - private readonly BatchEntitySpawner batchEntitySpawner; + private readonly BatchEntitySpawner batchEntitySpawner; - private readonly object worldEntitiesLock; - private readonly object globalRootEntitiesLock; + private readonly object worldEntitiesLock; + private readonly object globalRootEntitiesLock; - public WorldEntityManager(EntityRegistry entityRegistry, BatchEntitySpawner batchEntitySpawner) - { - List worldEntities = entityRegistry.GetEntities(); + public WorldEntityManager(EntityRegistry entityRegistry, BatchEntitySpawner batchEntitySpawner) + { + List worldEntities = entityRegistry.GetEntities(); - globalRootEntitiesById = entityRegistry.GetEntities().ToDictionary(entity => entity.Id); + globalRootEntitiesById = entityRegistry.GetEntities().ToDictionary(entity => entity.Id); - phasingEntitiesByBatchId = worldEntities.Where(entity => entity is not GlobalRootEntity) - .GroupBy(entity => entity.AbsoluteEntityCell.BatchId) - .ToDictionary(group => group.Key, group => group.ToList()); + worldEntitiesByBatchId = worldEntities.Where(entity => entity is not GlobalRootEntity) + .GroupBy(entity => entity.AbsoluteEntityCell.BatchId) + .ToDictionary(group => group.Key, group => group.ToDictionary(entity => entity.Id, entity => entity)); - phasingEntitiesByCellId = worldEntities.Where(entity => entity is not GlobalRootEntity) - .GroupBy(entity => entity.AbsoluteEntityCell) - .ToDictionary(group => group.Key, group => group.ToList()); - this.entityRegistry = entityRegistry; - this.batchEntitySpawner = batchEntitySpawner; + 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; - worldEntitiesLock = new(); - globalRootEntitiesLock = new(); - } + worldEntitiesLock = new(); + globalRootEntitiesLock = new(); + } - public List GetGlobalRootEntities(bool rootOnly = false) + public List GetGlobalRootEntities(bool rootOnly = false) + { + if (rootOnly) { - if (rootOnly) - { - return GetGlobalRootEntities().Where(entity => entity.ParentId == null).ToList(); - } - return GetGlobalRootEntities(); + return GetGlobalRootEntities().Where(entity => entity.ParentId == null).ToList(); } + return GetGlobalRootEntities(); + } - public List GetGlobalRootEntities() where T : GlobalRootEntity + public List GetGlobalRootEntities() where T : GlobalRootEntity + { + lock (globalRootEntitiesLock) { - lock (globalRootEntitiesLock) - { - return new(globalRootEntitiesById.Values.OfType()); - } + return new(globalRootEntitiesById.Values.OfType()); } + } - public List GetEntities(NitroxInt3 batchId) + public List GetEntities(AbsoluteEntityCell cell) + { + lock (worldEntitiesLock) { - List result; - - lock (worldEntitiesLock) + if (worldEntitiesByCell.TryGetValue(cell, out Dictionary batchEntities)) { - if (!phasingEntitiesByBatchId.TryGetValue(batchId, out result)) - { - result = phasingEntitiesByBatchId[batchId] = new List(); - } + return batchEntities.Values.ToList(); } - - return result; } - public List GetEntities(AbsoluteEntityCell cellId) - { - List result; - - lock (worldEntitiesLock) - { - if (!phasingEntitiesByCellId.TryGetValue(cellId, out result)) - { - result = phasingEntitiesByCellId[cellId] = new List(); - } - } + return []; + } - return result; - } + public Optional UpdateEntityPosition(NitroxId id, NitroxVector3 position, NitroxQuaternion rotation) + { + Optional opEntity = entityRegistry.GetEntityById(id); - public Optional UpdateEntityPosition(NitroxId id, NitroxVector3 position, NitroxQuaternion rotation) + if (!opEntity.HasValue) { - Optional opEntity = entityRegistry.GetEntityById(id); - - if (!opEntity.HasValue) - { - Log.Debug("Could not update entity position because it was not found (maybe it was recently picked up)"); - return Optional.Empty; - } + Log.Debug("Could not update entity position because it was not found (maybe it was recently picked up)"); + return Optional.Empty; + } - WorldEntity entity = opEntity.Value; - AbsoluteEntityCell oldCell = entity.AbsoluteEntityCell; + WorldEntity entity = opEntity.Value; + AbsoluteEntityCell oldCell = entity.AbsoluteEntityCell; - entity.Transform.Position = position; - entity.Transform.Rotation = rotation; + entity.Transform.Position = position; + entity.Transform.Rotation = rotation; - AbsoluteEntityCell newCell = entity.AbsoluteEntityCell; - if (oldCell != newCell) - { - EntitySwitchedCells(entity, oldCell, newCell); - } - - return Optional.Of(newCell); + AbsoluteEntityCell newCell = entity.AbsoluteEntityCell; + if (oldCell != newCell) + { + EntitySwitchedCells(entity, oldCell, newCell); } - public void AddGlobalRootEntity(GlobalRootEntity entity, bool addToRegistry = true) + return Optional.Of(newCell); + } + + public Optional RemoveGlobalRootEntity(NitroxId entityId, bool removeFromRegistry = true) + { + Optional removedEntity = Optional.Empty; + lock (globalRootEntitiesLock) { - lock (globalRootEntitiesLock) + if (removeFromRegistry) { - if (!globalRootEntitiesById.ContainsKey(entity.Id)) + // 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)) { - if (addToRegistry) + List playerEntities = FindPlayerEntitiesInChildren(globalRootEntity); + foreach (PlayerWorldEntity childPlayerEntity in playerEntities) { - entityRegistry.AddOrUpdate(entity); + // Reparent the entity on top of GlobalRoot + globalRootEntity.ChildEntities.Remove(childPlayerEntity); + childPlayerEntity.ParentId = null; + + // Make sure the PlayerEntity is correctly registered + AddOrUpdateGlobalRootEntity(childPlayerEntity); } - globalRootEntitiesById.Add(entity.Id, entity); - } - else - { - Log.Info($"Entity Already Exists for Id: {entity.Id} Item: {entity.TechType}"); } + removedEntity = entityRegistry.RemoveEntity(entityId); } + globalRootEntitiesById.Remove(entityId); } + return removedEntity; + } - public Optional RemoveGlobalRootEntity(NitroxId entityId, bool removeFromRegistry = true) + public void TrackEntityInTheWorld(WorldEntity entity) + { + if (entity is GlobalRootEntity globalRootEntity) { - Optional 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)) - { - List playerEntities = new(); - FindPlayerEntitiesInChildrenRecursively(globalRootEntity, playerEntities); - 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 - UpdateGlobalRootEntity(childPlayerEntity); - } - } - removedEntity = entityRegistry.RemoveEntity(entityId); - } - globalRootEntitiesById.Remove(entityId); - } - return removedEntity; + AddOrUpdateGlobalRootEntity(globalRootEntity, false); + return; } - public void TrackEntityInTheWorld(WorldEntity entity) + RegisterWorldEntity(entity); + } + + /// + /// Automatically registers a WorldEntity in its AbsoluteEntityCell + /// + /// + /// The provided should NOT be a GlobalRootEntity (they don't stand in cells) + /// + public void RegisterWorldEntity(WorldEntity entity) + { + RegisterWorldEntityInCell(entity, entity.AbsoluteEntityCell); + } + + public void RegisterWorldEntityInCell(WorldEntity entity, AbsoluteEntityCell cell) + { + lock (worldEntitiesLock) { - if (entity is GlobalRootEntity globalRootEntity) + if (!worldEntitiesByBatchId.TryGetValue(cell.BatchId, out Dictionary worldEntitiesInBatch)) { - AddGlobalRootEntity(globalRootEntity, false); + worldEntitiesInBatch = worldEntitiesByBatchId[cell.BatchId] = []; } - else - { - lock (worldEntitiesLock) - { - if (!phasingEntitiesByBatchId.TryGetValue(entity.AbsoluteEntityCell.BatchId, out List phasingEntitiesInBatch)) - { - phasingEntitiesInBatch = phasingEntitiesByBatchId[entity.AbsoluteEntityCell.BatchId] = new List(); - } + worldEntitiesInBatch[entity.Id] = entity; - phasingEntitiesInBatch.Add(entity); - } - - lock (worldEntitiesLock) - { - if (!phasingEntitiesByCellId.TryGetValue(entity.AbsoluteEntityCell, out List phasingEntitiesInCell)) - { - phasingEntitiesInCell = phasingEntitiesByCellId[entity.AbsoluteEntityCell] = new List(); - } - - phasingEntitiesInCell.Add(entity); - } + if (!worldEntitiesByCell.TryGetValue(cell, out Dictionary worldEntitiesInCell)) + { + worldEntitiesInCell = worldEntitiesByCell[cell] = []; } + worldEntitiesInCell[entity.Id] = entity; } + } - public void LoadAllUnspawnedEntities(System.Threading.CancellationToken token) - { - IMap map = NitroxServiceLocator.LocateService(); - - int totalEntites = 0; + /// + /// Automatically unregisters a WorldEntity in its AbsoluteEntityCell + /// + public void UnregisterWorldEntity(WorldEntity entity) + { + UnregisterWorldEntityFromCell(entity.Id, entity.AbsoluteEntityCell); + } - for (int x = 0; x < map.DimensionsInBatches.X; x++) + public void UnregisterWorldEntityFromCell(NitroxId entityId, AbsoluteEntityCell cell) + { + lock (worldEntitiesLock) + { + if (worldEntitiesByBatchId.TryGetValue(cell.BatchId, out Dictionary worldEntitiesInBatch)) { - 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})"); - - totalEntites += spawned; - } - } - - if (totalEntites > 0) - { - Log.Info($"Loading: {(int)((totalEntites/ 504732.0) * 100)}%"); - } + worldEntitiesInBatch.Remove(entityId); } - } - public bool IsBatchSpawned(NitroxInt3 batchId) - { - return batchEntitySpawner.IsBatchSpawned(batchId); + if (worldEntitiesByCell.TryGetValue(cell, out Dictionary worldEntitiesInCell)) + { + worldEntitiesInCell.Remove(entityId); + } } + } - public int LoadUnspawnedEntities(NitroxInt3 batchId, bool suppressLogs) - { - List spawnedEntities = batchEntitySpawner.LoadUnspawnedEntities(batchId, suppressLogs); + public void LoadAllUnspawnedEntities(System.Threading.CancellationToken token) + { + IMap map = NitroxServiceLocator.LocateService(); - List nonCellRootEntities = spawnedEntities.Where(entity => typeof(WorldEntity).IsAssignableFrom(entity.GetType()) && - entity.GetType() != typeof(CellRootEntity)) - .Cast() - .ToList(); + int totalEntites = 0; - // 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()) + for (int x = 0; x < map.DimensionsInBatches.X; x++) + { + token.ThrowIfCancellationRequested(); + for (int y = 0; y < map.DimensionsInBatches.Y; y++) { - foreach (WorldEntity worldEntity in cellRoot.ChildEntities.Cast()) + for (int z = 0; z < map.DimensionsInBatches.Z; z++) { - worldEntity.ParentId = null; - worldEntity.Transform.SetParent(null, true); - nonCellRootEntities.Add(worldEntity); - } + int spawned = LoadUnspawnedEntities(new(x, y, z), true); - cellRoot.ChildEntities = new List(); - } - // Specific type of entities which is not parented to a CellRootEntity - nonCellRootEntities.AddRange(spawnedEntities.OfType()); + Log.Debug($"Loaded {spawned} entities from batch ({x}, {y}, {z})"); - entityRegistry.AddEntitiesIgnoringDuplicate(nonCellRootEntities.OfType().ToList()); + totalEntites += spawned; + } + } - foreach (WorldEntity entity in nonCellRootEntities) + if (totalEntites > 0) { - List entitiesInBatch = GetEntities(entity.AbsoluteEntityCell.BatchId); - entitiesInBatch.Add(entity); - - List entitiesInCell = GetEntities(entity.AbsoluteEntityCell); - entitiesInCell.Add(entity); + // TODO: Count + Log.Info($"Loading: {(int)((totalEntites/ 504732.0) * 100)}%"); } - - return nonCellRootEntities.Count; } + } - private void EntitySwitchedCells(WorldEntity entity, AbsoluteEntityCell oldCell, AbsoluteEntityCell newCell) + public int LoadUnspawnedEntities(NitroxInt3 batchId, bool suppressLogs) + { + List spawnedEntities = batchEntitySpawner.LoadUnspawnedEntities(batchId, suppressLogs); + + List entitiesInCells = spawnedEntities.Where(entity => typeof(WorldEntity).IsAssignableFrom(entity.GetType()) && + entity.GetType() != typeof(CellRootEntity) && + entity.GetType() != typeof(GlobalRootEntity)) + .Cast() + .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()) { - if (entity is GlobalRootEntity) + foreach (WorldEntity worldEntity in cellRoot.ChildEntities.Cast()) { - return; // We don't care what cell a global root entity resides in. Only phasing entities. + worldEntity.ParentId = null; + worldEntity.Transform.SetParent(null, true); + entitiesInCells.Add(worldEntity); } - if (oldCell.BatchId != newCell.BatchId) - { - lock (worldEntitiesLock) - { - List oldList = GetEntities(oldCell.BatchId); - oldList.Remove(entity); + cellRoot.ChildEntities = new List(); + } + // Specific type of entities which is not parented to a CellRootEntity + entitiesInCells.AddRange(spawnedEntities.OfType()); - List newList = GetEntities(newCell.BatchId); - newList.Add(entity); - } - } + entityRegistry.AddEntitiesIgnoringDuplicate(entitiesInCells.OfType().ToList()); - lock (worldEntitiesLock) - { - List oldList = GetEntities(oldCell); - oldList.Remove(entity); + foreach (WorldEntity entity in entitiesInCells) + { + RegisterWorldEntity(entity); + } - List newList = GetEntities(newCell); - newList.Add(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. } - public void StopTrackingEntity(WorldEntity entity) + if (oldCell.BatchId != newCell.BatchId) { - if (entity is GlobalRootEntity) - { - RemoveGlobalRootEntity(entity.Id, false); - } - else + lock (worldEntitiesLock) { - lock (worldEntitiesLock) - { - if (phasingEntitiesByBatchId.TryGetValue(entity.AbsoluteEntityCell.BatchId, out List batchEntities)) - { - batchEntities.Remove(entity); - } - } + // Specifically remove entity from oldCell + UnregisterWorldEntityFromCell(entity.Id, oldCell); - lock (worldEntitiesLock) - { - if (phasingEntitiesByCellId.TryGetValue(entity.AbsoluteEntityCell, out List cellEntities)) - { - cellEntities.Remove(entity); - } - } + // Automatically add entity to its new cell + RegisterWorldEntityInCell(entity, newCell); } } + } - public bool TryDestroyEntity(NitroxId entityId, out Optional entity) + public void StopTrackingEntity(WorldEntity entity) + { + if (entity is GlobalRootEntity) { - entity = entityRegistry.RemoveEntity(entityId); + RemoveGlobalRootEntity(entity.Id, false); + } + else + { + UnregisterWorldEntity(entity); + } + } - if (!entity.HasValue) - { - return false; - } + public bool TryDestroyEntity(NitroxId entityId, out Optional entity) + { + entity = entityRegistry.RemoveEntity(entityId); - if (entity.Value is WorldEntity worldEntity) - { - StopTrackingEntity(worldEntity); - } + if (!entity.HasValue) + { + return false; + } - return true; + if (entity.Value is WorldEntity worldEntity) + { + StopTrackingEntity(worldEntity); } - /// - /// To avoid risking not having the same entity in and in EntityRegistry, we update both at the same time. - /// - public void UpdateGlobalRootEntity(GlobalRootEntity entity) + return true; + } + + /// + /// To avoid risking not having the same entity in and in EntityRegistry, we update both at the same time. + /// + public void AddOrUpdateGlobalRootEntity(GlobalRootEntity entity, bool addOrUpdateRegistry = true) + { + lock (globalRootEntitiesLock) { - lock (globalRootEntitiesLock) + if (addOrUpdateRegistry) { entityRegistry.AddOrUpdate(entity); - globalRootEntitiesById[entity.Id] = entity; } + globalRootEntitiesById[entity.Id] = entity; } + } + + private List FindPlayerEntitiesInChildren(Entity parentEntity) + { + List playerWorldEntities = []; + List entitiesToSearch = [parentEntity]; - private void FindPlayerEntitiesInChildrenRecursively(Entity parentEntity, List playerEntities) + while (entitiesToSearch.Count > 0) { - foreach (Entity childEntity in parentEntity.ChildEntities) + Entity currentEntity = entitiesToSearch[^1]; + entitiesToSearch.RemoveAt(entitiesToSearch.Count - 1); + + if (currentEntity is PlayerWorldEntity playerWorldEntity) { - if (childEntity is PlayerWorldEntity playerWorldEntity) - { - playerEntities.Add(playerWorldEntity); - continue; - } - FindPlayerEntitiesInChildrenRecursively(childEntity, playerEntities); + playerWorldEntities.Add(playerWorldEntity); + } + else + { + entitiesToSearch.AddRange(currentEntity.ChildEntities); } } + return playerWorldEntities; } }