diff --git a/build.gradle b/build.gradle index cccc1c139..2480d54cb 100644 --- a/build.gradle +++ b/build.gradle @@ -128,6 +128,9 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine:3.0.4' + // https://mvnrepository.com/artifact/com.zaxxer/SparseBitSet + implementation group: 'com.zaxxer', name: 'SparseBitSet', version: '1.2' + // Guava 21.0+ required for Mixin api 'com.google.guava:guava:31.0.1-jre' diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 55fa5f177..607a44936 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -6,14 +6,12 @@ import net.minestom.server.command.CommandManager; import net.minestom.server.data.DataManager; import net.minestom.server.data.DataType; import net.minestom.server.data.SerializableData; -import net.minestom.server.entity.Player; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.exception.ExceptionManager; import net.minestom.server.extensions.Extension; import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.fluid.Fluid; import net.minestom.server.gamedata.tags.TagManager; -import net.minestom.server.instance.Chunk; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.BlockManager; import net.minestom.server.instance.block.rule.BlockPlacementRule; @@ -23,7 +21,6 @@ import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.packet.server.play.PluginMessagePacket; import net.minestom.server.network.packet.server.play.ServerDifficultyPacket; -import net.minestom.server.network.packet.server.play.UpdateViewDistancePacket; import net.minestom.server.network.socket.Server; import net.minestom.server.ping.ResponseDataConsumer; import net.minestom.server.recipe.RecipeManager; @@ -120,8 +117,8 @@ public final class MinecraftServer { private static boolean started; private static volatile boolean stopping; - private static int chunkViewDistance = 8; - private static int entityViewDistance = 5; + private static int chunkViewDistance = Integer.getInteger("minestom.chunk-view-distance", 8); + private static int entityViewDistance = Integer.getInteger("minestom.entity-view-distance", 5); private static int compressionThreshold = 256; private static boolean groupedPacket = true; private static boolean terminalEnabled = System.getProperty("minestom.terminal.disabled") == null; @@ -446,20 +443,14 @@ public final class MinecraftServer { * * @param chunkViewDistance the new chunk view distance * @throws IllegalArgumentException if {@code chunkViewDistance} is not between 2 and 32 + * @deprecated should instead be defined with a java property */ + @Deprecated public static void setChunkViewDistance(int chunkViewDistance) { + Check.stateCondition(started, "You cannot change the chunk view distance after the server has been started."); Check.argCondition(!MathUtils.isBetween(chunkViewDistance, 2, 32), "The chunk view distance must be between 2 and 32"); MinecraftServer.chunkViewDistance = chunkViewDistance; - if (started) { - for (final Player player : connectionManager.getOnlinePlayers()) { - final Chunk playerChunk = player.getChunk(); - if (playerChunk != null) { - player.getPlayerConnection().sendPacket(new UpdateViewDistancePacket(player.getChunkRange())); - player.refreshVisibleChunks(playerChunk); - } - } - } } /** @@ -476,19 +467,14 @@ public final class MinecraftServer { * * @param entityViewDistance the new entity view distance * @throws IllegalArgumentException if {@code entityViewDistance} is not between 0 and 32 + * @deprecated should instead be defined with a java property */ + @Deprecated public static void setEntityViewDistance(int entityViewDistance) { + Check.stateCondition(started, "You cannot change the entity view distance after the server has been started."); Check.argCondition(!MathUtils.isBetween(entityViewDistance, 0, 32), "The entity view distance must be between 0 and 32"); MinecraftServer.entityViewDistance = entityViewDistance; - if (started) { - for (final Player player : connectionManager.getOnlinePlayers()) { - final Chunk playerChunk = player.getChunk(); - if (playerChunk != null) { - player.refreshVisibleEntities(playerChunk); - } - } - } } /** diff --git a/src/main/java/net/minestom/server/Viewable.java b/src/main/java/net/minestom/server/Viewable.java index 15d8da701..684f5c2e8 100644 --- a/src/main/java/net/minestom/server/Viewable.java +++ b/src/main/java/net/minestom/server/Viewable.java @@ -37,8 +37,7 @@ public interface Viewable { * * @return A Set containing all the element's viewers */ - @NotNull - Set getViewers(); + @NotNull Set<@NotNull Player> getViewers(); /** * Gets if a player is seeing this viewable object. diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index ce0f8662e..babbdcf03 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -18,7 +18,10 @@ import net.minestom.server.entity.metadata.EntityMeta; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalHandles; import net.minestom.server.event.entity.*; +import net.minestom.server.event.instance.AddEntityToInstanceEvent; +import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent; import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; @@ -26,7 +29,6 @@ import net.minestom.server.instance.block.BlockGetter; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.play.*; -import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.permission.Permission; import net.minestom.server.permission.PermissionHandler; import net.minestom.server.potion.Potion; @@ -35,6 +37,7 @@ import net.minestom.server.potion.TimedPotion; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; import net.minestom.server.utils.PacketUtils; +import net.minestom.server.utils.ViewEngine; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.block.BlockIterator; import net.minestom.server.utils.chunk.ChunkUtils; @@ -98,10 +101,58 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler protected double gravityAcceleration; protected int gravityTickCount; // Number of tick where gravity tick was applied - private boolean autoViewable; private final int id; - protected final Set viewers = ConcurrentHashMap.newKeySet(); - private final Set unmodifiableViewers = Collections.unmodifiableSet(viewers); + // Players must be aware of all surrounding entities + // General entities should only be aware of surrounding players to update their viewing list + private final EntityTracker.Target trackingTarget = this instanceof Player ? + EntityTracker.Target.ENTITIES : EntityTracker.Target.class.cast(EntityTracker.Target.PLAYERS); + protected final EntityTracker.Update trackingUpdate = new EntityTracker.Update<>() { + @Override + public void add(@NotNull Entity entity) { + viewEngine.handleAutoViewAddition(entity); + } + + @Override + public void remove(@NotNull Entity entity) { + viewEngine.handleAutoViewRemoval(entity); + } + + @Override + public void updateTracker(@NotNull Point point, @Nullable EntityTracker tracker) { + viewEngine.updateTracker(point, tracker); + } + }; + + protected final ViewEngine viewEngine = new ViewEngine(this, + player -> { + // Add viewable + if (!Entity.this.viewEngine.viewableOption.predicate(player) || + !player.viewEngine.viewerOption.predicate(this)) return; + Entity.this.viewEngine.viewableOption.register(player); + player.viewEngine.viewerOption.register(this); + updateNewViewer(player); + }, + player -> { + // Remove viewable + Entity.this.viewEngine.viewableOption.unregister(player); + player.viewEngine.viewerOption.unregister(this); + updateOldViewer(player); + }, + this instanceof Player player ? entity -> { + // Add viewer + if (!Entity.this.viewEngine.viewerOption.predicate(entity) || + !entity.viewEngine.viewableOption.predicate(player)) return; + Entity.this.viewEngine.viewerOption.register(entity); + entity.viewEngine.viewableOption.register(player); + entity.updateNewViewer(player); + } : null, + this instanceof Player player ? entity -> { + // Remove viewer + Entity.this.viewEngine.viewerOption.unregister(entity); + entity.viewEngine.viewableOption.unregister(player); + entity.updateOldViewer(player); + } : null); + protected final Set viewers = viewEngine.asSet(); private final NBTCompound nbtCompound = new NBTCompound(); private final Set permissions = new CopyOnWriteArraySet<>(); @@ -144,8 +195,6 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler this.entityMeta = EntityTypeImpl.createMeta(entityType, this, this.metadata); - setAutoViewable(true); - Entity.ENTITY_BY_ID.put(id, this); Entity.ENTITY_BY_UUID.put(uuid, this); @@ -243,8 +292,8 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks) { Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!"); final Runnable endCallback = () -> { + this.previousPosition = this.position; this.position = position; - this.previousPosition = position; refreshCoordinate(position); synchronizePosition(true); }; @@ -304,80 +353,130 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler } /** - * When set to true, the entity will automatically get new viewers when they come too close. - * This can be use to have complete control over which player can see it, without having to deal with - * raw packets. - *

- * True by default for all entities. - * When set to false, it is important to mention that the players will not be removed automatically from its viewers - * list, you would have to do that manually using {@link #addViewer(Player)} and {@link #removeViewer(Player)}.. + * Gets if this entity is automatically sent to surrounding players. + * True by default. * * @return true if the entity is automatically viewable for close players, false otherwise */ public boolean isAutoViewable() { - return autoViewable; + return viewEngine.viewableOption.isAuto(); } /** - * Makes the entity auto viewable or only manually. + * Decides if this entity should be auto-viewable by nearby players. * - * @param autoViewable should the entity be automatically viewable for close players + * @param autoViewable true to add surrounding players, false to remove * @see #isAutoViewable() */ public void setAutoViewable(boolean autoViewable) { - this.autoViewable = autoViewable; + this.viewEngine.viewableOption.updateAuto(autoViewable); + } + + @ApiStatus.Experimental + public void updateViewableRule(@NotNull Predicate predicate) { + this.viewEngine.viewableOption.updateRule(predicate); + } + + @ApiStatus.Experimental + public void updateViewableRule() { + this.viewEngine.viewableOption.updateRule(); + } + + /** + * Gets if surrounding entities are automatically visible by this. + * True by default. + * + * @return true if surrounding entities are visible by this + */ + @ApiStatus.Experimental + public boolean autoViewEntities() { + return viewEngine.viewerOption.isAuto(); + } + + /** + * Decides if surrounding entities must be visible. + * + * @param autoViewer true to add view surrounding entities, false to remove + */ + @ApiStatus.Experimental + public void setAutoViewEntities(boolean autoViewer) { + this.viewEngine.viewerOption.updateAuto(autoViewer); + } + + @ApiStatus.Experimental + public void updateViewerRule(@NotNull Predicate predicate) { + this.viewEngine.viewerOption.updateRule(predicate); + } + + @ApiStatus.Experimental + public void updateViewerRule() { + this.viewEngine.viewerOption.updateRule(); } @Override public final boolean addViewer(@NotNull Player player) { - if (player == this) return false; - return addViewer0(player); - } - - protected boolean addViewer0(@NotNull Player player) { - if (!this.viewers.add(player)) { - return false; - } - player.viewableEntities.add(this); - - PlayerConnection playerConnection = player.getPlayerConnection(); - playerConnection.sendPacket(getEntityType().registry().spawnType().getSpawnPacket(this)); - if (hasVelocity()) { - playerConnection.sendPacket(getVelocityPacket()); - } - playerConnection.sendPacket(getMetadataPacket()); - // Passenger - final Set passengers = this.passengers; - if (!passengers.isEmpty()) { - for (Entity passenger : passengers) { - passenger.addViewer(player); - } - playerConnection.sendPacket(getPassengersPacket()); - } - // Head position - playerConnection.sendPacket(new EntityHeadLookPacket(getEntityId(), position.yaw())); + if (!viewEngine.manualAdd(player)) return false; + updateNewViewer(player); return true; } @Override public final boolean removeViewer(@NotNull Player player) { - if (player == this) return false; - return removeViewer0(player); - } - - protected boolean removeViewer0(@NotNull Player player) { - if (!viewers.remove(player)) { - return false; - } - player.getPlayerConnection().sendPacket(new DestroyEntitiesPacket(getEntityId())); - player.viewableEntities.remove(this); + if (!viewEngine.manualRemove(player)) return false; + updateOldViewer(player); return true; } - @NotNull + /** + * Called when a new viewer must be shown. + * Method can be subject to deadlocking if the target's viewers are also accessed. + * + * @param player the player to send the packets to + */ + @ApiStatus.Internal + public void updateNewViewer(@NotNull Player player) { + player.sendPacket(getEntityType().registry().spawnType().getSpawnPacket(this)); + if (hasVelocity()) player.sendPacket(getVelocityPacket()); + player.sendPacket(getMetadataPacket()); + // Passengers + final Set passengers = this.passengers; + if (!passengers.isEmpty()) { + for (Entity passenger : passengers) { + if (passenger != player) passenger.viewEngine.viewableOption.addition.accept(player); + } + player.sendPacket(getPassengersPacket()); + } + // Head position + player.sendPacket(new EntityHeadLookPacket(getEntityId(), position.yaw())); + } + + /** + * Called when a viewer must be destroyed. + * Method can be subject to deadlocking if the target's viewers are also accessed. + * + * @param player the player to send the packets to + */ + @ApiStatus.Internal + public void updateOldViewer(@NotNull Player player) { + final Set passengers = this.passengers; + if (!passengers.isEmpty()) { + for (Entity passenger : passengers) { + if (passenger != player) passenger.viewEngine.viewableOption.removal.accept(player); + } + } + player.sendPacket(new DestroyEntitiesPacket(getEntityId())); + } + @Override - public Set getViewers() { - return unmodifiableViewers; + public @NotNull Set getViewers() { + return viewers; + } + + /** + * Gets if this entity's viewers (surrounding players) can be predicted from surrounding chunks. + */ + public boolean hasPredictableViewers() { + return viewEngine.hasPredictableViewers(); } /** @@ -397,8 +496,8 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler this.entityMeta = EntityTypeImpl.createMeta(entityType, this, this.metadata); Set viewers = new HashSet<>(getViewers()); - getViewers().forEach(this::removeViewer0); - viewers.forEach(this::addViewer0); + getViewers().forEach(this::updateOldViewer); + viewers.forEach(this::updateNewViewer); } @NotNull @@ -434,11 +533,6 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler return; } - // Fix current chunk being null if the entity has been spawned before - if (currentChunk == null) { - refreshCurrentChunk(instance.getChunkAt(position)); - } - // Check if the entity chunk is loaded if (!ChunkUtils.isLoaded(currentChunk)) { // No update for entities in unloaded chunk @@ -483,17 +577,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler private void velocityTick() { this.gravityTickCount = onGround ? 0 : gravityTickCount + 1; - - final boolean isSocketClient = PlayerUtils.isSocketClient(this); - if (isSocketClient) { - if (position.samePoint(previousPosition)) - return; // Didn't move since last tick - // Calculate velocity from client - velocity = position.sub(previousPosition).asVec().mul(MinecraftServer.TICK_PER_SECOND); - previousPosition = position; - return; - } - + if (PlayerUtils.isSocketClient(this)) return; if (vehicle != null) return; final boolean noGravity = hasNoGravity(); @@ -538,6 +622,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler if (this instanceof ItemEntity) { // TODO find other exceptions + this.previousPosition = this.position; this.position = finalVelocityPosition; refreshCoordinate(finalVelocityPosition); } else { @@ -751,19 +836,33 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler Check.stateCondition(!instance.isRegistered(), "Instances need to be registered, please use InstanceManager#registerInstance or InstanceManager#registerSharedInstance"); final Instance previousInstance = this.instance; - if (previousInstance != null) { - previousInstance.UNSAFE_removeEntity(this); + if (Objects.equals(previousInstance, instance)) { + return teleport(spawnPosition); // Already in the instance, teleport to spawn point } + AddEntityToInstanceEvent event = new AddEntityToInstanceEvent(instance, this); + EventDispatcher.call(event); + if (event.isCancelled()) return null; // TODO what to return? + + if (previousInstance != null) removeFromInstance(previousInstance); + + this.isActive = true; this.position = spawnPosition; this.previousPosition = spawnPosition; - this.isActive = true; this.instance = instance; - return instance.loadOptionalChunk(position).thenAccept(chunk -> { - Check.notNull(chunk, "Entity has been placed in an unloaded chunk!"); - refreshCurrentChunk(chunk); - instance.UNSAFE_addEntity(this); - spawn(); - EventDispatcher.call(new EntitySpawnEvent(this, instance)); + return instance.loadOptionalChunk(spawnPosition).thenAccept(chunk -> { + try { + Check.notNull(chunk, "Entity has been placed in an unloaded chunk!"); + refreshCurrentChunk(chunk); + if (this instanceof Player player) { + instance.getWorldBorder().init(player); + player.sendPacket(instance.createTimePacket()); + } + instance.getEntityTracker().register(this, spawnPosition, trackingTarget, trackingUpdate); + spawn(); + EventDispatcher.call(new EntitySpawnEvent(this, instance)); + } catch (Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + } }); } @@ -784,6 +883,11 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler return setInstance(instance, this.position); } + private void removeFromInstance(Instance instance) { + EventDispatcher.call(new RemoveEntityFromInstanceEvent(instance, this)); + instance.getEntityTracker().unregister(this, position, trackingTarget, trackingUpdate); + } + /** * Gets the entity current velocity. * @@ -897,16 +1001,16 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler * @throws IllegalStateException if {@link #getInstance()} returns null or the passenger cannot be added */ public void addPassenger(@NotNull Entity entity) { - Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance"); + final Instance currentInstance = this.instance; + Check.stateCondition(currentInstance == null, "You need to set an instance using Entity#setInstance"); Check.stateCondition(entity == getVehicle(), "Cannot add the entity vehicle as a passenger"); final Entity vehicle = entity.getVehicle(); - if (vehicle != null) { - vehicle.removePassenger(entity); - } + if (vehicle != null) vehicle.removePassenger(entity); + if (!currentInstance.equals(entity.getInstance())) + entity.setInstance(currentInstance, position).join(); this.passengers.add(entity); entity.vehicle = this; sendPacketToViewersAndSelf(getPassengersPacket()); - // Updates the position of the new passenger, and then teleports the passenger updatePassengerPosition(position, entity); entity.synchronizePosition(false); @@ -1166,9 +1270,16 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler final Pos position = ignoreView ? previousPosition.withCoord(newPosition) : newPosition; if (position.equals(lastSyncedPosition)) return; this.position = position; + this.previousPosition = previousPosition; if (!position.samePoint(previousPosition)) { refreshCoordinate(position); + // Update player velocity + if (PlayerUtils.isSocketClient(this)) { + // Calculate from client + this.velocity = position.sub(previousPosition).asVec().mul(MinecraftServer.TICK_PER_SECOND); + } } + // Update viewers final boolean viewChange = !position.sameView(lastSyncedPosition); final double distanceX = Math.abs(position.x() - lastSyncedPosition.x()); final double distanceY = Math.abs(position.y() - lastSyncedPosition.y()); @@ -1240,29 +1351,31 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler * @param newPosition the new position */ private void refreshCoordinate(Point newPosition) { - if (hasPassenger()) { - for (Entity passenger : getPassengers()) { + // Passengers update + final Set passengers = getPassengers(); + if (!passengers.isEmpty()) { + for (Entity passenger : passengers) { updatePassengerPosition(newPosition, passenger); } } + // Handle chunk switch final Instance instance = getInstance(); - if (instance != null) { - final int lastChunkX = currentChunk.getChunkX(); - final int lastChunkZ = currentChunk.getChunkZ(); - final int newChunkX = newPosition.chunkX(); - final int newChunkZ = newPosition.chunkZ(); - if (lastChunkX != newChunkX || lastChunkZ != newChunkZ) { - // Entity moved in a new chunk - final Chunk newChunk = instance.getChunk(newChunkX, newChunkZ); - Check.notNull(newChunk, "The entity {0} tried to move in an unloaded chunk at {1}", getEntityId(), newPosition); - instance.UNSAFE_switchEntityChunk(this, currentChunk, newChunk); - if (this instanceof Player player) { - // Refresh player view - player.refreshVisibleChunks(newChunk); - player.refreshVisibleEntities(newChunk); - } - refreshCurrentChunk(newChunk); + assert instance != null; + instance.getEntityTracker().move(this, previousPosition, newPosition, trackingTarget, trackingUpdate); + final int lastChunkX = currentChunk.getChunkX(); + final int lastChunkZ = currentChunk.getChunkZ(); + final int newChunkX = newPosition.chunkX(); + final int newChunkZ = newPosition.chunkZ(); + if (lastChunkX != newChunkX || lastChunkZ != newChunkZ) { + // Entity moved in a new chunk + final Chunk newChunk = instance.getChunk(newChunkX, newChunkZ); + Check.notNull(newChunk, "The entity {0} tried to move in an unloaded chunk at {1}", getEntityId(), newPosition); + if (this instanceof Player player) { // Update visible chunks + player.sendPacket(new UpdateViewPositionPacket(newChunkX, newChunkZ)); + ChunkUtils.forDifferingChunksInRange(newChunkX, newChunkZ, lastChunkX, lastChunkZ, + MinecraftServer.getChunkViewDistance(), player.chunkAdder, player.chunkRemover); } + refreshCurrentChunk(newChunk); } } @@ -1342,21 +1455,17 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler public void remove() { if (isRemoved()) return; // Remove passengers if any (also done with LivingEntity#kill) - if (hasPassenger()) { - getPassengers().forEach(this::removePassenger); - } - var vehicle = this.vehicle; - if (vehicle != null) { - vehicle.removePassenger(this); - } + Set passengers = getPassengers(); + if (!passengers.isEmpty()) passengers.forEach(this::removePassenger); + final Entity vehicle = this.vehicle; + if (vehicle != null) vehicle.removePassenger(this); MinecraftServer.getUpdateManager().getThreadProvider().removeEntity(this); this.removed = true; this.shouldRemove = true; Entity.ENTITY_BY_ID.remove(id); Entity.ENTITY_BY_UUID.remove(uuid); - if (instance != null) { - instance.UNSAFE_removeEntity(this); - } + Instance currentInstance = this.instance; + if (currentInstance != null) removeFromInstance(currentInstance); } /** diff --git a/src/main/java/net/minestom/server/entity/ItemEntity.java b/src/main/java/net/minestom/server/entity/ItemEntity.java index 72c48b38b..9faf2d958 100644 --- a/src/main/java/net/minestom/server/entity/ItemEntity.java +++ b/src/main/java/net/minestom/server/entity/ItemEntity.java @@ -3,7 +3,7 @@ package net.minestom.server.entity; import net.minestom.server.entity.metadata.item.ItemEntityMeta; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.entity.EntityItemMergeEvent; -import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.EntityTracker; import net.minestom.server.item.ItemStack; import net.minestom.server.item.StackingRule; import net.minestom.server.utils.time.Cooldown; @@ -13,7 +13,6 @@ import org.jetbrains.annotations.Nullable; import java.time.Duration; import java.time.temporal.TemporalUnit; -import java.util.Set; /** * Represents an item on the ground. @@ -71,47 +70,26 @@ public class ItemEntity extends Entity { (mergeDelay == null || !Cooldown.hasCooldown(time, lastMergeCheck, mergeDelay))) { this.lastMergeCheck = time; - final Chunk chunk = instance.getChunkAt(getPosition()); - final Set entities = instance.getChunkEntities(chunk); - for (Entity entity : entities) { - if (entity instanceof ItemEntity) { + this.instance.getEntityTracker().nearbyEntities(position, mergeRange, + EntityTracker.Target.ITEMS, itemEntity -> { + if (itemEntity == this) return; + if (!itemEntity.isPickable() || !itemEntity.isMergeable()) return; + if (getDistance(itemEntity) > mergeRange) return; - // Do not merge with itself - if (entity == this) - continue; + final ItemStack itemStackEntity = itemEntity.getItemStack(); + final StackingRule stackingRule = itemStack.getStackingRule(); + final boolean canStack = stackingRule.canBeStacked(itemStack, itemStackEntity); - final ItemEntity itemEntity = (ItemEntity) entity; - if (!itemEntity.isPickable() || !itemEntity.isMergeable()) - continue; - - // Too far, do not merge - if (getDistance(itemEntity) > mergeRange) - continue; - - final ItemStack itemStackEntity = itemEntity.getItemStack(); - - final StackingRule stackingRule = itemStack.getStackingRule(); - final boolean canStack = stackingRule.canBeStacked(itemStack, itemStackEntity); - - if (!canStack) - continue; - - final int totalAmount = stackingRule.getAmount(itemStack) + stackingRule.getAmount(itemStackEntity); - final boolean canApply = stackingRule.canApply(itemStack, totalAmount); - - if (!canApply) - continue; - - final ItemStack result = stackingRule.apply(itemStack, totalAmount); - - EntityItemMergeEvent entityItemMergeEvent = new EntityItemMergeEvent(this, itemEntity, result); - EventDispatcher.callCancellable(entityItemMergeEvent, () -> { - setItemStack(entityItemMergeEvent.getResult()); - itemEntity.remove(); + if (!canStack) return; + final int totalAmount = stackingRule.getAmount(itemStack) + stackingRule.getAmount(itemStackEntity); + if (!stackingRule.canApply(itemStack, totalAmount)) return; + final ItemStack result = stackingRule.apply(itemStack, totalAmount); + EntityItemMergeEvent entityItemMergeEvent = new EntityItemMergeEvent(this, itemEntity, result); + EventDispatcher.callCancellable(entityItemMergeEvent, () -> { + setItemStack(entityItemMergeEvent.getResult()); + itemEntity.remove(); + }); }); - - } - } } } diff --git a/src/main/java/net/minestom/server/entity/LivingEntity.java b/src/main/java/net/minestom/server/entity/LivingEntity.java index e3752f89b..080c52b36 100644 --- a/src/main/java/net/minestom/server/entity/LivingEntity.java +++ b/src/main/java/net/minestom/server/entity/LivingEntity.java @@ -15,7 +15,7 @@ import net.minestom.server.event.entity.EntityDeathEvent; import net.minestom.server.event.entity.EntityFireEvent; import net.minestom.server.event.item.EntityEquipEvent; import net.minestom.server.event.item.PickupItemEvent; -import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.EntityTracker; import net.minestom.server.inventory.EquipmentHandler; import net.minestom.server.item.ItemStack; import net.minestom.server.network.ConnectionState; @@ -200,32 +200,21 @@ public class LivingEntity extends Entity implements EquipmentHandler { // Items picking if (canPickupItem() && itemPickupCooldown.isReady(time)) { itemPickupCooldown.refreshLastUpdate(time); - - final Chunk chunk = getChunk(); // TODO check surrounding chunks - final Set entities = instance.getChunkEntities(chunk); - for (Entity entity : entities) { - if (entity instanceof ItemEntity) { - // Do not pick up if not visible - if (this instanceof Player && !entity.isViewer((Player) this)) - continue; - - final ItemEntity itemEntity = (ItemEntity) entity; - if (!itemEntity.isPickable()) - continue; - - final BoundingBox itemBoundingBox = itemEntity.getBoundingBox(); - if (expandedBoundingBox.intersect(itemBoundingBox)) { - if (itemEntity.shouldRemove() || itemEntity.isRemoveScheduled()) - continue; - PickupItemEvent pickupItemEvent = new PickupItemEvent(this, itemEntity); - EventDispatcher.callCancellable(pickupItemEvent, () -> { - final ItemStack item = itemEntity.getItemStack(); - sendPacketToViewersAndSelf(new CollectItemPacket(itemEntity.getEntityId(), getEntityId(), item.getAmount())); - entity.remove(); - }); - } - } - } + this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.getWidth(), + EntityTracker.Target.ITEMS, itemEntity -> { + if (this instanceof Player player && !itemEntity.isViewer(player)) return; + if (!itemEntity.isPickable()) return; + final BoundingBox itemBoundingBox = itemEntity.getBoundingBox(); + if (expandedBoundingBox.intersect(itemBoundingBox)) { + if (itemEntity.shouldRemove() || itemEntity.isRemoveScheduled()) return; + PickupItemEvent pickupItemEvent = new PickupItemEvent(this, itemEntity); + EventDispatcher.callCancellable(pickupItemEvent, () -> { + final ItemStack item = itemEntity.getItemStack(); + sendPacketToViewersAndSelf(new CollectItemPacket(itemEntity.getEntityId(), getEntityId(), item.getAmount())); + itemEntity.remove(); + }); + } + }); } } @@ -528,17 +517,11 @@ public class LivingEntity extends Entity implements EquipmentHandler { } @Override - protected boolean addViewer0(@NotNull Player player) { - if (!super.addViewer0(player)) { - return false; - } - final PlayerConnection playerConnection = player.getPlayerConnection(); - playerConnection.sendPacket(getEquipmentsPacket()); - playerConnection.sendPacket(getPropertiesPacket()); - if (getTeam() != null) { - playerConnection.sendPacket(getTeam().createTeamsCreationPacket()); - } - return true; + public void updateNewViewer(@NotNull Player player) { + super.updateNewViewer(player); + player.sendPacket(getEquipmentsPacket()); + player.sendPacket(getPropertiesPacket()); + if (getTeam() != null) player.sendPacket(getTeam().createTeamsCreationPacket()); } @Override diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index c49dc3267..2c8556024 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -41,6 +41,7 @@ import net.minestom.server.event.item.ItemUpdateStateEvent; import net.minestom.server.event.item.PickupExperienceEvent; import net.minestom.server.event.player.*; import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.PlayerInventory; @@ -67,13 +68,12 @@ import net.minestom.server.resourcepack.ResourcePack; import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Team; import net.minestom.server.statistic.PlayerStatistic; -import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.TickUtils; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.chunk.ChunkUtils; -import net.minestom.server.utils.entity.EntityUtils; +import net.minestom.server.utils.function.IntegerBiConsumer; import net.minestom.server.utils.identity.NamedAndIdentified; import net.minestom.server.utils.instance.InstanceUtils; import net.minestom.server.utils.inventory.PlayerInventoryUtils; @@ -89,9 +89,9 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.function.UnaryOperator; /** @@ -110,8 +110,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, private String username; private Component usernameComponent; protected final PlayerConnection playerConnection; - // All the entities that this player can see - protected final Set viewableEntities = ConcurrentHashMap.newKeySet(); private int latency; private Component displayName; @@ -119,8 +117,29 @@ public class Player extends LivingEntity implements CommandSender, Localizable, private DimensionType dimensionType; private GameMode gameMode; - // Chunks that the player can view - protected final Set viewableChunks = ConcurrentHashMap.newKeySet(); + final IntegerBiConsumer chunkAdder = (chunkX, chunkZ) -> { + // Load new chunks + this.instance.loadOptionalChunk(chunkX, chunkZ).thenAccept(chunk -> { + try { + if (chunk != null) { + chunk.sendChunk(this); + GlobalHandles.PLAYER_CHUNK_LOAD.call(new PlayerChunkLoadEvent(this, chunkX, chunkZ)); + } + } catch (Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + } + }); + }; + final IntegerBiConsumer chunkRemover = (chunkX, chunkZ) -> { + // Unload old chunks + final Instance instance = this.instance; + if (instance == null) return; + final Chunk chunk = instance.getChunk(chunkX, chunkZ); + if (chunk != null) { + sendPacket(new UnloadChunkPacket(chunkX, chunkZ)); + EventDispatcher.call(new PlayerChunkUnloadEvent(this, chunkX, chunkZ)); + } + }; private final AtomicInteger teleportId = new AtomicInteger(); private int receivedTeleportId; @@ -312,23 +331,19 @@ public class Player extends LivingEntity implements CommandSender, Localizable, // Experience orb pickup if (experiencePickupCooldown.isReady(time)) { experiencePickupCooldown.refreshLastUpdate(time); - final Chunk chunk = getChunk(); // TODO check surrounding chunks - final Set entities = instance.getChunkEntities(chunk); - for (Entity entity : entities) { - if (entity instanceof ExperienceOrb) { - final ExperienceOrb experienceOrb = (ExperienceOrb) entity; - final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox(); - if (expandedBoundingBox.intersect(itemBoundingBox)) { - if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled()) - continue; - PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(this, experienceOrb); - EventDispatcher.callCancellable(pickupExperienceEvent, () -> { - short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player - entity.remove(); - }); - } - } - } + this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.getWidth(), + EntityTracker.Target.EXPERIENCE_ORBS, experienceOrb -> { + final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox(); + if (expandedBoundingBox.intersect(itemBoundingBox)) { + if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled()) + return; + PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(this, experienceOrb); + EventDispatcher.callCancellable(pickupExperienceEvent, () -> { + short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player + experienceOrb.remove(); + }); + } + }); } // Eating animation @@ -464,10 +479,14 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } } } + final Pos position = this.position; + final int chunkX = position.chunkX(); + final int chunkZ = position.chunkZ(); // Clear all viewable entities - this.viewableEntities.forEach(entity -> entity.removeViewer(this)); + this.instance.getEntityTracker().visibleEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES, + trackingUpdate::remove); // Clear all viewable chunks - this.viewableChunks.forEach(chunk -> chunk.removeViewer(this)); + ChunkUtils.forChunksInRange(chunkX, chunkZ, MinecraftServer.getChunkViewDistance(), chunkRemover); // Remove from the tab-list PacketUtils.broadcastPacket(getRemovePlayerToList()); @@ -477,15 +496,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } @Override - protected boolean removeViewer0(@NotNull Player player) { - if (player == this || !super.removeViewer0(player)) { - return false; - } + public void updateOldViewer(@NotNull Player player) { + super.updateOldViewer(player); // Team if (this.getTeam() != null && this.getTeam().getMembers().size() == 1) {// If team only contains "this" player - player.getPlayerConnection().sendPacket(this.getTeam().createTeamDestructionPacket()); + player.sendPacket(this.getTeam().createTeamDestructionPacket()); } - return true; } @Override @@ -514,18 +530,37 @@ public class Player extends LivingEntity implements CommandSender, Localizable, public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { final Instance currentInstance = this.instance; Check.argCondition(currentInstance == instance, "Instance should be different than the current one"); - // true if the chunks need to be sent to the client, can be false if the instances share the same chunks (e.g. SharedInstance) - if (!InstanceUtils.areLinked(currentInstance, instance) || !spawnPosition.sameChunk(this.position)) { - final boolean firstSpawn = currentInstance == null; - return instance.loadOptionalChunk(spawnPosition) - .thenRun(() -> spawnPlayer(instance, spawnPosition, firstSpawn, - !Objects.equals(dimensionType, instance.getDimensionType()), true)); - } else { + if (InstanceUtils.areLinked(currentInstance, instance) && spawnPosition.sameChunk(this.position)) { // The player already has the good version of all the chunks. // We just need to refresh his entity viewing list and add him to the instance - return AsyncUtils.VOID_FUTURE - .thenRun(() -> spawnPlayer(instance, spawnPosition, false, false, false)); + spawnPlayer(instance, spawnPosition, null, false, false, false); + return AsyncUtils.VOID_FUTURE; } + // Must update the player chunks + final boolean dimensionChange = !Objects.equals(dimensionType, instance.getDimensionType()); + final Thread runThread = Thread.currentThread(); + final Consumer runnable = (i) -> spawnPlayer(i, spawnPosition, + currentInstance, + currentInstance == null, dimensionChange, true); + // Wait for all surrounding chunks to load + List> futures = new ArrayList<>(); + ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(), + (chunkX, chunkZ) -> futures.add(instance.loadOptionalChunk(chunkX, chunkZ))); + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) + .thenCompose(unused -> { + if (runThread == Thread.currentThread()) { + runnable.accept(instance); + return AsyncUtils.VOID_FUTURE; + } else { + // Complete the future during the next instance tick + CompletableFuture future = new CompletableFuture<>(); + instance.scheduleNextTick(i -> { + runnable.accept(i); + future.complete(null); + }); + return future; + } + }); } /** @@ -549,33 +584,33 @@ public class Player extends LivingEntity implements CommandSender, Localizable, *

* UNSAFE: only called with {@link #setInstance(Instance, Pos)}. * - * @param spawnPosition the position to teleport the player - * @param firstSpawn true if this is the player first spawn - * @param updateChunks true if chunks should be refreshed, false if the new instance shares the same - * chunks + * @param spawnPosition the position to teleport the player + * @param previousInstance the previous player instance, null if first spawn + * @param firstSpawn true if this is the player first spawn + * @param updateChunks true if chunks should be refreshed, false if the new instance shares the same + * chunks */ private void spawnPlayer(@NotNull Instance instance, @NotNull Pos spawnPosition, + @Nullable Instance previousInstance, boolean firstSpawn, boolean dimensionChange, boolean updateChunks) { - final Set previousChunks = Set.copyOf(viewableChunks); if (!firstSpawn) { // Player instance changed, clear current viewable collections - previousChunks.forEach(chunk -> chunk.removeViewer(this)); + if (updateChunks) + ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(), chunkRemover); - //TODO: entity#removeViewer sends a packet for each removed entity - //Sending destroy entity packets is not necessary when the dimension changes - //and, potentially, this could also be rewritten to send only a single DestroyEntitiesPacket - //with the list of all destroyed entities - this.viewableEntities.forEach(entity -> entity.removeViewer(this)); + } + if (previousInstance != null) { + previousInstance.getEntityTracker().visibleEntities(position, + EntityTracker.Target.ENTITIES, trackingUpdate::remove); } - if (dimensionChange) { - sendDimension(instance.getDimensionType()); - } + if (dimensionChange) sendDimension(instance.getDimensionType()); super.setInstance(instance, spawnPosition); if (updateChunks) { - refreshVisibleChunks(); + sendPacket(new UpdateViewPositionPacket(spawnPosition.chunkX(), spawnPosition.chunkZ())); + ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(), chunkAdder); } synchronizePosition(true); // So the player doesn't get stuck @@ -584,10 +619,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.inventory.update(); } - PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn); - EventDispatcher.call(spawnEvent); - - this.playerConnection.flush(); + EventDispatcher.call(new PlayerSpawnEvent(this, instance, firstSpawn)); } /** @@ -1085,13 +1117,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, sendPacketToViewersAndSelf(getEquipmentsPacket()); getInventory().update(); - - { - // Send new chunks - final Chunk chunk = instance.getChunkAt(position); - Check.notNull(chunk, "Tried to interact with an unloaded chunk."); - refreshVisibleChunks(chunk); - } } /** @@ -1146,94 +1171,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.playerConnection.sendPacket(new SetExperiencePacket(exp, level, 0)); } - /** - * Called when the player changes chunk (move from one to another). - * Can also be used to refresh the list of chunks that the client should see based on {@link #getChunkRange()}. - *

- * It does remove and add the player from the chunks viewers list when removed or added. - * It also calls the events {@link PlayerChunkUnloadEvent} and {@link PlayerChunkLoadEvent}. - * - * @param newChunk the current/new player chunk (can be the current one) - */ - public void refreshVisibleChunks(@NotNull Chunk newChunk) { - final int newChunkX = newChunk.getChunkX(); - final int newChunkZ = newChunk.getChunkZ(); - final int range = getChunkRange(); - // Previous chunks indexes - final long[] lastVisibleChunks = viewableChunks.stream().mapToLong(ChunkUtils::getChunkIndex).toArray(); - // New chunks indexes - final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunkX, newChunkZ, range); - - // Update client render distance - updateViewPosition(newChunkX, newChunkZ); - - // Unload old chunks - ArrayUtils.forDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks, chunkIndex -> { - final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex); - final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex); - this.playerConnection.sendPacket(new UnloadChunkPacket(chunkX, chunkZ)); - final Chunk chunk = instance.getChunk(chunkX, chunkZ); - if (chunk != null) { - chunk.removeViewer(this); - } - }); - // Load new chunks - ArrayUtils.forDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks, chunkIndex -> { - final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex); - final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex); - this.instance.loadOptionalChunk(chunkX, chunkZ).thenAccept(chunk -> { - if (chunk == null) { - // Cannot load chunk (auto-load is not enabled) - return; - } - try { - chunk.addViewer(this); - } catch (Exception e) { - MinecraftServer.getExceptionManager().handleException(e); - } - }); - }); - } - - public void refreshVisibleChunks() { - final Chunk chunk = getChunk(); - if (chunk != null) { - refreshVisibleChunks(chunk); - } - } - - /** - * Refreshes the list of entities that the player should be able to see based - * on {@link MinecraftServer#getEntityViewDistance()} and {@link Entity#isAutoViewable()}. - * - * @param newChunk the new chunk of the player (can be the current one) - */ - public void refreshVisibleEntities(@NotNull Chunk newChunk) { - final int entityViewDistance = MinecraftServer.getEntityViewDistance(); - final float maximalDistance = entityViewDistance * Chunk.CHUNK_SECTION_SIZE; - // Manage already viewable entities - this.viewableEntities.stream() - .filter(entity -> entity.getDistance(this) > maximalDistance) - .forEach(entity -> { - // Entity shouldn't be viewable anymore - if (isAutoViewable()) { - entity.removeViewer(this); - } - if (entity instanceof Player && entity.isAutoViewable()) { - removeViewer((Player) entity); - } - }); - // Manage entities in unchecked chunks - EntityUtils.forEachRange(instance, newChunk.toPosition(), entityViewDistance, entity -> { - if (entity.isAutoViewable() && !entity.viewers.contains(this)) { - entity.addViewer(this); - } - if (entity instanceof Player && isAutoViewable() && !viewers.contains(entity)) { - addViewer((Player) entity); - } - }); - } - /** * Gets the player connection. *

@@ -1528,28 +1465,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.didCloseInventory = didCloseInventory; } - /** - * Gets the player viewable chunks. - *

- * WARNING: adding or removing a chunk there will not load/unload it, - * use {@link Chunk#addViewer(Player)} or {@link Chunk#removeViewer(Player)}. - * - * @return a {@link Set} containing all the chunks that the player sees - */ - public Set getViewableChunks() { - return viewableChunks; - } - - /** - * Sends a {@link UpdateViewPositionPacket} to the player. - * - * @param chunkX the chunk X - * @param chunkZ the chunk Z - */ - public void updateViewPosition(int chunkX, int chunkZ) { - playerConnection.sendPacket(new UpdateViewPositionPacket(chunkX, chunkZ)); - } - public int getNextTeleportId() { return teleportId.incrementAndGet(); } @@ -1916,15 +1831,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.vehicleInformation.refresh(sideways, forward, jump, unmount); } - /** - * @return the chunk range of the viewers, - * which is {@link MinecraftServer#getChunkViewDistance()} or {@link PlayerSettings#getViewDistance()} - * based on which one is the lowest - */ - public int getChunkRange() { - return Math.min(getSettings().viewDistance, MinecraftServer.getChunkViewDistance()); - } - /** * Gets the last sent keep alive id. * @@ -2201,9 +2107,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, */ public void refresh(String locale, byte viewDistance, ChatMessageType chatMessageType, boolean chatColors, byte displayedSkinParts, MainHand mainHand) { - - final boolean viewDistanceChanged = this.viewDistance != viewDistance; - this.locale = locale; this.viewDistance = viewDistance; this.chatMessageType = chatMessageType; @@ -2213,11 +2116,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, // TODO: Use the metadata object here metadata.setIndex((byte) 17, Metadata.Byte(displayedSkinParts)); - - // Client changed his view distance in the settings - if (viewDistanceChanged) { - refreshVisibleChunks(); - } } } diff --git a/src/main/java/net/minestom/server/entity/fakeplayer/FakePlayer.java b/src/main/java/net/minestom/server/entity/fakeplayer/FakePlayer.java index a668c0cb7..0e8b3de1b 100644 --- a/src/main/java/net/minestom/server/entity/fakeplayer/FakePlayer.java +++ b/src/main/java/net/minestom/server/entity/fakeplayer/FakePlayer.java @@ -129,14 +129,10 @@ public class FakePlayer extends Player implements NavigableEntity { } @Override - protected boolean addViewer0(@NotNull Player player) { - if (viewers.contains(player)) { - return false; - } - + public void updateNewViewer(@NotNull Player player) { player.getPlayerConnection().sendPacket(getAddPlayerToList()); handleTabList(player.getPlayerConnection()); - return super.addViewer0(player); + super.updateNewViewer(player); } /** diff --git a/src/main/java/net/minestom/server/event/instance/RemoveEntityFromInstanceEvent.java b/src/main/java/net/minestom/server/event/instance/RemoveEntityFromInstanceEvent.java index 134dc2d62..8e4ce98bf 100644 --- a/src/main/java/net/minestom/server/event/instance/RemoveEntityFromInstanceEvent.java +++ b/src/main/java/net/minestom/server/event/instance/RemoveEntityFromInstanceEvent.java @@ -1,22 +1,17 @@ package net.minestom.server.event.instance; import net.minestom.server.entity.Entity; -import net.minestom.server.event.trait.CancellableEvent; -import net.minestom.server.event.trait.EntityEvent; -import net.minestom.server.event.trait.InstanceEvent; +import net.minestom.server.event.trait.EntityInstanceEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; /** * Called by an Instance when an entity is removed from it. */ -public class RemoveEntityFromInstanceEvent implements InstanceEvent, EntityEvent, CancellableEvent { - +public class RemoveEntityFromInstanceEvent implements EntityInstanceEvent { private final Instance instance; private final Entity entity; - private boolean cancelled; - public RemoveEntityFromInstanceEvent(@NotNull Instance instance, @NotNull Entity entity) { this.instance = instance; this.entity = entity; @@ -32,18 +27,7 @@ public class RemoveEntityFromInstanceEvent implements InstanceEvent, EntityEvent * * @return entity being removed */ - @NotNull - public Entity getEntity() { + public @NotNull Entity getEntity() { return entity; } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public void setCancelled(boolean cancel) { - this.cancelled = cancel; - } } diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index 8fb828abc..fbb91098c 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -6,27 +6,22 @@ import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; import net.minestom.server.entity.pathfinding.PFColumnarSpace; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.GlobalHandles; -import net.minestom.server.event.player.PlayerChunkLoadEvent; -import net.minestom.server.event.player.PlayerChunkUnloadEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockGetter; import net.minestom.server.instance.block.BlockSetter; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; +import net.minestom.server.utils.ViewEngine; import net.minestom.server.utils.chunk.ChunkSupplier; import net.minestom.server.world.biomes.Biome; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jglrxavpok.hephaistos.nbt.NBTCompound; -import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; // TODO light data & API @@ -58,8 +53,7 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka private boolean readOnly; protected volatile boolean loaded = true; - protected final Set viewers = ConcurrentHashMap.newKeySet(); - private final Set unmodifiableViewers = Collections.unmodifiableSet(viewers); + private final ViewEngine viewers = new ViewEngine(); // Path finding protected PFColumnarSpace columnarSpace; @@ -80,6 +74,9 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka } else { this.biomes = new Biome[biomeCount]; } + + final EntityTracker tracker = instance.getEntityTracker(); + this.viewers.updateTracker(toPosition(), tracker); } /** @@ -262,56 +259,30 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka } /** - * Sends the chunk to {@code player} and add it to the player viewing chunks collection - * and send a {@link PlayerChunkLoadEvent}. + * Adds the player to the viewing collection. Chunk packet must be sent manually. * * @param player the viewer to add * @return true if the player has just been added to the viewer collection */ @Override public boolean addViewer(@NotNull Player player) { - final boolean result = this.viewers.add(player); - - // Add to the viewable chunks set - player.getViewableChunks().add(this); - - // Send the chunk data & light packets to the player - sendChunk(player); - - if (result) { - PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ); - GlobalHandles.PLAYER_CHUNK_LOAD.call(playerChunkLoadEvent); - } - - return result; + return viewers.manualAdd(player); } /** - * Removes the chunk to the player viewing chunks collection - * and send a {@link PlayerChunkUnloadEvent}. + * Removes the player from the viewing collection. Chunk packet must be sent manually. * * @param player the viewer to remove * @return true if the player has just been removed to the viewer collection */ @Override public boolean removeViewer(@NotNull Player player) { - final boolean result = this.viewers.remove(player); - - // Remove from the viewable chunks set - player.getViewableChunks().remove(this); - - if (result) { - PlayerChunkUnloadEvent playerChunkUnloadEvent = new PlayerChunkUnloadEvent(player, chunkX, chunkZ); - EventDispatcher.call(playerChunkUnloadEvent); - } - - return result; + return viewers.manualRemove(player); } - @NotNull @Override - public Set getViewers() { - return unmodifiableViewers; + public @NotNull Set getViewers() { + return viewers.asSet(); } @Override diff --git a/src/main/java/net/minestom/server/instance/EntityTracker.java b/src/main/java/net/minestom/server/instance/EntityTracker.java new file mode 100644 index 000000000..08a9d296c --- /dev/null +++ b/src/main/java/net/minestom/server/instance/EntityTracker.java @@ -0,0 +1,164 @@ +package net.minestom.server.instance; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.ExperienceOrb; +import net.minestom.server.entity.ItemEntity; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.List; +import java.util.Set; + +/** + * Defines how {@link Entity entities} are tracked within an {@link Instance instance}. + *

+ * Implementations are expected to be thread-safe. + */ +@ApiStatus.Experimental +public sealed interface EntityTracker permits EntityTrackerImpl { + + /** + * Register an entity to be tracked. + */ + void register(@NotNull Entity entity, @NotNull Point point, + @NotNull Target target, @Nullable Update update); + + /** + * Unregister an entity tracking. + */ + void unregister(@NotNull Entity entity, @NotNull Point point, + @NotNull Target target, @Nullable Update update); + + /** + * Called every time an entity move, you may want to verify if the new + * position is in a different chunk. + */ + void move(@NotNull Entity entity, + @NotNull Point oldPoint, @NotNull Point newPoint, + @NotNull Target target, @Nullable Update update); + + /** + * Gets the entities newly visible and invisible from one chunk to another. + */ + void difference(int oldChunkX, int oldChunkZ, + int newChunkX, int newChunkZ, + @NotNull Target target, @NotNull Update update); + + default void difference(@NotNull Point from, @NotNull Point to, + @NotNull Target target, @NotNull Update update) { + difference(from.chunkX(), from.chunkZ(), to.chunkX(), to.chunkZ(), target, update); + } + + /** + * Gets the entities present in the specified chunk. + */ + void chunkEntities(int chunkX, int chunkZ, + @NotNull Target target, @NotNull Query query); + + default void chunkEntities(@NotNull Point point, + @NotNull Target target, @NotNull Query query) { + chunkEntities(point.chunkX(), point.chunkZ(), target, query); + } + + /** + * Gets the entities present in range of the specified chunk. + *

+ * This is used for auto-viewable features. + */ + void visibleEntities(int chunkX, int chunkZ, + @NotNull Target target, @NotNull Query query); + + default void visibleEntities(@NotNull Point point, + @NotNull Target target, @NotNull Query query) { + visibleEntities(point.chunkX(), point.chunkZ(), target, query); + } + + /** + * Gets a list containing references to all the entity lists visible from a chunk + */ + @NotNull List> references(int chunkX, int chunkZ, @NotNull Target target); + + default @NotNull List> references(@NotNull Point point, @NotNull Target target) { + return references(point.chunkX(), point.chunkZ(), target); + } + + /** + * Gets the entities within a range. + */ + void nearbyEntities(@NotNull Point point, double range, + @NotNull Target target, @NotNull Query query); + + /** + * Gets all the entities tracked by this class. + */ + @UnmodifiableView + @NotNull Set<@NotNull T> entities(@NotNull Target target); + + @UnmodifiableView + default @NotNull Set<@NotNull Entity> entities() { + return entities(Target.ENTITIES); + } + + /** + * Run {@code runnable} and ensure that the tracking state is locked during execution. + */ + void synchronize(@NotNull Point point, @NotNull Runnable runnable); + + /** + * Represents the type of entity you want to retrieve. + * + * @param the entity type + */ + @ApiStatus.NonExtendable + interface Target { + Target ENTITIES = create(Entity.class); + Target PLAYERS = create(Player.class); + Target ITEMS = create(ItemEntity.class); + Target EXPERIENCE_ORBS = create(ExperienceOrb.class); + + List> TARGETS = List.of(EntityTracker.Target.ENTITIES, EntityTracker.Target.PLAYERS, EntityTracker.Target.ITEMS, EntityTracker.Target.EXPERIENCE_ORBS); + + Class type(); + + int ordinal(); + + private static EntityTracker.Target create(Class type) { + final int ordinal = EntityTrackerImpl.TARGET_COUNTER.getAndIncrement(); + return new Target<>() { + @Override + public Class type() { + return type; + } + + @Override + public int ordinal() { + return ordinal; + } + }; + } + } + + /** + * Callback to know the newly visible entities and those to remove. + */ + interface Update { + void add(@NotNull E entity); + + void remove(@NotNull E entity); + + void updateTracker(@NotNull Point point, @Nullable EntityTracker tracker); + } + + /** + * Query entities. + *

+ * This is not a functional interface, we reserve the right to add other methods. + */ + interface Query { + void consume(E entity); + } +} diff --git a/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java b/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java new file mode 100644 index 000000000..fda24a12e --- /dev/null +++ b/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java @@ -0,0 +1,181 @@ +package net.minestom.server.instance; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.utils.chunk.ChunkUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.LongFunction; + +import static net.minestom.server.utils.chunk.ChunkUtils.forDifferingChunksInRange; +import static net.minestom.server.utils.chunk.ChunkUtils.getChunkIndex; + +final class EntityTrackerImpl implements EntityTracker { + static final AtomicInteger TARGET_COUNTER = new AtomicInteger(); + private static final LongFunction> LIST_SUPPLIER = l -> new CopyOnWriteArrayList<>(); + + // Store all data associated to a Target + // The array index is the Target enum ordinal + private final TargetEntry[] entries = EntityTracker.Target.TARGETS.stream().map((Function, TargetEntry>) TargetEntry::new).toArray(TargetEntry[]::new); + + @Override + public synchronized void register(@NotNull Entity entity, @NotNull Point point, + @NotNull Target target, @Nullable Update update) { + final long index = getChunkIndex(point); + for (TargetEntry entry : entries) { + if (entry.target.type().isInstance(entity)) { + entry.entities.add(entity); + entry.addToChunk(index, entity); + } + } + if (update != null) { + visibleEntities(point, target, update::add); + update.updateTracker(point, this); + } + } + + @Override + public synchronized void unregister(@NotNull Entity entity, @NotNull Point point, + @NotNull Target target, @Nullable Update update) { + final long index = getChunkIndex(point); + for (TargetEntry entry : entries) { + if (entry.target.type().isInstance(entity)) { + entry.entities.remove(entity); + entry.removeFromChunk(index, entity); + } + } + if (update != null) { + visibleEntities(point, target, update::remove); + update.updateTracker(point, null); + } + } + + @Override + public void move(@NotNull Entity entity, + @NotNull Point oldPoint, @NotNull Point newPoint, + @NotNull Target target, @Nullable Update update) { + if (oldPoint.sameChunk(newPoint)) return; + synchronized (this) { + final long oldIndex = getChunkIndex(oldPoint); + final long newIndex = getChunkIndex(newPoint); + for (TargetEntry entry : entries) { + if (entry.target.type().isInstance(entity)) { + entry.addToChunk(newIndex, entity); + entry.removeFromChunk(oldIndex, entity); + } + } + if (update != null) { + difference(oldPoint, newPoint, target, update); + update.updateTracker(newPoint, this); + } + } + } + + @Override + public synchronized void difference(int oldChunkX, int oldChunkZ, + int newChunkX, int newChunkZ, + @NotNull Target target, @NotNull Update update) { + final TargetEntry entry = entries[target.ordinal()]; + forDifferingChunksInRange(newChunkX, newChunkZ, oldChunkX, oldChunkZ, + MinecraftServer.getEntityViewDistance(), (chunkX, chunkZ) -> { + // Add + final List entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ)); + if (entities == null || entities.isEmpty()) return; + for (Entity entity : entities) update.add((T) entity); + }, (chunkX, chunkZ) -> { + // Remove + final List entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ)); + if (entities == null || entities.isEmpty()) return; + for (Entity entity : entities) update.remove((T) entity); + }); + } + + @Override + public synchronized void chunkEntities(int chunkX, int chunkZ, @NotNull Target target, @NotNull Query query) { + final TargetEntry entry = entries[target.ordinal()]; + final List entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ)); + if (entities == null || entities.isEmpty()) return; + for (Entity entity : entities) query.consume((T) entity); + } + + @Override + public void visibleEntities(int chunkX, int chunkZ, @NotNull Target target, @NotNull Query query) { + for (List entities : references(chunkX, chunkZ, target)) { + if (entities.isEmpty()) continue; + for (Entity entity : entities) query.consume((T) entity); + } + } + + @Override + public synchronized @NotNull List> references(int chunkX, int chunkZ, @NotNull Target target) { + // Gets reference to all chunk entities lists within the range + // This is used to avoid a map lookup per chunk + final TargetEntry entry = (TargetEntry) entries[target.ordinal()]; + return entry.chunkRangeEntities.computeIfAbsent(ChunkUtils.getChunkIndex(chunkX, chunkZ), + chunkIndex -> { + List> entities = new ArrayList<>(); + ChunkUtils.forChunksInRange(ChunkUtils.getChunkCoordX(chunkIndex), ChunkUtils.getChunkCoordZ(chunkIndex), + MinecraftServer.getEntityViewDistance(), + (x, z) -> entities.add(entry.chunkEntities.computeIfAbsent(getChunkIndex(x, z), i -> (List) LIST_SUPPLIER.apply(i)))); + return List.copyOf(entities); + }); + } + + @Override + public synchronized void nearbyEntities(@NotNull Point point, double range, @NotNull Target target, @NotNull Query query) { + final int chunkRange = Math.abs((int) (range / Chunk.CHUNK_SECTION_SIZE)) + 1; + final double squaredRange = range * range; + ChunkUtils.forChunksInRange(point, chunkRange, (chunkX, chunkZ) -> + chunkEntities(chunkX, chunkZ, target, entity -> { + if (point.distanceSquared(entity.getPosition()) < squaredRange) { + query.consume(entity); + } + })); + } + + @Override + public @UnmodifiableView @NotNull Set<@NotNull T> entities(@NotNull Target target) { + return (Set) entries[target.ordinal()].entitiesView; + } + + @Override + public synchronized void synchronize(@NotNull Point point, @NotNull Runnable runnable) { + runnable.run(); + } + + private static final class TargetEntry { + private final EntityTracker.Target target; + private final Set entities = ConcurrentHashMap.newKeySet(); // Thread-safe since exposed + private final Set entitiesView = Collections.unmodifiableSet(entities); + // Chunk index -> entities inside it + private final Long2ObjectMap> chunkEntities = new Long2ObjectOpenHashMap<>(); + // Chunk index -> lists of visible entities (references to chunkEntities entries) + private final Long2ObjectMap>> chunkRangeEntities = new Long2ObjectOpenHashMap<>(); + + TargetEntry(Target target) { + this.target = target; + } + + void addToChunk(long index, T entity) { + this.chunkEntities.computeIfAbsent(index, i -> (List) LIST_SUPPLIER.apply(i)).add(entity); + } + + void removeFromChunk(long index, T entity) { + List entities = chunkEntities.get(index); + if (entities != null) entities.remove(entity); + } + } +} diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index 30ba6b107..74a8bc1af 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -1,7 +1,5 @@ package net.minestom.server.instance; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.pointer.Pointers; import net.minestom.server.MinecraftServer; @@ -9,18 +7,14 @@ import net.minestom.server.Tickable; import net.minestom.server.UpdateManager; import net.minestom.server.adventure.audience.PacketGroupingAudience; import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; import net.minestom.server.data.Data; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.ExperienceOrb; import net.minestom.server.entity.Player; import net.minestom.server.entity.pathfinding.PFInstanceSpace; -import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalHandles; -import net.minestom.server.event.instance.AddEntityToInstanceEvent; import net.minestom.server.event.instance.InstanceTickEvent; -import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent; import net.minestom.server.instance.block.*; import net.minestom.server.network.packet.server.play.BlockActionPacket; import net.minestom.server.network.packet.server.play.TimeUpdatePacket; @@ -28,7 +22,6 @@ import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.chunk.ChunkUtils; -import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; @@ -41,9 +34,9 @@ import org.jglrxavpok.hephaistos.nbt.NBTCompound; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * Instances are what are called "worlds" in Minecraft, you can add an entity in it using {@link Entity#setInstance(Instance)}. @@ -79,14 +72,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta // Field for tick events private long lastTickAge = System.currentTimeMillis(); - // Entities present in this instance - protected final Set entities = ConcurrentHashMap.newKeySet(); - protected final Set players = ConcurrentHashMap.newKeySet(); - protected final Set creatures = ConcurrentHashMap.newKeySet(); - protected final Set experienceOrbs = ConcurrentHashMap.newKeySet(); - // Entities per chunk - protected final Object entitiesLock = new Object(); // Lock used to prevent the entities Set and Map to be subject to race condition - protected final Long2ObjectMap> chunkEntities = new Long2ObjectOpenHashMap<>(); + private final EntityTracker entityTracker = new EntityTrackerImpl(); // the uuid of this instance protected UUID uniqueId; @@ -221,7 +207,6 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta public abstract @Nullable Chunk getChunk(int chunkX, int chunkZ); /** - * * @param chunkX the chunk X * @param chunkZ this chunk Z * @return true if the chunk is loaded @@ -231,7 +216,6 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta } /** - * * @param point coordinate of a block or other * @return true if the chunk is loaded */ @@ -427,7 +411,8 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta * * @return the {@link TimeUpdatePacket} with this instance data */ - private @NotNull TimeUpdatePacket createTimePacket() { + @ApiStatus.Internal + public @NotNull TimeUpdatePacket createTimePacket() { long time = this.time; if (timeRate == 0) { //Negative values stop the sun and moon from moving @@ -452,7 +437,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta * @return an unmodifiable {@link Set} containing all the entities in the instance */ public @NotNull Set<@NotNull Entity> getEntities() { - return Collections.unmodifiableSet(entities); + return entityTracker.entities(); } /** @@ -462,7 +447,7 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta */ @Override public @NotNull Set<@NotNull Player> getPlayers() { - return Collections.unmodifiableSet(players); + return entityTracker.entities(EntityTracker.Target.PLAYERS); } /** @@ -470,8 +455,12 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta * * @return an unmodifiable {@link Set} containing all the creatures in the instance */ + @Deprecated public @NotNull Set<@NotNull EntityCreature> getCreatures() { - return Collections.unmodifiableSet(creatures); + return entityTracker.entities().stream() + .filter(EntityCreature.class::isInstance) + .map(entity -> (EntityCreature) entity) + .collect(Collectors.toUnmodifiableSet()); } /** @@ -479,8 +468,12 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta * * @return an unmodifiable {@link Set} containing all the experience orbs in the instance */ + @Deprecated public @NotNull Set<@NotNull ExperienceOrb> getExperienceOrbs() { - return Collections.unmodifiableSet(experienceOrbs); + return entityTracker.entities().stream() + .filter(ExperienceOrb.class::isInstance) + .map(entity -> (ExperienceOrb) entity) + .collect(Collectors.toUnmodifiableSet()); } /** @@ -491,15 +484,9 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta * if {@code chunk} is unloaded, return an empty {@link HashSet} */ public @NotNull Set<@NotNull Entity> getChunkEntities(Chunk chunk) { - if (!ChunkUtils.isLoaded(chunk)) - return Collections.emptySet(); - final Set entities; - synchronized (entitiesLock) { - if ((entities = chunkEntities.get(ChunkUtils.getChunkIndex(chunk))) == null) { - return Collections.emptySet(); - } - } - return Collections.unmodifiableSet(entities); + Set result = new HashSet<>(); + this.entityTracker.chunkEntities(chunk.toPosition(), EntityTracker.Target.ENTITIES, result::add); + return result; } /** @@ -510,35 +497,8 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta * @return entities that are not further than the specified distance from the transmitted position. */ public @NotNull Collection getNearbyEntities(@NotNull Point point, double range) { - int minX = ChunkUtils.getChunkCoordinate(point.x() - range); - int maxX = ChunkUtils.getChunkCoordinate(point.x() + range); - int minZ = ChunkUtils.getChunkCoordinate(point.z() - range); - int maxZ = ChunkUtils.getChunkCoordinate(point.z() + range); - - // Cache squared range to prevent sqrt operations - double squaredRange = range * range; - List result = new ArrayList<>(); - synchronized (entitiesLock) { - for (int x = minX; x <= maxX; ++x) { - for (int z = minZ; z <= maxZ; ++z) { - Chunk chunk = getChunk(x, z); - - if (chunk == null) { - continue; - } - - Set chunkEntities = getChunkEntities(chunk); - - // Filter all entities out of range - for (Entity chunkEntity : chunkEntities) { - if (point.distanceSquared(chunkEntity.getPosition()) < squaredRange) { - result.add(chunkEntity); - } - } - } - } - } + this.entityTracker.nearbyEntities(point, range, EntityTracker.Target.ENTITIES, result::add); return result; } @@ -587,6 +547,11 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta return getChunk(point.chunkX(), point.chunkZ()); } + @ApiStatus.Experimental + public EntityTracker getEntityTracker() { + return entityTracker; + } + /** * Gets the instance unique id. * @@ -596,139 +561,6 @@ public abstract class Instance implements BlockGetter, BlockSetter, Tickable, Ta return uniqueId; } - // UNSAFE METHODS (need most of the time to be synchronized) - - /** - * Used when called {@link Entity#setInstance(Instance)}, it is used to refresh viewable chunks - * and add viewers if {@code entity} is a {@link Player}. - *

- * Warning: unsafe, you probably want to use {@link Entity#setInstance(Instance)} instead. - * - * @param entity the entity to add - */ - @ApiStatus.Internal - public void UNSAFE_addEntity(@NotNull Entity entity) { - final Instance lastInstance = entity.getInstance(); - if (lastInstance != null && lastInstance != this) { - lastInstance.UNSAFE_removeEntity(entity); // If entity is in another instance, remove it from there and add it to this - } - AddEntityToInstanceEvent event = new AddEntityToInstanceEvent(this, entity); - EventDispatcher.callCancellable(event, () -> { - final Pos entityPosition = entity.getPosition(); - final boolean isPlayer = entity instanceof Player; - - if (isPlayer) { - final Player player = (Player) entity; - getWorldBorder().init(player); - player.getPlayerConnection().sendPacket(createTimePacket()); - } - - // Send all visible entities - EntityUtils.forEachRange(this, entityPosition, MinecraftServer.getEntityViewDistance(), ent -> { - if (isPlayer) { - if (ent.isAutoViewable()) - ent.addViewer((Player) entity); - } - - if (ent instanceof Player) { - if (entity.isAutoViewable()) - entity.addViewer((Player) ent); - } - }); - - // Load the chunk if not already (or throw an error if auto chunk load is disabled) - loadOptionalChunk(entityPosition).thenAccept(chunk -> { - Check.notNull(chunk, "You tried to spawn an entity in an unloaded chunk, {0}", entityPosition); - UNSAFE_addEntityToChunk(entity, chunk); - }); - }); - } - - /** - * Used when an {@link Entity} is removed from the instance, it removes all of his viewers. - *

- * Warning: unsafe, you probably want to set the entity to another instance. - * - * @param entity the entity to remove - */ - @ApiStatus.Internal - public void UNSAFE_removeEntity(@NotNull Entity entity) { - if (entity.getInstance() != this) return; - RemoveEntityFromInstanceEvent event = new RemoveEntityFromInstanceEvent(this, entity); - EventDispatcher.callCancellable(event, () -> { - // Remove this entity from players viewable list and send delete entities packet - entity.getViewers().forEach(entity::removeViewer); - - // Remove the entity from cache - final Chunk chunk = getChunkAt(entity.getPosition()); - Check.notNull(chunk, "Tried to interact with an unloaded chunk."); - UNSAFE_removeEntityFromChunk(entity, chunk); - }); - } - - /** - * Changes an entity chunk. - * - * @param entity the entity to change its chunk - * @param lastChunk the last entity chunk - * @param newChunk the new entity chunk - */ - @ApiStatus.Internal - public synchronized void UNSAFE_switchEntityChunk(@NotNull Entity entity, @NotNull Chunk lastChunk, @NotNull Chunk newChunk) { - Check.notNull(newChunk, "The chunk {0} is not loaded, you can make it automatic by using Instance#enableAutoChunkLoad(true)", newChunk); - Check.argCondition(!newChunk.isLoaded(), "Chunk {0} has been unloaded previously", newChunk); - final long oldIndex = ChunkUtils.getChunkIndex(lastChunk); - final long newIndex = ChunkUtils.getChunkIndex(newChunk); - synchronized (entitiesLock) { - removeEntityChunk(oldIndex, entity); - addEntityChunk(newIndex, entity); - } - } - - private void UNSAFE_addEntityToChunk(@NotNull Entity entity, @NotNull Chunk chunk) { - final long chunkIndex = ChunkUtils.getChunkIndex(chunk); - synchronized (entitiesLock) { - addEntityChunk(chunkIndex, entity); - this.entities.add(entity); - if (entity instanceof Player) { - this.players.add((Player) entity); - } else if (entity instanceof EntityCreature) { - this.creatures.add((EntityCreature) entity); - } else if (entity instanceof ExperienceOrb) { - this.experienceOrbs.add((ExperienceOrb) entity); - } - } - } - - private void UNSAFE_removeEntityFromChunk(@NotNull Entity entity, @NotNull Chunk chunk) { - final long chunkIndex = ChunkUtils.getChunkIndex(chunk); - synchronized (entitiesLock) { - removeEntityChunk(chunkIndex, entity); - this.entities.remove(entity); - if (entity instanceof Player) { - this.players.remove(entity); - } else if (entity instanceof EntityCreature) { - this.creatures.remove(entity); - } else if (entity instanceof ExperienceOrb) { - this.experienceOrbs.remove(entity); - } - } - } - - private void addEntityChunk(long index, Entity entity) { - this.chunkEntities.computeIfAbsent(index, i -> ConcurrentHashMap.newKeySet()).add(entity); - } - - private void removeEntityChunk(long index, Entity entity) { - var chunkEntities = this.chunkEntities.get(index); - if (chunkEntities != null) { - chunkEntities.remove(entity); - if (chunkEntities.isEmpty()) { - this.chunkEntities.remove(index); - } - } - } - /** * Performs a single tick in the instance, including scheduled tasks from {@link #scheduleNextTick(Consumer)}. *

diff --git a/src/main/java/net/minestom/server/instance/InstanceContainer.java b/src/main/java/net/minestom/server/instance/InstanceContainer.java index ecde56b08..fd8baf81b 100644 --- a/src/main/java/net/minestom/server/instance/InstanceContainer.java +++ b/src/main/java/net/minestom/server/instance/InstanceContainer.java @@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalHandles; @@ -219,16 +220,11 @@ public class InstanceContainer extends Instance { EventDispatcher.call(new InstanceChunkUnloadEvent(this, chunk)); // Remove all entities in chunk - getChunkEntities(chunk).forEach(entity -> { - if (!(entity instanceof Player)) entity.remove(); - }); + getEntityTracker().chunkEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES, Entity::remove); // Clear cache synchronized (chunks) { this.chunks.remove(index); } - synchronized (entitiesLock) { - this.chunkEntities.remove(index); - } chunk.unload(); UPDATE_MANAGER.signalChunkUnload(chunk); } diff --git a/src/main/java/net/minestom/server/instance/WorldBorder.java b/src/main/java/net/minestom/server/instance/WorldBorder.java index 6ac106f31..4cb6e284d 100644 --- a/src/main/java/net/minestom/server/instance/WorldBorder.java +++ b/src/main/java/net/minestom/server/instance/WorldBorder.java @@ -6,6 +6,7 @@ import net.minestom.server.entity.Player; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.play.*; import net.minestom.server.utils.PacketUtils; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -226,7 +227,8 @@ public class WorldBorder { * * @param player the player to send the packet to */ - protected void init(@NotNull Player player) { + @ApiStatus.Internal + public void init(@NotNull Player player) { player.getPlayerConnection().sendPacket( InitializeWorldBorderPacket.of(centerX, centerZ, oldDiameter, newDiameter, speed, portalTeleportBoundary, warningTime, warningBlocks)); diff --git a/src/main/java/net/minestom/server/utils/PacketUtils.java b/src/main/java/net/minestom/server/utils/PacketUtils.java index df63ecf3e..ea45db110 100644 --- a/src/main/java/net/minestom/server/utils/PacketUtils.java +++ b/src/main/java/net/minestom/server/utils/PacketUtils.java @@ -153,7 +153,7 @@ public final class PacketUtils { @ApiStatus.Experimental public static void prepareViewablePacket(@NotNull Viewable viewable, @NotNull ServerPacket serverPacket, @Nullable Entity entity) { - if (entity != null && !entity.isAutoViewable()) { + if (entity != null && !entity.hasPredictableViewers()) { // Operation cannot be optimized entity.sendPacketToViewers(serverPacket); return; diff --git a/src/main/java/net/minestom/server/utils/ViewEngine.java b/src/main/java/net/minestom/server/utils/ViewEngine.java new file mode 100644 index 000000000..6de91f104 --- /dev/null +++ b/src/main/java/net/minestom/server/utils/ViewEngine.java @@ -0,0 +1,305 @@ +package net.minestom.server.utils; + +import com.zaxxer.sparsebits.SparseBitSet; +import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.EntityTracker; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Defines which players are able to see this element. + */ +@ApiStatus.Internal +public final class ViewEngine { + private final Entity entity; + private final ObjectArraySet manualViewers = new ObjectArraySet<>(); + + private EntityTracker tracker; + private Point lastTrackingPoint; + + // Decide if this entity should be viewable to X players + public final Option viewableOption; + // Decide if this entity should view X entities + public final Option viewerOption; + + private final Set set = new SetImpl(); + private final Object mutex = this; + + public ViewEngine(@Nullable Entity entity, + Consumer autoViewableAddition, Consumer autoViewableRemoval, + Consumer autoViewerAddition, Consumer autoViewerRemoval) { + this.entity = entity; + this.viewableOption = new Option<>(Entity::autoViewEntities, autoViewableAddition, autoViewableRemoval); + this.viewerOption = new Option<>(Entity::isAutoViewable, autoViewerAddition, autoViewerRemoval); + } + + public ViewEngine() { + this(null, null, null, null, null); + } + + public void updateTracker(@NotNull Point point, @Nullable EntityTracker tracker) { + synchronized (mutex) { + this.tracker = tracker; + this.lastTrackingPoint = point; + if (tracker != null) { + this.viewableOption.references = tracker.references(point, EntityTracker.Target.PLAYERS); + this.viewerOption.references = tracker.references(point, EntityTracker.Target.ENTITIES); + } else { + this.viewableOption.references = null; + this.viewerOption.references = null; + } + } + } + + public boolean manualAdd(@NotNull Player player) { + if (player == this.entity) return false; + synchronized (mutex) { + return !manualViewers.add(player); + } + } + + public boolean manualRemove(@NotNull Player player) { + if (player == this.entity) return false; + synchronized (mutex) { + return !manualViewers.remove(player); + } + } + + public boolean hasPredictableViewers() { + // Verify if this entity's viewers can be predicted from surrounding entities + synchronized (mutex) { + return viewableOption.isAuto() && manualViewers.isEmpty(); + } + } + + public void handleAutoViewAddition(Entity entity) { + handleAutoView(entity, viewerOption.addition, viewableOption.addition); + } + + public void handleAutoViewRemoval(Entity entity) { + handleAutoView(entity, viewerOption.removal, viewableOption.removal); + } + + private void handleAutoView(Entity entity, Consumer viewer, Consumer viewable) { + if (this.entity == entity) + return; // Ensure that self isn't added or removed as viewer + if (entity.getVehicle() != null) + return; // Passengers are handled by the vehicle, inheriting its viewing settings + if (this.entity instanceof Player && viewerOption.isAuto() && entity.isAutoViewable()) { + viewer.accept(entity); // Send packet to this player + } + if (entity instanceof Player player && player.autoViewEntities() && viewableOption.isAuto()) { + viewable.accept(player); // Send packet to the range-visible player + } + } + + private boolean validAutoViewer(Player player) { + return entity == null || viewableOption.isRegistered(player); + } + + public Set asSet() { + return set; + } + + public final class Option { + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater