using System; using System.Collections.Generic; using System.Linq; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.Packets; namespace NitroxServer.GameLogic.Entities; public class EntitySimulation { private const SimulationLockType DEFAULT_ENTITY_SIMULATION_LOCKTYPE = SimulationLockType.TRANSIENT; private readonly EntityRegistry entityRegistry; private readonly WorldEntityManager worldEntityManager; private readonly PlayerManager playerManager; private readonly ISimulationWhitelist simulationWhitelist; private readonly SimulationOwnershipData simulationOwnershipData; public EntitySimulation(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, ISimulationWhitelist simulationWhitelist) { this.entityRegistry = entityRegistry; this.worldEntityManager = worldEntityManager; this.simulationOwnershipData = simulationOwnershipData; this.playerManager = playerManager; this.simulationWhitelist = simulationWhitelist; } public List GetSimulationChangesForCell(Player player, AbsoluteEntityCell cell) { List entities = worldEntityManager.GetEntities(cell); List addedEntities = FilterSimulatableEntities(player, entities); List ownershipChanges = new(); foreach (WorldEntity entity in addedEntities) { bool doesEntityMove = ShouldSimulateEntityMovement(entity); ownershipChanges.Add(new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)); } return ownershipChanges; } public void FillWithRemovedCells(Player player, AbsoluteEntityCell removedCell, List ownershipChanges) { List entities = worldEntityManager.GetEntities(removedCell); IEnumerable revokedEntities = entities.Where(entity => !player.CanSee(entity) && simulationOwnershipData.RevokeIfOwner(entity.Id, player)); AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges); } public void BroadcastSimulationChanges(List ownershipChanges) { if (ownershipChanges.Count > 0) { SimulationOwnershipChange ownershipChange = new(ownershipChanges); playerManager.SendPacketToAllPlayers(ownershipChange); } } public List CalculateSimulationChangesFromPlayerDisconnect(Player player) { List ownershipChanges = new(); List revokedEntityIds = simulationOwnershipData.RevokeAllForOwner(player); List revokedEntities = entityRegistry.GetEntities(revokedEntityIds); AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges); return ownershipChanges; } public SimulatedEntity AssignNewEntityToPlayer(Entity entity, Player player, bool shouldEntityMove = true) { if (simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)) { bool doesEntityMove = shouldEntityMove && entity is WorldEntity worldEntity && ShouldSimulateEntityMovement(worldEntity); return new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); } throw new Exception($"New entity was already being simulated by someone else: {entity.Id}"); } public List AssignGlobalRootEntitiesAndGetData(Player player) { List simulatedEntities = new(); foreach (GlobalRootEntity entity in worldEntityManager.GetGlobalRootEntities()) { simulationOwnershipData.TryToAcquire(entity.Id, player, SimulationLockType.TRANSIENT); if (!simulationOwnershipData.TryGetLock(entity.Id, out SimulationOwnershipData.PlayerLock playerLock)) { continue; } bool doesEntityMove = ShouldSimulateEntityMovement(entity); SimulatedEntity simulatedEntity = new(entity.Id, playerLock.Player.Id, doesEntityMove, playerLock.LockType); simulatedEntities.Add(simulatedEntity); } return simulatedEntities; } private void AssignEntitiesToOtherPlayers(Player oldPlayer, IEnumerable entities, List ownershipChanges) { List otherPlayers = playerManager.GetConnectedPlayersExcept(oldPlayer); foreach (Entity entity in entities) { if (TryAssignEntityToPlayers(otherPlayers, entity, out SimulatedEntity simulatedEntity)) { ownershipChanges.Add(simulatedEntity); } } } public bool TryAssignEntityToPlayers(List players, Entity entity, out SimulatedEntity simulatedEntity) { NitroxId id = entity.Id; foreach (Player player in players) { if (player.CanSee(entity) && simulationOwnershipData.TryToAcquire(id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)) { bool doesEntityMove = entity is WorldEntity worldEntity && ShouldSimulateEntityMovement(worldEntity); Log.Verbose($"Player {player.Name} has taken over simulating {id}"); simulatedEntity = new(id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); return true; } } simulatedEntity = null; return false; } private List FilterSimulatableEntities(Player player, List entities) { return entities.Where(entity => { bool isEligibleForSimulation = player.CanSee(entity) && ShouldSimulateEntity(entity); return isEligibleForSimulation && simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); }).ToList(); } public bool ShouldSimulateEntity(WorldEntity entity) { return simulationWhitelist.UtilityWhitelist.Contains(entity.TechType) || ShouldSimulateEntityMovement(entity); } public bool ShouldSimulateEntityMovement(WorldEntity entity) { return !entity.SpawnedByServer || simulationWhitelist.MovementWhitelist.Contains(entity.TechType); } public bool ShouldSimulateEntityMovement(NitroxId entityId) { return entityRegistry.TryGetEntityById(entityId, out WorldEntity worldEntity) && ShouldSimulateEntityMovement(worldEntity); } public void EntityDestroyed(NitroxId id) { simulationOwnershipData.RevokeOwnerOfId(id); } }