Entity tracking rework (#486)

This commit is contained in:
TheMode 2021-11-01 18:04:00 +01:00 committed by GitHub
parent 0bcfc39a9d
commit faa289a097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1188 additions and 716 deletions

View File

@ -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'

View File

@ -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);
}
}
}
}
/**

View File

@ -37,8 +37,7 @@ public interface Viewable {
*
* @return A Set containing all the element's viewers
*/
@NotNull
Set<Player> getViewers();
@NotNull Set<@NotNull Player> getViewers();
/**
* Gets if a player is seeing this viewable object.

View File

@ -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<Player> viewers = ConcurrentHashMap.newKeySet();
private final Set<Player> 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<Entity> trackingTarget = this instanceof Player ?
EntityTracker.Target.ENTITIES : EntityTracker.Target.class.cast(EntityTracker.Target.PLAYERS);
protected final EntityTracker.Update<Entity> 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<Player> viewers = viewEngine.asSet();
private final NBTCompound nbtCompound = new NBTCompound();
private final Set<Permission> 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<Void> 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.
* <p>
* 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<Player> 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<Entity> 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<Entity> 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<Entity> 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<Entity> 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<Player> getViewers() {
return unmodifiableViewers;
public @NotNull Set<Player> 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<Player> 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<Entity> 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<Entity> 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);
}
/**

View File

@ -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<Entity> 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();
});
});
}
}
}
}

View File

@ -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<Entity> 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

View File

@ -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<Entity> 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<Chunk> 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<Entity> 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<Void> 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<Instance> runnable = (i) -> spawnPlayer(i, spawnPosition,
currentInstance,
currentInstance == null, dimensionChange, true);
// Wait for all surrounding chunks to load
List<CompletableFuture<Chunk>> 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<Void> 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,
* <p>
* 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<Chunk> 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()}.
* <p>
* 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.
* <p>
@ -1528,28 +1465,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
this.didCloseInventory = didCloseInventory;
}
/**
* Gets the player viewable chunks.
* <p>
* 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<Chunk> 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();
}
}
}

View File

@ -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);
}
/**

View File

@ -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;
}
}

View File

@ -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<Player> viewers = ConcurrentHashMap.newKeySet();
private final Set<Player> 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<Player> getViewers() {
return unmodifiableViewers;
public @NotNull Set<Player> getViewers() {
return viewers.asSet();
}
@Override

View File

@ -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}.
* <p>
* Implementations are expected to be thread-safe.
*/
@ApiStatus.Experimental
public sealed interface EntityTracker permits EntityTrackerImpl {
/**
* Register an entity to be tracked.
*/
<T extends Entity> void register(@NotNull Entity entity, @NotNull Point point,
@NotNull Target<T> target, @Nullable Update<T> update);
/**
* Unregister an entity tracking.
*/
<T extends Entity> void unregister(@NotNull Entity entity, @NotNull Point point,
@NotNull Target<T> target, @Nullable Update<T> update);
/**
* Called every time an entity move, you may want to verify if the new
* position is in a different chunk.
*/
<T extends Entity> void move(@NotNull Entity entity,
@NotNull Point oldPoint, @NotNull Point newPoint,
@NotNull Target<T> target, @Nullable Update<T> update);
/**
* Gets the entities newly visible and invisible from one chunk to another.
*/
<T extends Entity> void difference(int oldChunkX, int oldChunkZ,
int newChunkX, int newChunkZ,
@NotNull Target<T> target, @NotNull Update<T> update);
default <T extends Entity> void difference(@NotNull Point from, @NotNull Point to,
@NotNull Target<T> target, @NotNull Update<T> update) {
difference(from.chunkX(), from.chunkZ(), to.chunkX(), to.chunkZ(), target, update);
}
/**
* Gets the entities present in the specified chunk.
*/
<T extends Entity> void chunkEntities(int chunkX, int chunkZ,
@NotNull Target<T> target, @NotNull Query<T> query);
default <T extends Entity> void chunkEntities(@NotNull Point point,
@NotNull Target<T> target, @NotNull Query<T> query) {
chunkEntities(point.chunkX(), point.chunkZ(), target, query);
}
/**
* Gets the entities present in range of the specified chunk.
* <p>
* This is used for auto-viewable features.
*/
<T extends Entity> void visibleEntities(int chunkX, int chunkZ,
@NotNull Target<T> target, @NotNull Query<T> query);
default <T extends Entity> void visibleEntities(@NotNull Point point,
@NotNull Target<T> target, @NotNull Query<T> query) {
visibleEntities(point.chunkX(), point.chunkZ(), target, query);
}
/**
* Gets a list containing references to all the entity lists visible from a chunk
*/
<T extends Entity> @NotNull List<List<T>> references(int chunkX, int chunkZ, @NotNull Target<T> target);
default <T extends Entity> @NotNull List<List<T>> references(@NotNull Point point, @NotNull Target<T> target) {
return references(point.chunkX(), point.chunkZ(), target);
}
/**
* Gets the entities within a range.
*/
<T extends Entity> void nearbyEntities(@NotNull Point point, double range,
@NotNull Target<T> target, @NotNull Query<T> query);
/**
* Gets all the entities tracked by this class.
*/
@UnmodifiableView
@NotNull <T extends Entity> Set<@NotNull T> entities(@NotNull Target<T> 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 <E> the entity type
*/
@ApiStatus.NonExtendable
interface Target<E extends Entity> {
Target<Entity> ENTITIES = create(Entity.class);
Target<Player> PLAYERS = create(Player.class);
Target<ItemEntity> ITEMS = create(ItemEntity.class);
Target<ExperienceOrb> EXPERIENCE_ORBS = create(ExperienceOrb.class);
List<EntityTracker.Target<? extends Entity>> TARGETS = List.of(EntityTracker.Target.ENTITIES, EntityTracker.Target.PLAYERS, EntityTracker.Target.ITEMS, EntityTracker.Target.EXPERIENCE_ORBS);
Class<E> type();
int ordinal();
private static <T extends Entity> EntityTracker.Target<T> create(Class<T> type) {
final int ordinal = EntityTrackerImpl.TARGET_COUNTER.getAndIncrement();
return new Target<>() {
@Override
public Class<T> type() {
return type;
}
@Override
public int ordinal() {
return ordinal;
}
};
}
}
/**
* Callback to know the newly visible entities and those to remove.
*/
interface Update<E extends Entity> {
void add(@NotNull E entity);
void remove(@NotNull E entity);
void updateTracker(@NotNull Point point, @Nullable EntityTracker tracker);
}
/**
* Query entities.
* <p>
* This is not a functional interface, we reserve the right to add other methods.
*/
interface Query<E extends Entity> {
void consume(E entity);
}
}

View File

@ -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<Entity>> LIST_SUPPLIER = l -> new CopyOnWriteArrayList<>();
// Store all data associated to a Target
// The array index is the Target enum ordinal
private final TargetEntry<Entity>[] entries = EntityTracker.Target.TARGETS.stream().map((Function<Target<?>, TargetEntry>) TargetEntry::new).toArray(TargetEntry[]::new);
@Override
public synchronized <T extends Entity> void register(@NotNull Entity entity, @NotNull Point point,
@NotNull Target<T> target, @Nullable Update<T> update) {
final long index = getChunkIndex(point);
for (TargetEntry<Entity> 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 <T extends Entity> void unregister(@NotNull Entity entity, @NotNull Point point,
@NotNull Target<T> target, @Nullable Update<T> update) {
final long index = getChunkIndex(point);
for (TargetEntry<Entity> 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 <T extends Entity> void move(@NotNull Entity entity,
@NotNull Point oldPoint, @NotNull Point newPoint,
@NotNull Target<T> target, @Nullable Update<T> update) {
if (oldPoint.sameChunk(newPoint)) return;
synchronized (this) {
final long oldIndex = getChunkIndex(oldPoint);
final long newIndex = getChunkIndex(newPoint);
for (TargetEntry<Entity> 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 <T extends Entity> void difference(int oldChunkX, int oldChunkZ,
int newChunkX, int newChunkZ,
@NotNull Target<T> target, @NotNull Update<T> update) {
final TargetEntry<Entity> entry = entries[target.ordinal()];
forDifferingChunksInRange(newChunkX, newChunkZ, oldChunkX, oldChunkZ,
MinecraftServer.getEntityViewDistance(), (chunkX, chunkZ) -> {
// Add
final List<Entity> 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<Entity> entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ));
if (entities == null || entities.isEmpty()) return;
for (Entity entity : entities) update.remove((T) entity);
});
}
@Override
public synchronized <T extends Entity> void chunkEntities(int chunkX, int chunkZ, @NotNull Target<T> target, @NotNull Query<T> query) {
final TargetEntry<Entity> entry = entries[target.ordinal()];
final List<Entity> entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ));
if (entities == null || entities.isEmpty()) return;
for (Entity entity : entities) query.consume((T) entity);
}
@Override
public <T extends Entity> void visibleEntities(int chunkX, int chunkZ, @NotNull Target<T> target, @NotNull Query<T> query) {
for (List<T> entities : references(chunkX, chunkZ, target)) {
if (entities.isEmpty()) continue;
for (Entity entity : entities) query.consume((T) entity);
}
}
@Override
public synchronized @NotNull <T extends Entity> List<List<T>> references(int chunkX, int chunkZ, @NotNull Target<T> target) {
// Gets reference to all chunk entities lists within the range
// This is used to avoid a map lookup per chunk
final TargetEntry<T> entry = (TargetEntry<T>) entries[target.ordinal()];
return entry.chunkRangeEntities.computeIfAbsent(ChunkUtils.getChunkIndex(chunkX, chunkZ),
chunkIndex -> {
List<List<T>> 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<T>) LIST_SUPPLIER.apply(i))));
return List.copyOf(entities);
});
}
@Override
public synchronized <T extends Entity> void nearbyEntities(@NotNull Point point, double range, @NotNull Target<T> target, @NotNull Query<T> 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 <T extends Entity> Set<@NotNull T> entities(@NotNull Target<T> target) {
return (Set<T>) entries[target.ordinal()].entitiesView;
}
@Override
public synchronized void synchronize(@NotNull Point point, @NotNull Runnable runnable) {
runnable.run();
}
private static final class TargetEntry<T extends Entity> {
private final EntityTracker.Target<T> target;
private final Set<T> entities = ConcurrentHashMap.newKeySet(); // Thread-safe since exposed
private final Set<T> entitiesView = Collections.unmodifiableSet(entities);
// Chunk index -> entities inside it
private final Long2ObjectMap<List<T>> chunkEntities = new Long2ObjectOpenHashMap<>();
// Chunk index -> lists of visible entities (references to chunkEntities entries)
private final Long2ObjectMap<List<List<T>>> chunkRangeEntities = new Long2ObjectOpenHashMap<>();
TargetEntry(Target<T> target) {
this.target = target;
}
void addToChunk(long index, T entity) {
this.chunkEntities.computeIfAbsent(index, i -> (List<T>) LIST_SUPPLIER.apply(i)).add(entity);
}
void removeFromChunk(long index, T entity) {
List<T> entities = chunkEntities.get(index);
if (entities != null) entities.remove(entity);
}
}
}

View File

@ -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<Entity> entities = ConcurrentHashMap.newKeySet();
protected final Set<Player> players = ConcurrentHashMap.newKeySet();
protected final Set<EntityCreature> creatures = ConcurrentHashMap.newKeySet();
protected final Set<ExperienceOrb> 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<Set<Entity>> 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<Entity> entities;
synchronized (entitiesLock) {
if ((entities = chunkEntities.get(ChunkUtils.getChunkIndex(chunk))) == null) {
return Collections.emptySet();
}
}
return Collections.unmodifiableSet(entities);
Set<Entity> 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<Entity> 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<Entity> 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<Entity> 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}.
* <p>
* 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.
* <p>
* 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)}.
* <p>

View File

@ -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);
}

View File

@ -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));

View File

@ -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;

View File

@ -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<Player> manualViewers = new ObjectArraySet<>();
private EntityTracker tracker;
private Point lastTrackingPoint;
// Decide if this entity should be viewable to X players
public final Option<Player> viewableOption;
// Decide if this entity should view X entities
public final Option<Entity> viewerOption;
private final Set<Player> set = new SetImpl();
private final Object mutex = this;
public ViewEngine(@Nullable Entity entity,
Consumer<Player> autoViewableAddition, Consumer<Player> autoViewableRemoval,
Consumer<Entity> autoViewerAddition, Consumer<Entity> 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<Entity> viewer, Consumer<Player> 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<Player> asSet() {
return set;
}
public final class Option<T extends Entity> {
@SuppressWarnings("rawtypes")
private static final AtomicIntegerFieldUpdater<Option> UPDATER = AtomicIntegerFieldUpdater.newUpdater(Option.class, "auto");
// The condition that must be met for this option to be considered auto.
private final Predicate<T> loopPredicate;
// The consumers to be called when an entity is added/removed.
public final Consumer<T> addition, removal;
// Contains all the entity ids that are viewable by this option.
public final SparseBitSet bitSet = new SparseBitSet();
// 1 if auto, 0 if manual
private volatile int auto = 1;
// References from the entity trackers.
private List<List<T>> references;
// The custom rule used to determine if an entity is viewable.
private Predicate<T> predicate = entity -> true;
public Option(Predicate<T> loopPredicate,
Consumer<T> addition, Consumer<T> removal) {
this.loopPredicate = loopPredicate;
this.addition = addition;
this.removal = removal;
}
public boolean isAuto() {
return auto == 1;
}
public boolean predicate(T entity) {
return predicate.test(entity);
}
public boolean isRegistered(T entity) {
return bitSet.get(entity.getEntityId());
}
public void register(T entity) {
this.bitSet.set(entity.getEntityId());
}
public void unregister(T entity) {
this.bitSet.clear(entity.getEntityId());
}
public void updateAuto(boolean autoViewable) {
final boolean previous = UPDATER.getAndSet(this, autoViewable ? 1 : 0) == 1;
if (previous != autoViewable) {
synchronized (mutex) {
if (autoViewable) update(references, loopPredicate, addition);
else update(references, this::isRegistered, removal);
}
}
}
public void updateRule(Predicate<T> predicate) {
synchronized (mutex) {
this.predicate = predicate;
updateRule();
}
}
public void updateRule() {
synchronized (mutex) {
update(references, loopPredicate, entity -> {
final boolean result = predicate.test(entity);
if (result != isRegistered(entity)) {
if (result) addition.accept(entity);
else removal.accept(entity);
}
});
}
}
private void update(List<List<T>> references,
Predicate<T> visibilityPredicate,
Consumer<T> action) {
if (tracker == null || references == null) return;
tracker.synchronize(lastTrackingPoint, () -> {
for (List<T> entities : references) {
if (entities.isEmpty()) continue;
for (T entity : entities) {
if (entity == ViewEngine.this.entity || !visibilityPredicate.test(entity)) continue;
if (entity instanceof Player player && manualViewers.contains(player)) continue;
if (entity.getVehicle() != null) continue;
action.accept(entity);
}
}
});
}
}
final class SetImpl extends AbstractSet<Player> {
@Override
public @NotNull Iterator<Player> iterator() {
synchronized (mutex) {
return new It();
}
}
@Override
public int size() {
synchronized (mutex) {
return manualViewers.size() + viewableOption.bitSet.size();
}
}
@Override
public boolean isEmpty() {
synchronized (mutex) {
return manualViewers.isEmpty() && viewableOption.bitSet.isEmpty();
}
}
@Override
public boolean contains(Object o) {
if (!(o instanceof Player player)) return false;
synchronized (mutex) {
return manualViewers.contains(player) || viewableOption.isRegistered(player);
}
}
@Override
public void forEach(Consumer<? super Player> action) {
synchronized (mutex) {
// Manual viewers
if (!manualViewers.isEmpty()) manualViewers.forEach(action);
// Auto
final List<List<Player>> auto = ViewEngine.this.viewableOption.references;
if (auto != null && viewableOption.isAuto()) {
for (List<Player> players : auto) {
if (players.isEmpty()) continue;
for (Player player : players) {
if (validAutoViewer(player)) action.accept(player);
}
}
}
}
}
final class It implements Iterator<Player> {
private Iterator<Player> current = ViewEngine.this.manualViewers.iterator();
private boolean autoIterator = false; // True if the current iterator comes from the auto-viewable references
private int index;
private Player next;
@Override
public boolean hasNext() {
synchronized (mutex) {
return next != null || (next = findNext()) != null;
}
}
@Override
public Player next() {
synchronized (mutex) {
if (next == null) return findNext();
final Player temp = this.next;
this.next = null;
return temp;
}
}
private Player findNext() {
Player result;
if ((result = nextValidEntry(current)) != null) return result;
this.autoIterator = true;
final var references = viewableOption.references;
if (references == null || !viewableOption.isAuto()) return null;
for (int i = index + 1; i < references.size(); i++) {
final List<Player> players = references.get(i);
Iterator<Player> iterator = players.iterator();
if ((result = nextValidEntry(iterator)) != null) {
this.current = iterator;
this.index = i;
return result;
}
}
return null;
}
private Player nextValidEntry(Iterator<Player> iterator) {
while (iterator.hasNext()) {
final Player player = iterator.next();
if (autoIterator ? validAutoViewer(player) : player != entity)
return player;
}
return null;
}
}
}
}

View File

@ -4,8 +4,8 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.function.IntegerBiConsumer;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -115,6 +115,10 @@ public final class ChunkUtils {
return getChunkIndex(chunk.getChunkX(), chunk.getChunkZ());
}
public static long getChunkIndex(@NotNull Point point) {
return getChunkIndex(point.chunkX(), point.chunkZ());
}
/**
* Converts a chunk index to its chunk X position.
*
@ -139,27 +143,38 @@ public final class ChunkUtils {
return y / Chunk.CHUNK_SECTION_SIZE;
}
/**
* Gets the chunks in range of a position.
*
* @param chunkX the initial chunk X
* @param chunkZ the initial chunk Z
* @param range how far should it retrieves chunk
* @return an array containing chunks index
*/
public static long @NotNull [] getChunksInRange(int chunkX, int chunkZ, int range) {
long[] array = new long[MathUtils.square(range * 2 + 1)];
int i = 0;
for (int x = -range; x <= range; ++x) {
for (int z = -range; z <= range; ++z) {
array[i++] = getChunkIndex(chunkX + x, chunkZ + z);
public static void forDifferingChunksInRange(int newChunkX, int newChunkZ,
int oldChunkX, int oldChunkZ,
int range, @NotNull IntegerBiConsumer callback) {
for (int x = newChunkX - range; x <= newChunkX + range; x++) {
for (int z = newChunkZ - range; z <= newChunkZ + range; z++) {
if (Math.abs(x - oldChunkX) > range || Math.abs(z - oldChunkZ) > range) {
callback.accept(x, z);
}
}
}
return array;
}
public static long @NotNull [] getChunksInRange(@NotNull Point point, int range) {
return getChunksInRange(point.chunkX(), point.chunkZ(), range);
public static void forDifferingChunksInRange(int newChunkX, int newChunkZ,
int oldChunkX, int oldChunkZ,
int range,
@NotNull IntegerBiConsumer newCallback, @NotNull IntegerBiConsumer oldCallback) {
// Find the new chunks
forDifferingChunksInRange(newChunkX, newChunkZ, oldChunkX, oldChunkZ, range, newCallback);
// Find the old chunks
forDifferingChunksInRange(oldChunkX, oldChunkZ, newChunkX, newChunkZ, range, oldCallback);
}
public static void forChunksInRange(int chunkX, int chunkZ, int range, IntegerBiConsumer consumer) {
for (int x = -range; x <= range; ++x) {
for (int z = -range; z <= range; ++z) {
consumer.accept(chunkX + x, chunkZ + z);
}
}
}
public static void forChunksInRange(@NotNull Point point, int range, IntegerBiConsumer consumer) {
forChunksInRange(point.chunkX(), point.chunkZ(), range, consumer);
}
/**

View File

@ -1,35 +1,16 @@
package net.minestom.server.utils.entity;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
public final class EntityUtils {
private EntityUtils() {
}
public static void forEachRange(@NotNull Instance instance, @NotNull Point point,
int viewDistance,
@NotNull Consumer<Entity> consumer) {
final long[] chunksInRange = ChunkUtils.getChunksInRange(point, viewDistance);
for (long chunkIndex : chunksInRange) {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null)
continue;
instance.getChunkEntities(chunk).forEach(consumer);
}
}
public static boolean isOnGround(@NotNull Entity entity) {
final Chunk chunk = entity.getChunk();
if (chunk == null)

View File

@ -0,0 +1,9 @@
package net.minestom.server.utils.function;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
@FunctionalInterface
public interface IntegerBiConsumer {
void accept(int v1, int v2);
}

View File

@ -50,6 +50,7 @@ public class Main {
commandManager.register(new RemoveCommand());
commandManager.register(new GiveCommand());
commandManager.register(new SetBlockCommand());
commandManager.register(new AutoViewCommand());
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED)));

View File

@ -0,0 +1,79 @@
package demo.commands;
import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.utils.entity.EntityFinder;
import java.util.List;
import static net.minestom.server.command.builder.arguments.ArgumentType.Boolean;
import static net.minestom.server.command.builder.arguments.ArgumentType.*;
public class AutoViewCommand extends Command {
public AutoViewCommand() {
super("autoview");
// Modify viewable
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
final boolean autoView = context.get("value");
player.setAutoViewable(autoView);
player.sendMessage("Auto-viewable set to " + autoView);
}, Literal("viewable"), Boolean("value"));
// Modify viewer
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
final boolean autoView = context.get("value");
player.setAutoViewEntities(autoView);
player.sendMessage("Auto-viewer set to " + autoView);
}, Literal("viewer"), Boolean("value"));
// Modify viewable rule
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
EntityFinder finder = context.get("targets");
final List<Entity> entities = finder.find(sender);
player.updateViewableRule(entities::contains);
player.sendMessage("Viewable rule updated to see " + entities.size() + " players");
}, Literal("rule-viewable"), Entity("targets").onlyPlayers(true));
// Modify viewer rule
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
EntityFinder finder = context.get("targets");
final List<Entity> entities = finder.find(sender);
player.updateViewerRule(entities::contains);
player.sendMessage("Viewer rule updated to see " + entities.size() + " entities");
}, Literal("rule-viewer"), Entity("targets"));
// Remove viewable rule
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
player.updateViewableRule(p -> true);
player.sendMessage("Viewable rule removed");
}, Literal("remove-rule-viewable"));
// Remove viewer rule
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
player.updateViewerRule(p -> true);
player.sendMessage("Viewer rule removed");
}, Literal("remove-rule-viewer"));
// Update viewable rule
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
player.updateViewableRule();
player.sendMessage("Viewable rule updated");
}, Literal("update-rule-viewable"));
// Update viewer rule
addSyntax((sender, context) -> {
if (!(sender instanceof Player player)) return;
player.updateViewerRule();
player.sendMessage("Viewer rule updated");
}, Literal("update-rule-viewer"));
}
}