package net.minestom.server.entity; import it.unimi.dsi.fastutil.longs.LongArrayPriorityQueue; import it.unimi.dsi.fastutil.longs.LongPriorityQueue; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.inventory.Book; import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.resource.ResourcePackCallback; import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.resource.ResourcePackStatus; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent.ShowEntity; import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.title.TitlePart; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.adventure.AdventurePacketConvertor; import net.minestom.server.adventure.Localizable; import net.minestom.server.adventure.audience.Audiences; import net.minestom.server.attribute.Attribute; import net.minestom.server.collision.BoundingBox; import net.minestom.server.command.CommandSender; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.effects.Effects; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.entity.metadata.LivingEntityMeta; import net.minestom.server.entity.metadata.PlayerMeta; import net.minestom.server.entity.vehicle.PlayerVehicleInformation; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.inventory.InventoryOpenEvent; import net.minestom.server.event.item.ItemDropEvent; 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.instance.block.Block; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.metadata.WrittenBookMeta; import net.minestom.server.listener.manager.PacketListenerManager; import net.minestom.server.message.ChatMessageType; import net.minestom.server.message.ChatPosition; import net.minestom.server.message.Messenger; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.packet.client.ClientPacket; import net.minestom.server.network.packet.server.SendablePacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.common.*; import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.packet.server.play.data.WorldPos; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; import net.minestom.server.recipe.Recipe; import net.minestom.server.recipe.RecipeManager; import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Team; import net.minestom.server.snapshot.EntitySnapshot; import net.minestom.server.snapshot.PlayerSnapshot; import net.minestom.server.snapshot.SnapshotImpl; import net.minestom.server.snapshot.SnapshotUpdater; import net.minestom.server.statistic.PlayerStatistic; import net.minestom.server.timer.Scheduler; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.PropertyUtils; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.chunk.ChunkUpdateLimitChecker; import net.minestom.server.utils.chunk.ChunkUtils; 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; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.DimensionType; import org.jctools.queues.MessagePassingQueue; import org.jctools.queues.MpscUnboundedXaddArrayQueue; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import java.util.function.UnaryOperator; /** * Those are the major actors of the server *
* You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}.
*/
public class Player extends LivingEntity implements CommandSender, Localizable, HoverEventSource
* WARNING: executed in the main update thread
* UNSAFE: Only meant to be used when a socket player connects through the server.
*/
@ApiStatus.Internal
public CompletableFuture This will result in them being removed from the current instance, player list, etc.
* Be aware that because chunk operations are expensive,
* it is possible for this method to be non-blocking when retrieving chunks is required.
*
* @param instance the new player instance
* @param spawnPosition the new position of the player
* @return a future called once the player instance changed
*/
@Override
public CompletableFuture
* Does add the player to {@code instance}, remove all viewable entities and call {@link PlayerSpawnEvent}.
*
* 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
*/
private void spawnPlayer(@NotNull Instance instance, @NotNull Pos spawnPosition,
boolean firstSpawn, boolean dimensionChange, boolean updateChunks) {
if (!firstSpawn && !dimensionChange) {
// Player instance changed, clear current viewable collections
if (updateChunks)
ChunkUtils.forChunksInRange(spawnPosition, settings.getEffectiveViewDistance(), chunkRemover);
}
if (dimensionChange) sendDimension(instance.getDimensionType(), instance.getDimensionName());
super.setInstance(instance, spawnPosition);
if (updateChunks) {
final int chunkX = spawnPosition.chunkX();
final int chunkZ = spawnPosition.chunkZ();
chunksLoadedByClient = new Vec(chunkX, chunkZ);
chunkUpdateLimitChecker.addToHistory(getChunk());
sendPacket(new UpdateViewPositionPacket(chunkX, chunkZ));
// Load the nearby chunks and queue them to be sent to them
ChunkUtils.forChunksInRange(spawnPosition, settings.getEffectiveViewDistance(), chunkAdder);
}
synchronizePositionAfterTeleport(spawnPosition, 0); // So the player doesn't get stuck
if (dimensionChange) {
sendPacket(new SpawnPositionPacket(spawnPosition, 0));
instance.getWorldBorder().init(this);
sendPacket(new TimeUpdatePacket(instance.getWorldAge(), instance.getTime()));
}
if (dimensionChange || firstSpawn) {
this.inventory.update();
sendPacket(new HeldItemChangePacket(heldSlot));
// Tell the client to leave the loading terrain screen
sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.LEVEL_CHUNKS_LOAD_START, 0));
}
EventDispatcher.call(new PlayerSpawnEvent(this, instance, firstSpawn));
}
@ApiStatus.Internal
public void onChunkBatchReceived(float newTargetChunksPerTick) {
// logger.debug("chunk batch received player={} chunks/tick={} lead={}", username, newTargetChunksPerTick, chunkBatchLead);
chunkBatchLead -= 1;
targetChunksPerTick = Float.isNaN(newTargetChunksPerTick) ? MIN_CHUNKS_PER_TICK : MathUtils.clamp(
newTargetChunksPerTick * CHUNKS_PER_TICK_MULTIPLIER, MIN_CHUNKS_PER_TICK, MAX_CHUNKS_PER_TICK);
// Beyond the first batch we can preemptively send up to 10 (matching mojang server)
if (maxChunkBatchLead == 1) maxChunkBatchLead = 10;
}
/**
* Queues the given chunk to be sent to the player.
*
* @param chunk The chunk to send
*/
public void sendChunk(@NotNull Chunk chunk) {
if (!chunk.isLoaded()) return;
chunkQueueLock.lock();
try {
chunkQueue.enqueue(ChunkUtils.getChunkIndex(chunk.getChunkX(), chunk.getChunkZ()));
} finally {
chunkQueueLock.unlock();
}
}
private void sendPendingChunks() {
// If we have nothing to send or have sent the max # of batches without reply, do nothing
if (chunkQueue.isEmpty() || chunkBatchLead >= maxChunkBatchLead) return;
// Increment the pending chunk count by the target chunks per tick
pendingChunkCount = Math.min(pendingChunkCount + targetChunksPerTick, MAX_CHUNKS_PER_TICK);
if (pendingChunkCount < 1) return; // Cant send anything
chunkQueueLock.lock();
try {
int batchSize = 0;
sendPacket(new ChunkBatchStartPacket());
while (!chunkQueue.isEmpty() && pendingChunkCount >= 1f) {
long chunkIndex = chunkQueue.dequeueLong();
int chunkX = ChunkUtils.getChunkCoordX(chunkIndex), chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
var chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null || !chunk.isLoaded()) continue;
sendPacket(chunk.getFullDataPacket());
EventDispatcher.call(new PlayerChunkLoadEvent(this, chunkX, chunkZ));
pendingChunkCount -= 1f;
batchSize += 1;
}
sendPacket(new ChunkBatchFinishedPacket(batchSize));
chunkBatchLead += 1;
// logger.debug("chunk batch sent player={} chunks={} lead={}", username, batchSize, chunkBatchLead);
} finally {
chunkQueueLock.unlock();
}
}
@Override
protected void updatePose() {
Pose oldPose = getPose();
Pose newPose;
// Figure out their expected state
var meta = getEntityMeta();
if (meta.isFlyingWithElytra()) {
newPose = Pose.FALL_FLYING;
} else if (false) { // When should they be sleeping? We don't have any in-bed state...
newPose = Pose.SLEEPING;
} else if (meta.isSwimming()) {
newPose = Pose.SWIMMING;
} else if (meta instanceof LivingEntityMeta livingMeta && livingMeta.isInRiptideSpinAttack()) {
newPose = Pose.SPIN_ATTACK;
} else if (isSneaking() && !isFlying()) {
newPose = Pose.SNEAKING;
} else {
newPose = Pose.STANDING;
}
// Try to put them in their expected state, or the closest if they don't fit.
if (canFitWithBoundingBox(newPose)) {
// Use expected state
} else if (canFitWithBoundingBox(Pose.SNEAKING)) {
newPose = Pose.SNEAKING;
} else if (canFitWithBoundingBox(Pose.SWIMMING)) {
newPose = Pose.SWIMMING;
} else {
// If they can't fit anywhere, just use standing
newPose = Pose.STANDING;
}
if (newPose != oldPose) setPose(newPose);
}
/**
* Returns true if the player can fit at the current position with the given {@link net.minestom.server.entity.Entity.Pose}, false otherwise.
*
* @param pose The pose to check
*/
private boolean canFitWithBoundingBox(@NotNull Pose pose) {
BoundingBox bb = pose == Pose.STANDING ? boundingBox : BoundingBox.fromPose(pose);
if (bb == null) return false;
var position = getPosition();
var iter = bb.getBlocks(getPosition());
while (iter.hasNext()) {
var pos = iter.next();
var block = instance.getBlock(pos, Block.Getter.Condition.TYPE);
// For now just ignore scaffolding. It seems to have a dynamic bounding box, or is just parsed
// incorrectly in MinestomDataGenerator.
if (block.id() == Block.SCAFFOLDING.id()) continue;
var hit = block.registry().collisionShape()
.intersectBox(position.sub(pos.blockX(), pos.blockY(), pos.blockZ()), bb);
if (hit) return false;
}
return true;
}
/**
* Sends a plugin message to the player.
*
* @param channel the message channel
* @param data the message data
*/
public void sendPluginMessage(@NotNull String channel, byte @NotNull [] data) {
sendPacket(new PluginMessagePacket(channel, data));
}
/**
* Sends a plugin message to the player.
*
* Message encoded to UTF-8.
*
* @param channel the message channel
* @param message the message
*/
public void sendPluginMessage(@NotNull String channel, @NotNull String message) {
sendPluginMessage(channel, message.getBytes(StandardCharsets.UTF_8));
}
/**
* Deprecated, as the Adventure library has deprecated this method in the Audience class
* @param source the identity of the source of the message
* @param message a message
* @param type the type
*/
@Override
@Deprecated
public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) {
Messenger.sendMessage(this, message, ChatPosition.fromMessageType(type), source.uuid());
}
@Override
public void playSound(@NotNull Sound sound) {
this.playSound(sound, this.position.x(), this.position.y(), this.position.z());
}
public void playSound(@NotNull Sound sound, @NotNull Point point) {
sendPacket(AdventurePacketConvertor.createSoundPacket(sound, point.x(), point.y(), point.z()));
}
@Override
public void playSound(@NotNull Sound sound, double x, double y, double z) {
sendPacket(AdventurePacketConvertor.createSoundPacket(sound, x, y, z));
}
@Override
public void playSound(@NotNull Sound sound, Sound.@NotNull Emitter emitter) {
final ServerPacket packet;
if (emitter == Sound.Emitter.self()) {
packet = AdventurePacketConvertor.createSoundPacket(sound, this);
} else {
packet = AdventurePacketConvertor.createSoundPacket(sound, emitter);
}
sendPacket(packet);
}
@Override
public void stopSound(@NotNull SoundStop stop) {
sendPacket(AdventurePacketConvertor.createSoundStopPacket(stop));
}
/**
* Plays a given effect at the given position for this player.
*
* @param effect the effect to play
* @param x x position of the effect
* @param y y position of the effect
* @param z z position of the effect
* @param data data for the effect
* @param disableRelativeVolume disable volume scaling based on distance
*/
public void playEffect(@NotNull Effects effect, int x, int y, int z, int data, boolean disableRelativeVolume) {
sendPacket(new EffectPacket(effect.getId(), new Vec(x, y, z), data, disableRelativeVolume));
}
@Override
public void sendPlayerListHeaderAndFooter(@NotNull Component header, @NotNull Component footer) {
sendPacket(new PlayerListHeaderAndFooterPacket(header, footer));
}
@Override
public Note that this method will throw an exception if the player's entity type has
* been changed with {@link #switchEntityType(EntityType)}. It is wise to check
* {@link #getEntityType()} first. Note that this function is uncallable if the player has their entity type switched
* with {@link #switchEntityType(EntityType)}. Note that this function is uncallable if the player has their entity type switched
* with {@link #switchEntityType(EntityType)}.
* Sets to null to show the player username.
*
* @param displayName the display name, null to display the username
*/
public void setDisplayName(@Nullable Component displayName) {
this.displayName = displayName;
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, infoEntry()));
}
/**
* Gets the player skin.
*
* @return the player skin object,
* null means that the player has his {@link #getUuid()} default skin
*/
public @Nullable PlayerSkin getSkin() {
return skin;
}
/**
* Changes the player skin.
*
* This does remove the player for all viewers to spawn it again with the correct new skin.
*
* @param skin the player skin, null to reset it to his {@link #getUuid()} default skin
* @see PlayerSkinInitEvent if you want to apply the skin at connection
*/
public synchronized void setSkin(@Nullable PlayerSkin skin) {
this.skin = skin;
if (instance == null)
return;
DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket(getEntityId());
final PlayerInfoRemovePacket removePlayerPacket = getRemovePlayerToList();
final PlayerInfoUpdatePacket addPlayerPacket = getAddPlayerToList();
RespawnPacket respawnPacket = new RespawnPacket(getDimensionType().toString(), instance.getDimensionName(),
0, gameMode, gameMode, false, levelFlat, deathLocation, portalCooldown, RespawnPacket.COPY_ALL);
sendPacket(removePlayerPacket);
sendPacket(destroyEntitiesPacket);
sendPacket(addPlayerPacket);
sendPacket(respawnPacket);
refreshClientStateAfterRespawn();
{
// Remove player
PacketUtils.broadcastPlayPacket(removePlayerPacket);
sendPacketToViewers(destroyEntitiesPacket);
// Show player again
PacketUtils.broadcastPlayPacket(addPlayerPacket);
getViewers().forEach(player -> showPlayer(player.getPlayerConnection()));
}
getInventory().update();
teleport(getPosition());
}
public void setDeathLocation(@NotNull DimensionType type, @NotNull Pos position) {
this.deathLocation = new WorldPos(type.getName().asString(), position);
}
public @Nullable WorldPos getDeathLocation() {
return this.deathLocation;
}
/**
* Gets if the player has the respawn screen enabled or disabled.
*
* @return true if the player has the respawn screen, false if he didn't
*/
public boolean isEnableRespawnScreen() {
return enableRespawnScreen;
}
/**
* Enables or disable the respawn screen.
*
* @param enableRespawnScreen true to enable the respawn screen, false to disable it
*/
public void setEnableRespawnScreen(boolean enableRespawnScreen) {
this.enableRespawnScreen = enableRespawnScreen;
sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.ENABLE_RESPAWN_SCREEN, enableRespawnScreen ? 0 : 1));
}
/**
* Gets the player's name as a component. This will either return the display name
* (if set) or a component holding the username.
*
* @return the name
*/
@Override
public @NotNull Component getName() {
return Objects.requireNonNullElse(displayName, usernameComponent);
}
/**
* Gets the player's username.
*
* @return the player's username
*/
public @NotNull String getUsername() {
return username;
}
/**
* Changes the internal player name, used for the {@link AsyncPlayerPreLoginEvent}
* mostly unsafe outside of it.
*
* @param username the new player name
*/
public void setUsernameField(@NotNull String username) {
this.username = username;
this.usernameComponent = Component.text(username);
}
/**
* Calls an {@link ItemDropEvent} with a specified item.
*
* Returns false if {@code item} is air.
*
* @param item the item to drop
* @return true if player can drop the item (event not cancelled), false otherwise
*/
public boolean dropItem(@NotNull ItemStack item) {
if (item.isAir()) return false;
ItemDropEvent itemDropEvent = new ItemDropEvent(this, item);
EventDispatcher.call(itemDropEvent);
return !itemDropEvent.isCancelled();
}
@Override
public void sendResourcePacks(@NotNull ResourcePackRequest request) {
if (request.replace()) clearResourcePacks();
for (var pack : request.packs()) {
sendPacket(new ResourcePackPushPacket(pack, request.required(), request.prompt()));
resourcePackCallbacks.put(pack.id(), request.callback());
if (resourcePackFuture == null) {
resourcePackFuture = new CompletableFuture<>();
}
}
}
@Override
public void removeResourcePacks(@NotNull UUID id, @NotNull UUID @NotNull ... others) {
sendPacket(new ResourcePackPopPacket(id));
for (var other : others) {
sendPacket(new ResourcePackPopPacket(other));
}
}
@Override
public void clearResourcePacks() {
sendPacket(new ResourcePackPopPacket((UUID) null));
}
/**
* If there are resource packs in-flight, a future is returned which will be completed when
* all resource packs have been responded to by the client. Otherwise null is returned.
*/
@ApiStatus.Internal
public @Nullable CompletableFuture
* Can be altered by the {@link PlayerRespawnEvent#setRespawnPosition(Pos)}.
*
* @return a copy of the default respawn point
*/
public @NotNull Pos getRespawnPoint() {
return respawnPoint;
}
/**
* Changes the default spawn point.
*
* @param respawnPoint the player respawn point
*/
public void setRespawnPoint(@NotNull Pos respawnPoint) {
this.respawnPoint = respawnPoint;
}
/**
* Called after the player teleportation to refresh his position
* and send data to his new viewers.
*/
protected void refreshAfterTeleport() {
sendPacketsToViewers(getEntityType().registry().spawnType().getSpawnPacket(this));
// Update for viewers
sendPacketToViewersAndSelf(getVelocityPacket());
sendPacketToViewersAndSelf(getMetadataPacket());
sendPacketToViewersAndSelf(getPropertiesPacket());
sendPacketToViewersAndSelf(getEquipmentsPacket());
getInventory().update();
}
/**
* Sets the player food and health values to their maximum.
*/
protected void refreshHealth() {
this.food = 20;
this.foodSaturation = 5;
// refresh health and send health packet
heal();
}
/**
* Gets the percentage displayed in the experience bar.
*
* @return the exp percentage 0-1
*/
public float getExp() {
return exp;
}
/**
* Used to change the percentage experience bar.
* This cannot change the displayed level, see {@link #setLevel(int)}.
*
* @param exp a percentage between 0 and 1
* @throws IllegalArgumentException if {@code exp} is not between 0 and 1
*/
public void setExp(float exp) {
Check.argCondition(!MathUtils.isBetween(exp, 0, 1), "Exp should be between 0 and 1");
this.exp = exp;
sendPacket(new SetExperiencePacket(exp, level, 0));
}
/**
* Gets the level of the player displayed in the experience bar.
*
* @return the player level
*/
public int getLevel() {
return level;
}
/**
* Used to change the level of the player
* This cannot change the displayed percentage bar see {@link #setExp(float)}
*
* @param level the new level of the player
*/
public void setLevel(int level) {
this.level = level;
sendPacket(new SetExperiencePacket(exp, level, 0));
}
public int getPortalCooldown() {
return portalCooldown;
}
public void setPortalCooldown(int portalCooldown) {
this.portalCooldown = portalCooldown;
}
/**
* Gets the player connection.
*
* Used to send packets and get stuff related to the connection.
*
* @return the player connection
*/
public @NotNull PlayerConnection getPlayerConnection() {
return playerConnection;
}
/**
* Shortcut for {@link PlayerConnection#sendPacket(SendablePacket)}.
*
* @param packet the packet to send
*/
@ApiStatus.Experimental
public void sendPacket(@NotNull SendablePacket packet) {
this.playerConnection.sendPacket(packet);
}
@ApiStatus.Experimental
public void sendPackets(@NotNull SendablePacket... packets) {
this.playerConnection.sendPackets(packets);
}
@ApiStatus.Experimental
public void sendPackets(@NotNull Collection
* Should only be used within an inventory listener (event or condition).
*
* @return true if the inventory has been closed, false otherwise
*/
public boolean didCloseInventory() {
return didCloseInventory;
}
/**
* Used internally to reset the didCloseInventory field.
*
* Shouldn't be used externally without proper understanding of its consequence.
*
* @param didCloseInventory the new didCloseInventory field
*/
@ApiStatus.Internal
public void UNSAFE_changeDidCloseInventory(boolean didCloseInventory) {
this.didCloseInventory = didCloseInventory;
}
public int getNextTeleportId() {
return teleportId.incrementAndGet();
}
public int getLastSentTeleportId() {
return teleportId.get();
}
public int getLastReceivedTeleportId() {
return receivedTeleportId;
}
public void refreshReceivedTeleportId(int receivedTeleportId) {
this.receivedTeleportId = receivedTeleportId;
}
/**
* Used to synchronize player position with viewers on spawn or after {@link Entity#teleport(Pos, long[], int)}
* in cases where a {@link PlayerPositionAndLookPacket} is required
*
* @param position the position used by {@link PlayerPositionAndLookPacket}
* this may not be the same as the {@link Entity#position}
* @param relativeFlags byte flags used by {@link PlayerPositionAndLookPacket}
*/
@ApiStatus.Internal
void synchronizePositionAfterTeleport(@NotNull Pos position, int relativeFlags) {
sendPacket(new PlayerPositionAndLookPacket(position, (byte) relativeFlags, getNextTeleportId()));
super.synchronizePosition();
}
/**
* Forces the player's client to look towards the target yaw/pitch
*
* @param yaw the new yaw
* @param pitch the new pitch
*/
@Override
public void setView(float yaw, float pitch) {
teleport(new Pos(0, 0, 0, yaw, pitch), null, RelativeFlags.COORD).join();
}
/**
* Forces the player's client to look towards the specified point
*
* Note: the player's position is not updated on the server until
* the client receives this packet
*
* @param point the point to look at
*/
@Override
public void lookAt(@NotNull Point point) {
// Let the player's client provide updated position values
sendPacket(new FacePlayerPacket(FacePlayerPacket.FacePosition.EYES, point, 0, null));
}
/**
* Forces the player's client to look towards the specified entity
*
* Note: the player's position is not updated on the server until
* the client receives this packet
*
* @param entity the entity to look at
*/
@Override
public void lookAt(@NotNull Entity entity) {
// Let the player's client provide updated position values
sendPacket(new FacePlayerPacket(FacePlayerPacket.FacePosition.EYES, entity.getPosition(), entity.getEntityId(), FacePlayerPacket.FacePosition.EYES));
}
/**
* Gets the player permission level.
*
* @return the player permission level
*/
public int getPermissionLevel() {
return permissionLevel;
}
/**
* Changes the player permission level.
*
* @param permissionLevel the new player permission level
* @throws IllegalArgumentException if {@code permissionLevel} is not between 0 and 4
*/
public void setPermissionLevel(int permissionLevel) {
Check.argCondition(!MathUtils.isBetween(permissionLevel, 0, 4), "permissionLevel has to be between 0 and 4");
this.permissionLevel = permissionLevel;
// Condition to prevent sending the packets before spawning the player
if (isActive()) {
final byte permissionLevelStatus = (byte) (STATUS_PERMISSION_LEVEL_OFFSET + permissionLevel);
triggerStatus(permissionLevelStatus);
}
}
/**
* Sets or remove the reduced debug screen.
*
* @param reduced should the player has the reduced debug screen
*/
public void setReducedDebugScreenInformation(boolean reduced) {
this.reducedDebugScreenInformation = reduced;
final byte debugScreenStatus = (byte) (reduced ? STATUS_ENABLE_REDUCED_DEBUG_INFO : STATUS_DISABLE_REDUCED_DEBUG_INFO);
triggerStatus(debugScreenStatus);
}
/**
* Gets if the player has the reduced debug screen.
*
* @return true if the player has the reduced debug screen, false otherwise
*/
public boolean hasReducedDebugScreenInformation() {
return reducedDebugScreenInformation;
}
/**
* The invulnerable field appear in the {@link PlayerAbilitiesPacket} packet.
*
* @return true if the player is invulnerable, false otherwise
*/
public boolean isInvulnerable() {
return super.isInvulnerable();
}
/**
* This do update the {@code invulnerable} field in the packet {@link PlayerAbilitiesPacket}
* and prevent the player from receiving damage.
*
* @param invulnerable should the player be invulnerable
*/
public void setInvulnerable(boolean invulnerable) {
super.setInvulnerable(invulnerable);
refreshAbilities();
}
@Override
public void setSneaking(boolean sneaking) {
if (isFlying()) { //If we are flying, don't set the players pose to sneaking as this can clip them through blocks
this.entityMeta.setSneaking(sneaking);
} else {
super.setSneaking(sneaking);
}
}
/**
* Gets if the player is currently flying.
*
* @return true if the player if flying, false otherwise
*/
public boolean isFlying() {
return flying;
}
/**
* Sets the player flying.
*
* @param flying should the player fly
*/
public void setFlying(boolean flying) {
refreshFlying(flying);
refreshAbilities();
}
/**
* Updates the internal flying field.
*
* Mostly unsafe since there is nothing to backup the value, used internally for creative players.
*
* @param flying the new flying field
* @see #setFlying(boolean) instead
*/
public void refreshFlying(boolean flying) {
//When the player starts or stops flying, their pose needs to change
if (this.flying != flying) {
Pose pose = getPose();
if (this.isSneaking() && pose == Pose.STANDING) {
setPose(Pose.SNEAKING);
} else if (pose == Pose.SNEAKING) {
setPose(Pose.STANDING);
}
}
this.flying = flying;
}
/**
* Gets if the player is allowed to fly.
*
* @return true if the player if allowed to fly, false otherwise
*/
public boolean isAllowFlying() {
return allowFlying;
}
/**
* Allows or forbid the player to fly.
*
* @param allowFlying should the player be allowed to fly
*/
public void setAllowFlying(boolean allowFlying) {
this.allowFlying = allowFlying;
refreshAbilities();
}
public boolean isInstantBreak() {
return instantBreak;
}
/**
* Changes the player ability "Creative Mode".
*
* @param instantBreak true to allow instant break
* @see player abilities
*/
public void setInstantBreak(boolean instantBreak) {
this.instantBreak = instantBreak;
refreshAbilities();
}
/**
* Gets the player flying speed.
*
* @return the flying speed of the player
*/
public float getFlyingSpeed() {
return flyingSpeed;
}
/**
* Updates the internal field and send a {@link PlayerAbilitiesPacket} with the new flying speed.
*
* @param flyingSpeed the new flying speed of the player
*/
public void setFlyingSpeed(float flyingSpeed) {
this.flyingSpeed = flyingSpeed;
refreshAbilities();
}
public float getFieldViewModifier() {
return fieldViewModifier;
}
public void setFieldViewModifier(float fieldViewModifier) {
this.fieldViewModifier = fieldViewModifier;
refreshAbilities();
}
/**
* This is the map used to send the statistic packet.
* It is possible to add/remove/change statistic value directly into it.
*
* @return the modifiable statistic map
*/
public @NotNull Map
* Warning: could lead to have the player kicked because of a wrong keep alive packet.
*
* @param lastKeepAlive the new lastKeepAlive id
*/
public void refreshKeepAlive(long lastKeepAlive) {
this.lastKeepAlive = lastKeepAlive;
this.answerKeepAlive = false;
}
public boolean didAnswerKeepAlive() {
return answerKeepAlive;
}
public void refreshAnswerKeepAlive(boolean answerKeepAlive) {
this.answerKeepAlive = answerKeepAlive;
}
/**
* Changes the held item for the player viewers
* Also cancel eating if {@link #isEating()} was true.
*
* Warning: the player will not be noticed by this chance, only his viewers,
* see instead: {@link #setHeldItemSlot(byte)}.
*
* @param slot the new held slot
*/
public void refreshHeldSlot(byte slot) {
this.heldSlot = slot;
syncEquipment(EquipmentSlot.MAIN_HAND);
refreshEating(null);
}
public void refreshEating(@Nullable Hand eatingHand, long eatingTime) {
this.eatingHand = eatingHand;
if (eatingHand != null) {
this.startEatingTime = System.currentTimeMillis();
this.eatingTime = eatingTime;
} else {
this.startEatingTime = 0;
}
}
public void refreshEating(@Nullable Hand eatingHand) {
refreshEating(eatingHand, defaultEatingTime);
}
/**
* Used to call {@link ItemUpdateStateEvent} with the proper item
* It does check which hand to get the item to update.
*
* @param allowFood true if food should be updated, false otherwise
* @return the called {@link ItemUpdateStateEvent},
* null if there is no item to update the state
* @deprecated Use {@link #callItemUpdateStateEvent(Hand)} instead
*/
@Deprecated
public @Nullable ItemUpdateStateEvent callItemUpdateStateEvent(boolean allowFood, @Nullable Hand hand) {
if (hand == null)
return null;
final ItemStack updatedItem = getItemInHand(hand);
final boolean isFood = updatedItem.material().isFood();
if (isFood && !allowFood)
return null;
ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(this, hand, updatedItem);
EventDispatcher.call(itemUpdateStateEvent);
return itemUpdateStateEvent;
}
/**
* Used to call {@link ItemUpdateStateEvent} with the proper item
* It does check which hand to get the item to update. Allows food.
*
* @return the called {@link ItemUpdateStateEvent},
* null if there is no item to update the state
*/
public @Nullable ItemUpdateStateEvent callItemUpdateStateEvent(@Nullable Hand hand) {
return callItemUpdateStateEvent(true, hand);
}
public void refreshVehicleSteer(float sideways, float forward, boolean jump, boolean unmount) {
this.vehicleInformation.refresh(sideways, forward, jump, unmount);
}
/**
* Gets the last sent keep alive id.
*
* @return the last keep alive id sent to the player
*/
public long getLastKeepAlive() {
return lastKeepAlive;
}
@Override
public @NotNull HoverEvent
* WARNING: this alone does not sync the player, please use {@link #addViewer(Player)}.
*
* @param connection the connection to show the player to
*/
protected void showPlayer(@NotNull PlayerConnection connection) {
connection.sendPacket(getEntityType().registry().spawnType().getSpawnPacket(this));
connection.sendPacket(getVelocityPacket());
connection.sendPacket(getMetadataPacket());
connection.sendPacket(getEquipmentsPacket());
if (hasPassenger()) {
connection.sendPacket(getPassengersPacket());
}
connection.sendPacket(new EntityHeadLookPacket(getEntityId(), position.yaw()));
}
@Override
public @NotNull ItemStack getItemInMainHand() {
return inventory.getItemInMainHand();
}
@Override
public void setItemInMainHand(@NotNull ItemStack itemStack) {
inventory.setItemInMainHand(itemStack);
}
@Override
public @NotNull ItemStack getItemInOffHand() {
return inventory.getItemInOffHand();
}
@Override
public void setItemInOffHand(@NotNull ItemStack itemStack) {
inventory.setItemInOffHand(itemStack);
}
@Override
public @NotNull ItemStack getHelmet() {
return inventory.getHelmet();
}
@Override
public void setHelmet(@NotNull ItemStack itemStack) {
inventory.setHelmet(itemStack);
}
@Override
public @NotNull ItemStack getChestplate() {
return inventory.getChestplate();
}
@Override
public void setChestplate(@NotNull ItemStack itemStack) {
inventory.setChestplate(itemStack);
}
@Override
public @NotNull ItemStack getLeggings() {
return inventory.getLeggings();
}
@Override
public void setLeggings(@NotNull ItemStack itemStack) {
inventory.setLeggings(itemStack);
}
@Override
public @NotNull ItemStack getBoots() {
return inventory.getBoots();
}
@Override
public void setBoots(@NotNull ItemStack itemStack) {
inventory.setBoots(itemStack);
}
@Override
public Locale getLocale() {
final String locale = settings.locale;
if (locale == null) return null;
return Locale.forLanguageTag(locale.replace("_", "-"));
}
@Override
public @NotNull PlayerSnapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
final EntitySnapshot snapshot = super.updateSnapshot(updater);
return new SnapshotImpl.Player(snapshot, username, gameMode);
}
/**
* Sets the player's locale. This will only set the locale of the player as it
* is stored in the server. This will also be reset if the settings are refreshed.
*
* @param locale the new locale
*/
@Override
public void setLocale(@Nullable Locale locale) {
settings.locale = locale == null ? null : locale.toLanguageTag();
}
@Override
public @NotNull Identity identity() {
return this.identity;
}
@Override
public @NotNull Pointers pointers() {
return this.pointers;
}
@Override
public void setUuid(@NotNull UUID uuid) {
super.setUuid(uuid);
// update identity
this.identity = Identity.identity(uuid);
}
@Override
public boolean isPlayer() {
return true;
}
@Override
public Player asPlayer() {
return this;
}
protected void sendChunkUpdates(Chunk newChunk) {
if (chunkUpdateLimitChecker.addToHistory(newChunk)) {
final int newX = newChunk.getChunkX();
final int newZ = newChunk.getChunkZ();
final Vec old = chunksLoadedByClient;
sendPacket(new UpdateViewPositionPacket(newX, newZ));
ChunkUtils.forDifferingChunksInRange(newX, newZ, (int) old.x(), (int) old.z(),
settings.getEffectiveViewDistance(), chunkAdder, chunkRemover);
this.chunksLoadedByClient = new Vec(newX, newZ);
}
}
/**
* @see #teleport(Pos, long[], int)
*/
@Override
public @NotNull CompletableFuture
* WARNING: the player will not be noticed by this change, probably unsafe.
*
* @param locale the player locale
* @param viewDistance the player view distance
* @param chatMessageType the chat messages the player wishes to receive
* @param chatColors if chat colors should be displayed
* @param displayedSkinParts the player displayed skin parts
* @param mainHand the player main hand
*/
public void refresh(String locale, byte viewDistance, ChatMessageType chatMessageType, boolean chatColors,
byte displayedSkinParts, MainHand mainHand, boolean enableTextFiltering, boolean allowServerListings) {
this.locale = locale;
// Clamp viewDistance to valid bounds
this.viewDistance = (byte) MathUtils.clamp(viewDistance, 2, 32);
this.chatMessageType = chatMessageType;
this.chatColors = chatColors;
this.displayedSkinParts = displayedSkinParts;
this.mainHand = mainHand;
this.enableTextFiltering = enableTextFiltering;
this.allowServerListings = allowServerListings;
boolean isInPlayState = getPlayerConnection().getConnectionState() == ConnectionState.PLAY;
PlayerMeta playerMeta = getPlayerMeta();
if (isInPlayState) playerMeta.setNotifyAboutChanges(false);
playerMeta.setDisplayedSkinParts(displayedSkinParts);
playerMeta.setRightMainHand(this.mainHand == MainHand.RIGHT);
if (isInPlayState) playerMeta.setNotifyAboutChanges(true);
}
}
private int compareChunkDistance(long chunkIndexA, long chunkIndexB) {
int chunkAX = ChunkUtils.getChunkCoordX(chunkIndexA);
int chunkAZ = ChunkUtils.getChunkCoordZ(chunkIndexA);
int chunkBX = ChunkUtils.getChunkCoordX(chunkIndexB);
int chunkBZ = ChunkUtils.getChunkCoordZ(chunkIndexB);
int chunkDistanceA = Math.abs(chunkAX - chunksLoadedByClient.blockX()) + Math.abs(chunkAZ - chunksLoadedByClient.blockZ());
int chunkDistanceB = Math.abs(chunkBX - chunksLoadedByClient.blockX()) + Math.abs(chunkBZ - chunksLoadedByClient.blockZ());
return Integer.compare(chunkDistanceA, chunkDistanceB);
}
}