package net.minestom.server.entity; import net.minestom.server.MinecraftServer; import net.minestom.server.Viewable; import net.minestom.server.chat.ColoredText; import net.minestom.server.collision.BoundingBox; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.data.Data; import net.minestom.server.data.DataContainer; import net.minestom.server.event.Event; import net.minestom.server.event.EventCallback; import net.minestom.server.event.entity.EntityDeathEvent; import net.minestom.server.event.entity.EntitySpawnEvent; import net.minestom.server.event.entity.EntityTickEvent; import net.minestom.server.event.entity.EntityVelocityEvent; import net.minestom.server.event.handler.EventHandler; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.network.packet.server.play.*; import net.minestom.server.thread.ThreadProvider; import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.Position; import net.minestom.server.utils.Vector; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.callback.OptionalCallback; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.player.PlayerUtils; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; /** * Could be a player, a monster, or an object. *
* To create your own entity you probably want to extends {@link ObjectEntity} or {@link EntityCreature} instead.
*/
public abstract class Entity implements Viewable, EventHandler, DataContainer {
private static final Map
* 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 when being too far.
*
* @return true if the entity is automatically viewable for close players, false otherwise
*/
public boolean isAutoViewable() {
return autoViewable;
}
/**
* Makes the entity auto viewable or only manually.
*
* @param autoViewable should the entity be automatically viewable for close players
* @see #isAutoViewable()
*/
public void setAutoViewable(boolean autoViewable) {
this.autoViewable = autoViewable;
}
@Override
public boolean addViewer(@NotNull Player player) {
Check.notNull(player, "Viewer cannot be null");
boolean result = this.viewers.add(player);
if (!result)
return false;
player.viewableEntities.add(this);
return true;
}
@Override
public boolean removeViewer(@NotNull Player player) {
Check.notNull(player, "Viewer cannot be null");
if (!viewers.remove(player))
return false;
DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket();
destroyEntitiesPacket.entityIds = new int[]{getEntityId()};
player.getPlayerConnection().sendPacket(destroyEntitiesPacket);
player.viewableEntities.remove(this);
return true;
}
@NotNull
@Override
public Set
* Ignored if {@link #getInstance()} returns null.
*
* @param time the update time in milliseconds
*/
public void tick(long time) {
if (instance == null)
return;
if (scheduledRemoveTime != 0) { // Any entity with scheduled remove does not update
final boolean finished = time >= scheduledRemoveTime;
if (finished) {
remove();
}
return;
}
if (shouldRemove()) {
remove();
return;
}
BlockPosition blockPosition = position.toBlockPosition();
if (!ChunkUtils.isLoaded(instance, position.getX(), position.getZ()) || !ChunkUtils.isLoaded(instance, blockPosition.getX(), blockPosition.getZ())) {
// No update for entities in unloaded chunk
return;
}
// scheduled tasks
if (!nextTick.isEmpty()) {
Consumer
* All entities can be retrieved by calling {@link Entity#getEntity(int)}.
*
* @return the unique entity id
*/
public int getEntityId() {
return id;
}
/**
* Returns the entity type.
*
* @return the entity type
*/
public EntityType getEntityType() {
return entityType;
}
/**
* Gets the entity {@link UUID}.
*
* @return the entity unique id
*/
@NotNull
public UUID getUuid() {
return uuid;
}
/**
* Changes the internal entity UUID, mostly unsafe.
*
* @param uuid the new entity uuid
*/
protected void setUuid(@NotNull UUID uuid) {
this.uuid = uuid;
}
/**
* Returns false just after instantiation, set to true after calling {@link #setInstance(Instance)}.
*
* @return true if the entity has been linked to an instance, false otherwise
*/
public boolean isActive() {
return isActive;
}
/**
* Is used to check collision with coordinates or other blocks/entities.
*
* @return the entity bounding box
*/
@NotNull
public BoundingBox getBoundingBox() {
return boundingBox;
}
/**
* Changes the internal entity bounding box.
*
* WARNING: this does not change the entity hit-box which is client-side.
*
* @param x the bounding box X size
* @param y the bounding box Y size
* @param z the bounding box Z size
*/
public void setBoundingBox(float x, float y, float z) {
this.boundingBox = new BoundingBox(this, x, y, z);
}
/**
* Convenient method to get the entity current chunk.
*
* @return the entity chunk, can be null even if unlikely
*/
@Nullable
public Chunk getChunk() {
return instance.getChunkAt(lastX, lastZ);
}
/**
* Gets the entity current instance.
*
* @return the entity instance, can be null if the entity doesn't have an instance yet
*/
@Nullable
public Instance getInstance() {
return instance;
}
/**
* Changes the entity instance.
*
* @param instance the new instance of the entity
* @throws NullPointerException if {@code instance} is null
* @throws IllegalStateException if {@code instance} has not been registered in {@link InstanceManager}
*/
public void setInstance(@NotNull Instance instance) {
Check.notNull(instance, "instance cannot be null!");
Check.stateCondition(!instance.isRegistered(),
"Instances need to be registered, please use InstanceManager#registerInstance or InstanceManager#registerSharedInstance");
if (this.instance != null) {
this.instance.UNSAFE_removeEntity(this);
}
this.isActive = true;
this.instance = instance;
instance.UNSAFE_addEntity(this);
spawn();
EntitySpawnEvent entitySpawnEvent = new EntitySpawnEvent(this, instance);
callEvent(EntitySpawnEvent.class, entitySpawnEvent);
}
/**
* Gets the entity current velocity.
*
* @return the entity current velocity
*/
@NotNull
public Vector getVelocity() {
return velocity;
}
/**
* Changes the entity velocity and calls {@link EntityVelocityEvent}.
*
* The final velocity can be cancelled or modified by the event.
*
* @param velocity the new entity velocity
*/
public void setVelocity(@NotNull Vector velocity) {
EntityVelocityEvent entityVelocityEvent = new EntityVelocityEvent(this, velocity);
callCancellableEvent(EntityVelocityEvent.class, entityVelocityEvent, () -> {
this.velocity.copy(entityVelocityEvent.getVelocity());
sendPacketToViewersAndSelf(getVelocityPacket());
});
}
/**
* Gets if the entity currently has a velocity applied.
*
* @return true if velocity is not set to 0
*/
public boolean hasVelocity() {
return velocity.getX() != 0 ||
velocity.getY() != 0 ||
velocity.getZ() != 0;
}
/**
* Changes the gravity of the entity.
*
* @param gravityDragPerTick the gravity drag per tick
*/
public void setGravity(float gravityDragPerTick) {
this.gravityDragPerTick = gravityDragPerTick;
}
/**
* Gets the distance between two entities.
*
* @param entity the entity to get the distance from
* @return the distance between this and {@code entity}
*/
public float getDistance(@NotNull Entity entity) {
Check.notNull(entity, "Entity cannot be null");
return getPosition().getDistance(entity.getPosition());
}
/**
* Gets the entity vehicle or null.
*
* @return the entity vehicle, or null if there is not any
*/
@Nullable
public Entity getVehicle() {
return vehicle;
}
/**
* Adds a new passenger to this entity.
*
* @param entity the new passenger
* @throws NullPointerException if {@code entity} is null
* @throws IllegalStateException if {@link #getInstance()} returns null
*/
public void addPassenger(@NotNull Entity entity) {
Check.notNull(entity, "Passenger cannot be null");
Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance");
if (entity.getVehicle() != null) {
entity.getVehicle().removePassenger(entity);
}
this.passengers.add(entity);
entity.vehicle = this;
sendPacketToViewersAndSelf(getPassengersPacket());
}
/**
* Removes a passenger to this entity.
*
* @param entity the passenger to remove
* @throws NullPointerException if {@code entity} is null
* @throws IllegalStateException if {@link #getInstance()} returns null
*/
public void removePassenger(@NotNull Entity entity) {
Check.notNull(entity, "Passenger cannot be null");
Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance");
if (!passengers.remove(entity))
return;
entity.vehicle = null;
sendPacketToViewersAndSelf(getPassengersPacket());
}
/**
* Gets if the entity has any passenger.
*
* @return true if the entity has any passenger, false otherwise
*/
public boolean hasPassenger() {
return !passengers.isEmpty();
}
/**
* Gets the entity passengers.
*
* @return an unmodifiable list containing all the entity passengers
*/
@NotNull
public Set
* WARNING: if you want to apply damage or specify a duration,
* see {@link LivingEntity#setFireForDuration(int, TimeUnit)}.
*
* @param fire should the entity be set in fire
*/
public void setOnFire(boolean fire) {
this.onFire = fire;
sendMetadataIndex(0);
}
/**
* Gets if the entity is invisible or not.
*
* @return true if the entity is invisible, false otherwise
*/
public boolean isInvisible() {
return invisible;
}
/**
* Changes the internal invisible value and send a {@link EntityMetaDataPacket}
* to make visible or invisible the entity to its viewers.
*
* @param invisible true to set the entity invisible, false otherwise
*/
public void setInvisible(boolean invisible) {
this.invisible = invisible;
sendMetadataIndex(0);
}
/**
* Gets if the entity is glowing or not.
*
* @return true if the entity is glowing, false otherwise
*/
public boolean isGlowing() {
return glowing;
}
/**
* Sets or remove the entity glowing effect.
*
* @param glowing true to make the entity glows, false otherwise
*/
public void setGlowing(boolean glowing) {
this.glowing = glowing;
sendMetadataIndex(0);
}
/**
* Gets the entity custom name.
*
* @return the custom name of the entity, null if there is not
*/
public ColoredText getCustomName() {
return customName;
}
/**
* Changes the entity custom name.
*
* @param customName the custom name of the entity, null to remove it
*/
public void setCustomName(ColoredText customName) {
this.customName = customName;
sendMetadataIndex(2);
}
/**
* Gets the custom name visible metadata field.
*
* @return true if the custom name is visible, false otherwise
*/
public boolean isCustomNameVisible() {
return customNameVisible;
}
/**
* Changes the internal custom name visible field and send a {@link EntityMetaDataPacket}
* to update the entity state to its viewers.
*
* @param customNameVisible true to make the custom name visible, false otherwise
*/
public void setCustomNameVisible(boolean customNameVisible) {
this.customNameVisible = customNameVisible;
sendMetadataIndex(3);
}
public boolean isSilent() {
return silent;
}
public void setSilent(boolean silent) {
this.silent = silent;
sendMetadataIndex(4);
}
/**
* Changes the noGravity metadata field and change the gravity behaviour accordingly.
*
* @param noGravity should the entity ignore gravity
*/
public void setNoGravity(boolean noGravity) {
this.noGravity = noGravity;
sendMetadataIndex(5);
}
/**
* Gets the noGravity metadata field.
*
* @return true if the entity ignore gravity, false otherwise
*/
public boolean hasNoGravity() {
return noGravity;
}
/**
* Used to refresh the entity and its passengers position
* - put the entity in the right instance chunk
* - update the viewable chunks (load and unload)
* - add/remove players from the viewers list if {@link #isAutoViewable()} is enabled
*
* WARNING: unsafe, should only be used internally in Minestom. Use {@link #teleport(Position)} instead.
*
* @param x new position X
* @param y new position Y
* @param z new position Z
*/
public void refreshPosition(float x, float y, float z) {
this.lastX = position.getX();
this.lastY = position.getY();
this.lastZ = position.getZ();
position.setX(x);
position.setY(y);
position.setZ(z);
this.cacheX = x;
this.cacheY = y;
this.cacheZ = z;
if (hasPassenger()) {
for (Entity passenger : getPassengers()) {
passenger.refreshPosition(x, y, z);
}
}
final Instance instance = getInstance();
if (instance != null) {
final Chunk lastChunk = instance.getChunkAt(lastX, lastZ);
final Chunk newChunk = instance.getChunkAt(x, z);
if (lastChunk != null && newChunk != null && lastChunk != newChunk) {
synchronized (instance) {
instance.removeEntityFromChunk(this, lastChunk);
instance.addEntityToChunk(this, newChunk);
}
if (this instanceof Player) {
// Refresh player view
final Player player = (Player) this;
player.refreshVisibleChunks(newChunk);
player.refreshVisibleEntities(newChunk);
}
}
}
}
/**
* @param position the new position
* @see #refreshPosition(float, float, float)
*/
public void refreshPosition(@NotNull Position position) {
refreshPosition(position.getX(), position.getY(), position.getZ());
}
/**
* Updates the entity view internally.
*
* Warning: you probably want to use {@link #setView(float, float)}.
*
* @param yaw the yaw
* @param pitch the pitch
*/
public void refreshView(float yaw, float pitch) {
this.lastYaw = position.getYaw();
this.lastPitch = position.getPitch();
position.setYaw(yaw);
position.setPitch(pitch);
this.cacheYaw = yaw;
this.cachePitch = pitch;
}
/**
* Makes the entity sneak.
*
* WARNING: this will not work for the client itself.
*
* @param sneaking true to make the entity sneak
*/
public void setSneaking(boolean sneaking) {
this.crouched = sneaking;
this.pose = sneaking ? Pose.SNEAKING : Pose.STANDING;
sendMetadataIndex(0);
sendMetadataIndex(6);
}
/**
* Makes the entity sprint.
*
* WARNING: this will not work on the client itself.
*
* @param sprinting true to make the entity sprint
*/
public void setSprinting(boolean sprinting) {
this.sprinting = sprinting;
sendMetadataIndex(0);
}
/**
* Gets the entity position.
*
* @return the current position of the entity
*/
public Position getPosition() {
return position;
}
/**
* Gets the entity eye height.
*
* @return the entity eye height
*/
public float getEyeHeight() {
return eyeHeight;
}
/**
* Changes the entity eye height.
*
* @param eyeHeight the entity eye height
*/
public void setEyeHeight(float eyeHeight) {
this.eyeHeight = eyeHeight;
}
/**
* Gets if this entity is in the same chunk as the specified position.
*
* @param position the checked position chunk
* @return true if the entity is in the same chunk as {@code position}
*/
public boolean sameChunk(@NotNull Position position) {
Check.notNull(position, "Position cannot be null");
final Position pos = getPosition();
final int chunkX1 = ChunkUtils.getChunkCoordinate((int) Math.floor(pos.getX()));
final int chunkZ1 = ChunkUtils.getChunkCoordinate((int) Math.floor(pos.getZ()));
final int chunkX2 = ChunkUtils.getChunkCoordinate((int) Math.floor(position.getX()));
final int chunkZ2 = ChunkUtils.getChunkCoordinate((int) Math.floor(position.getZ()));
return chunkX1 == chunkX2 && chunkZ1 == chunkZ2;
}
/**
* Gets if the entity is in the same chunk as another.
*
* @param entity the entity to check
* @return true if both entities are in the same chunk, false otherwise
*/
public boolean sameChunk(@NotNull Entity entity) {
return sameChunk(entity.getPosition());
}
/**
* Removes the entity from the server immediately.
*
* WARNING: this do not trigger the {@link EntityDeathEvent} event.
*/
public void remove() {
this.removed = true;
this.shouldRemove = true;
entityById.remove(id);
if (instance != null)
instance.UNSAFE_removeEntity(this);
}
/**
* Gets if this entity has been removed.
*
* @return true if this entity is removed
*/
public boolean isRemoved() {
return removed;
}
/**
* Triggers {@link #remove()} after the specified time.
*
* @param delay the time before removing the entity
* @param timeUnit the unit of the delay
*/
public void scheduleRemove(long delay, @NotNull TimeUnit timeUnit) {
delay = timeUnit.toMilliseconds(delay);
if (delay == 0) { // Cancel the scheduled remove
this.scheduledRemoveTime = 0;
return;
}
this.scheduledRemoveTime = System.currentTimeMillis() + delay;
}
/**
* Gets if the entity removal has been scheduled with {@link #scheduleRemove(long, TimeUnit)}.
*
* @return true if the entity removal has been scheduled
*/
public boolean isRemoveScheduled() {
return scheduledRemoveTime != 0;
}
@NotNull
protected EntityVelocityPacket getVelocityPacket() {
final float strength = 8000f / MinecraftServer.TICK_PER_SECOND;
EntityVelocityPacket velocityPacket = new EntityVelocityPacket();
velocityPacket.entityId = getEntityId();
velocityPacket.velocityX = (short) (velocity.getX() * strength);
velocityPacket.velocityY = (short) (velocity.getY() * strength);
velocityPacket.velocityZ = (short) (velocity.getZ() * strength);
return velocityPacket;
}
/**
* Gets an {@link EntityMetaDataPacket} sent when adding viewers. Used for synchronization.
*
* @return The {@link EntityMetaDataPacket} related to this entity
*/
@NotNull
public EntityMetaDataPacket getMetadataPacket() {
EntityMetaDataPacket metaDataPacket = new EntityMetaDataPacket();
metaDataPacket.entityId = getEntityId();
metaDataPacket.consumer = getMetadataConsumer();
return metaDataPacket;
}
/**
* Should be override when wanting to add a new metadata index
*
* @return The consumer used to write {@link EntityMetaDataPacket} in {@link #getMetadataPacket()}
*/
@NotNull
public ConsumersendPacketsToViewers(getVelocityPacket());
.
*/
public void sendVelocityPacket() {
sendPacketsToViewers(getVelocityPacket());
}
/**
* Gets the number of ticks this entity has been active for.
*
* @return the number of ticks this entity has been active for
*/
public long getAliveTicks() {
return ticks;
}
/**
* How does this entity handle being in the void?
*/
protected void handleVoid() {
// Kill if in void
if (getInstance().isInVoid(this.position)) {
remove();
}
}
@NotNull
@Override
public Map