mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-28 12:07:42 +01:00
Entity tracking rework (#486)
This commit is contained in:
parent
0bcfc39a9d
commit
faa289a097
@ -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'
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
164
src/main/java/net/minestom/server/instance/EntityTracker.java
Normal file
164
src/main/java/net/minestom/server/instance/EntityTracker.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
305
src/main/java/net/minestom/server/utils/ViewEngine.java
Normal file
305
src/main/java/net/minestom/server/utils/ViewEngine.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
@ -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)));
|
||||
|
79
src/test/java/demo/commands/AutoViewCommand.java
Normal file
79
src/test/java/demo/commands/AutoViewCommand.java
Normal 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"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user