net.minestom.server.entity.Player Maven / Gradle / Ivy
Show all versions of minestom-snapshots Show documentation
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.ResourcePackInfo;
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.text.serializer.plain.PlainTextComponentSerializer;
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.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.attribute.Attribute;
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.ItemUsageCompleteEvent;
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.SharedInstance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemComponent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.component.WrittenBookContent;
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.registry.DynamicRegistry;
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.thread.Acquirable;
import net.minestom.server.timer.Scheduler;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.PacketUtils;
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.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.MpscArrayQueue;
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, Identified, NamedAndIdentified {
private static final Logger logger = LoggerFactory.getLogger(Player.class);
private static final DynamicRegistry DIMENSION_TYPE_REGISTRY = MinecraftServer.getDimensionTypeRegistry();
private static final Component REMOVE_MESSAGE = Component.text("You have been removed from the server without reason.", NamedTextColor.RED);
private static final Component MISSING_REQUIRED_RESOURCE_PACK = Component.text("Required resource pack was not loaded.", NamedTextColor.RED);
// Magic values: https://wiki.vg/Entity_statuses#Player
private static final int STATUS_ENABLE_REDUCED_DEBUG_INFO = 22;
private static final int STATUS_DISABLE_REDUCED_DEBUG_INFO = 23;
private static final int STATUS_PERMISSION_LEVEL_OFFSET = 24;
private long lastKeepAlive;
private boolean answerKeepAlive;
private String username;
private Component usernameComponent;
protected final PlayerConnection playerConnection;
private volatile int latency;
private Component displayName;
private PlayerSkin skin;
private Instance pendingInstance = null;
private int dimensionTypeId;
private GameMode gameMode;
private WorldPos deathLocation;
/**
* Keeps track of what chunks are sent to the client, this defines the center of the loaded area
* in the range of {@link ServerFlag#CHUNK_VIEW_DISTANCE}
*/
private Vec chunksLoadedByClient = Vec.ZERO;
private final ReentrantLock chunkQueueLock = new ReentrantLock();
private final LongPriorityQueue chunkQueue = new LongArrayPriorityQueue(this::compareChunkDistance);
private boolean needsChunkPositionSync = true;
private float targetChunksPerTick = 9f; // Always send 9 chunks immediately
private float pendingChunkCount = 0f; // Number of chunks to send on the current tick (ie 0.5 means we cannot send a chunk yet, 1.5 would send a single chunk with a 0.5 remainder)
private int maxChunkBatchLead = 1; // Maximum number of batches to send before waiting for a reply
private int chunkBatchLead = 0; // Number of batches sent without a reply
final IntegerBiConsumer chunkAdder = (chunkX, chunkZ) -> {
// Load new chunks
this.instance.loadOptionalChunk(chunkX, chunkZ).thenAccept(this::sendChunk);
};
final IntegerBiConsumer chunkRemover = (chunkX, chunkZ) -> {
// Unload old chunks
sendPacket(new UnloadChunkPacket(chunkX, chunkZ));
EventDispatcher.call(new PlayerChunkUnloadEvent(this, chunkX, chunkZ));
};
private final AtomicInteger teleportId = new AtomicInteger();
private int receivedTeleportId;
private final MpscArrayQueue packets = new MpscArrayQueue<>(ServerFlag.PLAYER_PACKET_QUEUE_SIZE);
private final boolean levelFlat;
private final PlayerSettings settings;
private float exp;
private int level;
private int portalCooldown = 0;
protected PlayerInventory inventory;
private Inventory openInventory;
// Used internally to allow the closing of inventory within the inventory listener
private boolean didCloseInventory;
private byte heldSlot;
private Pos respawnPoint;
private int food;
private float foodSaturation;
private long startItemUseTime;
private long itemUseTime;
private Hand itemUseHand;
// Game state (https://wiki.vg/Protocol#Change_Game_State)
private boolean enableRespawnScreen;
private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(6);
// Experience orb pickup
protected Cooldown experiencePickupCooldown = new Cooldown(Duration.of(10, TimeUnit.SERVER_TICK));
private BelowNameTag belowNameTag;
private int permissionLevel;
private boolean reducedDebugScreenInformation;
private boolean hardcore;
// Abilities
private boolean flying;
private boolean allowFlying;
private boolean instantBreak;
private float flyingSpeed = 0.05f;
private float fieldViewModifier = 0.1f;
// Statistics
private final Map statisticValueMap = new Hashtable<>();
// Vehicle
private final PlayerVehicleInformation vehicleInformation = new PlayerVehicleInformation();
// Adventure
private final Identity identity;
private final Pointers pointers;
// Resource packs
record PendingResourcePack(boolean required, @NotNull ResourcePackCallback callback) {
}
private final Map pendingResourcePacks = new HashMap<>();
// The future is non-null when a resource pack is in-flight, and completed when all statuses have been received.
private CompletableFuture resourcePackFuture = null;
public Player(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection playerConnection) {
super(EntityType.PLAYER, uuid);
this.username = username;
this.usernameComponent = Component.text(username);
this.playerConnection = playerConnection;
setRespawnPoint(Pos.ZERO);
this.settings = new PlayerSettings();
this.inventory = new PlayerInventory(this);
setCanPickupItem(true); // By default
// Allow the server to send the next keep alive packet
refreshAnswerKeepAlive(true);
this.gameMode = GameMode.SURVIVAL;
this.dimensionTypeId = DIMENSION_TYPE_REGISTRY.getId(DimensionType.OVERWORLD); // Default dimension
this.levelFlat = true;
getAttribute(Attribute.GENERIC_MOVEMENT_SPEED).setBaseValue(0.1);
// FakePlayer init its connection there
playerConnectionInit();
this.identity = Identity.identity(uuid);
this.pointers = Pointers.builder()
.withDynamic(Identity.UUID, this::getUuid)
.withDynamic(Identity.NAME, this::getUsername)
.withDynamic(Identity.DISPLAY_NAME, this::getDisplayName)
.build();
// When in configuration state no metadata updates can be sent.
metadata.setNotifyAboutChanges(false);
}
@ApiStatus.Internal
public void setPendingOptions(@NotNull Instance pendingInstance, boolean hardcore) {
// I(mattw) am not a big fan of this function, but somehow we need to store
// the instance and i didn't like a record in ConnectionManager either.
this.pendingInstance = pendingInstance;
this.hardcore = hardcore;
}
/**
* Used when the player is created.
* Init the player and spawn him.
*
* 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 UNSAFE_init() {
final Instance spawnInstance = this.pendingInstance;
this.pendingInstance = null;
this.removed = false;
this.dimensionTypeId = DIMENSION_TYPE_REGISTRY.getId(spawnInstance.getDimensionType().namespace());
final JoinGamePacket joinGamePacket = new JoinGamePacket(
getEntityId(), this.hardcore, List.of(), 0,
ServerFlag.CHUNK_VIEW_DISTANCE, ServerFlag.CHUNK_VIEW_DISTANCE,
false, true, false, dimensionTypeId, spawnInstance.getDimensionName(),
0, gameMode, null, false, levelFlat, deathLocation, portalCooldown, true);
sendPacket(joinGamePacket);
// Difficulty
sendPacket(new ServerDifficultyPacket(MinecraftServer.getDifficulty(), true));
sendPacket(new SpawnPositionPacket(respawnPoint, 0));
// Reenable metadata notifications as we leave the configuration state
metadata.setNotifyAboutChanges(true);
sendPacket(getMetadataPacket());
// Add player to list with spawning skin
PlayerSkin profileSkin = null;
if (playerConnection instanceof PlayerSocketConnection socketConnection) {
final GameProfile gameProfile = socketConnection.gameProfile();
if (gameProfile != null) {
for (GameProfile.Property property : gameProfile.properties()) {
if (property.name().equals("textures")) {
profileSkin = new PlayerSkin(property.value(), property.signature());
break;
}
}
}
}
PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this, profileSkin);
EventDispatcher.call(skinInitEvent);
this.skin = skinInitEvent.getSkin();
// FIXME: when using Geyser, this line remove the skin of the client
PacketUtils.broadcastPlayPacket(getAddPlayerToList());
var connectionManager = MinecraftServer.getConnectionManager();
for (var player : connectionManager.getOnlinePlayers()) {
if (player != this) {
sendPacket(player.getAddPlayerToList());
if (player.displayName != null) {
sendPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, player.infoEntry()));
}
}
}
//Teams
for (Team team : MinecraftServer.getTeamManager().getTeams()) {
sendPacket(team.createTeamsCreationPacket());
}
// Commands
refreshCommands();
// Recipes start
{
RecipeManager recipeManager = MinecraftServer.getRecipeManager();
sendPacket(recipeManager.getDeclareRecipesPacket());
List recipesIdentifier = new ArrayList<>();
for (Recipe recipe : recipeManager.getRecipes()) {
if (!recipe.shouldShow(this))
continue;
recipesIdentifier.add(recipe.id());
}
if (!recipesIdentifier.isEmpty()) {
UnlockRecipesPacket unlockRecipesPacket = new UnlockRecipesPacket(0,
false, false,
false, false,
false, false,
false, false,
recipesIdentifier, recipesIdentifier);
sendPacket(unlockRecipesPacket);
}
}
// Recipes end
// Some client updates
sendPacket(getPropertiesPacket()); // Send default properties
triggerStatus((byte) (STATUS_PERMISSION_LEVEL_OFFSET + permissionLevel)); // Set permission level
refreshHealth(); // Heal and send health packet
refreshAbilities(); // Send abilities packet
return setInstance(spawnInstance);
}
/**
* Moves the player immediately to the configuration state. The player is automatically moved
* to configuration upon finishing login, this method can be used to move them back to configuration
* after entering the play state.
*
* This will result in them being removed from the current instance, player list, etc.
*/
public void startConfigurationPhase() {
Check.stateCondition(playerConnection.getConnectionState() != ConnectionState.PLAY,
"Player must be in the play state for reconfiguration.");
// Remove the player, then send them back to configuration
remove(false);
var connectionManager = MinecraftServer.getConnectionManager();
connectionManager.transitionPlayToConfig(this);
}
/**
* Used to initialize the player connection
*/
protected void playerConnectionInit() {
PlayerConnection connection = playerConnection;
if (connection != null) connection.setPlayer(this);
}
@Override
public void update(long time) {
// Process received packets
interpretPacketQueue();
// It is possible to be removed during packet processing, if thats the case exit immediately.
if (isRemoved()) return;
// Send any available queued chunks
sendPendingChunks();
super.update(time); // Super update (item pickup/fire management)
// Experience orb pickup
if (experiencePickupCooldown.isReady(time)) {
experiencePickupCooldown.refreshLastUpdate(time);
final Point loweredPosition = position.sub(0, .5, 0);
this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.width(),
EntityTracker.Target.EXPERIENCE_ORBS, experienceOrb -> {
if (expandedBoundingBox.intersectEntity(loweredPosition, experienceOrb)) {
PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(this, experienceOrb);
EventDispatcher.callCancellable(pickupExperienceEvent, () -> {
short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player
experienceOrb.remove();
});
}
});
}
// Eating animation
if (isUsingItem()) {
if (itemUseTime > 0 && getCurrentItemUseTime() >= itemUseTime) {
triggerStatus((byte) 9); // Mark item use as finished
ItemUpdateStateEvent itemUpdateStateEvent = callItemUpdateStateEvent(itemUseHand);
// Refresh hand
final boolean isOffHand = itemUpdateStateEvent.getHand() == Player.Hand.OFF;
refreshActiveHand(false, isOffHand, false);
final ItemStack item = itemUpdateStateEvent.getItemStack();
final boolean isFood = item.has(ItemComponent.FOOD);
if (isFood || item.material() == Material.POTION) {
PlayerEatEvent playerEatEvent = new PlayerEatEvent(this, item, itemUseHand);
EventDispatcher.call(playerEatEvent);
}
var itemUsageCompleteEvent = new ItemUsageCompleteEvent(this, itemUseHand, item);
EventDispatcher.call(itemUsageCompleteEvent);
clearItemUse();
}
}
updatePose();
// Tick event
EventDispatcher.call(new PlayerTickEvent(this));
}
@Override
public void kill() {
if (!isDead()) {
Component deathText;
Component chatMessage;
// get death screen text to the killed player
{
if (lastDamage != null) {
deathText = lastDamage.buildDeathScreenText(this);
} else { // may happen if killed by the server without applying damage
deathText = Component.text("Killed by poor programming.");
}
}
// get death message to chat
{
if (lastDamage != null) {
chatMessage = lastDamage.buildDeathMessage(this);
} else { // may happen if killed by the server without applying damage
chatMessage = Component.text(getUsername() + " was killed by poor programming.");
}
}
// Call player death event
PlayerDeathEvent playerDeathEvent = new PlayerDeathEvent(this, deathText, chatMessage);
EventDispatcher.call(playerDeathEvent);
deathText = playerDeathEvent.getDeathText();
chatMessage = playerDeathEvent.getChatMessage();
// #buildDeathScreenText can return null, check here
if (deathText != null) {
sendPacket(new DeathCombatEventPacket(getEntityId(), deathText));
}
// #buildDeathMessage can return null, check here
if (chatMessage != null) {
Audiences.players().sendMessage(chatMessage);
}
// Set death location
if (getInstance() != null)
setDeathLocation(getInstance().getDimensionName(), getPosition());
}
super.kill();
}
/**
* Respawns the player by sending a {@link RespawnPacket} to the player and teleporting him
* to {@link #getRespawnPoint()}. It also resets fire and health.
*/
public void respawn() {
if (!isDead())
return;
setFireTicks(0);
entityMeta.setOnFire(false);
refreshHealth();
sendPacket(new RespawnPacket(dimensionTypeId, instance.getDimensionName(), 0, gameMode, gameMode,
false, levelFlat, deathLocation, portalCooldown, RespawnPacket.COPY_ALL));
refreshClientStateAfterRespawn();
PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this);
EventDispatcher.call(respawnEvent);
refreshIsDead(false);
updatePose();
Pos respawnPosition = respawnEvent.getRespawnPosition();
// The client unloads chunks when respawning, so resend all chunks next to spawn
ChunkUtils.forChunksInRange(respawnPosition, settings.getEffectiveViewDistance(), chunkAdder);
chunksLoadedByClient = new Vec(respawnPosition.chunkX(), respawnPosition.chunkZ());
// Client also needs all entities resent to them, since those are unloaded as well
this.instance.getEntityTracker().nearbyEntitiesByChunkRange(respawnPosition, settings.getEffectiveViewDistance(),
EntityTracker.Target.ENTITIES, entity -> {
// Skip refreshing self with a new viewer
if (!entity.getUuid().equals(getUuid()) && entity.isViewer(this)) {
entity.updateNewViewer(this);
}
});
teleport(respawnPosition).thenRun(this::refreshAfterTeleport);
}
/**
* Sends necessary packets to synchronize player data after a {@link RespawnPacket}
*/
private void refreshClientStateAfterRespawn() {
sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.LEVEL_CHUNKS_LOAD_START, 0));
sendPacket(new ServerDifficultyPacket(MinecraftServer.getDifficulty(), false));
sendPacket(new UpdateHealthPacket(this.getHealth(), food, foodSaturation));
sendPacket(new SetExperiencePacket(exp, level, 0));
triggerStatus((byte) (STATUS_PERMISSION_LEVEL_OFFSET + permissionLevel)); // Set permission level
refreshAbilities();
}
/**
* Refreshes the command list for this player. This checks the
* {@link net.minestom.server.command.builder.condition.CommandCondition}s
* again, and any changes will be visible to the player.
*/
public void refreshCommands() {
sendPacket(MinecraftServer.getCommandManager().createDeclareCommandsPacket(this));
}
@Override
public boolean isOnGround() {
return onGround;
}
@Override
public void remove(boolean permanent) {
if (isRemoved()) return;
if (permanent) {
this.packets.clear();
EventDispatcher.call(new PlayerDisconnectEvent(this));
}
super.remove(permanent);
final Inventory currentInventory = getOpenInventory();
if (currentInventory != null) currentInventory.removeViewer(this);
MinecraftServer.getBossBarManager().removeAllBossBars(this);
// Advancement tabs cache
{
Set advancementTabs = AdvancementTab.getTabs(this);
if (advancementTabs != null) {
for (AdvancementTab advancementTab : advancementTabs) {
advancementTab.removeViewer(this);
}
}
}
final Pos position = this.position;
final int chunkX = position.chunkX();
final int chunkZ = position.chunkZ();
// Clear all viewable chunks
ChunkUtils.forChunksInRange(chunkX, chunkZ, settings.getEffectiveViewDistance(), chunkRemover);
// Remove from the tab-list
PacketUtils.broadcastPlayPacket(getRemovePlayerToList());
// Prevent the player from being stuck in loading screen, or just unable to interact with the server
// This should be considered as a bug, since the player will ultimately time out anyway.
if (permanent && playerConnection.isOnline()) kick(REMOVE_MESSAGE);
}
@Override
public void sendPacketToViewersAndSelf(@NotNull SendablePacket packet) {
sendPacket(packet);
super.sendPacketToViewersAndSelf(packet);
}
/**
* Changes the player instance and load surrounding chunks if needed.
*
* 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 setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
final Instance currentInstance = this.instance;
Check.argCondition(currentInstance == instance, "Instance should be different than the current one");
if (SharedInstance.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
spawnPlayer(instance, spawnPosition, false, false, false);
return AsyncUtils.VOID_FUTURE;
}
// Must update the player chunks
chunkUpdateLimitChecker.clearHistory();
final boolean dimensionChange = currentInstance != null && !Objects.equals(currentInstance.getDimensionName(), instance.getDimensionName());
final Consumer runnable = (i) -> spawnPlayer(i, spawnPosition,
currentInstance == null, dimensionChange, true);
// Reset chunk queue state
needsChunkPositionSync = true;
targetChunksPerTick = 9f;
pendingChunkCount = 0f;
// Ensure that surrounding chunks are loaded
List> futures = new ArrayList<>();
ChunkUtils.forChunksInRange(spawnPosition, settings.getEffectiveViewDistance(), (chunkX, chunkZ) -> {
final CompletableFuture future = instance.loadOptionalChunk(chunkX, chunkZ);
if (!future.isDone()) futures.add(future);
});
if (futures.isEmpty()) {
// All chunks are already loaded
runnable.accept(instance);
return AsyncUtils.VOID_FUTURE;
}
// One or more chunks need to be loaded
final Thread runThread = Thread.currentThread();
CountDownLatch latch = new CountDownLatch(1);
Scheduler scheduler = MinecraftServer.getSchedulerManager();
CompletableFuture future = new CompletableFuture<>() {
@Override
public Void join() {
// Prevent deadlock
if (runThread == Thread.currentThread()) {
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
scheduler.process();
assert isDone();
}
return super.join();
}
};
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.thenRun(() -> {
scheduler.scheduleNextProcess(() -> {
runnable.accept(instance);
future.complete(null);
});
latch.countDown();
});
return future;
}
/**
* Changes the player instance without changing its position (defaulted to {@link #getRespawnPoint()}
* if the player is not in any instance).
*
* @param instance the new player instance
* @return a {@link CompletableFuture} called once the entity's instance has been set,
* this is due to chunks needing to load for players
* @see #setInstance(Instance, Pos)
*/
@Override
public CompletableFuture setInstance(@NotNull Instance instance) {
return setInstance(instance, this.instance != null ? getPosition() : getRespawnPoint());
}
/**
* Used to spawn the player once the client has all the required chunks.
*
* 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);
sendPendingChunks(); // Send available first chunk immediately to prevent falling through the floor
}
synchronizePositionAfterTeleport(spawnPosition, 0, true); // So the player doesn't get stuck
if (dimensionChange) {
sendPacket(new SpawnPositionPacket(spawnPosition, 0));
sendPacket(instance.createInitializeWorldBorderPacket());
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) ? ServerFlag.MIN_CHUNKS_PER_TICK : MathUtils.clamp(
newTargetChunksPerTick * ServerFlag.CHUNKS_PER_TICK_MULTIPLIER, ServerFlag.MIN_CHUNKS_PER_TICK, ServerFlag.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, ServerFlag.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);
// After sending the first chunk we always send a synchronize position to the client. This is to prevent
// cases where the client falls through the floor slightly while loading the first chunk.
// In the vanilla server they have an anticheat which teleports the client back if they enter the floor,
// but since Minestom does not have an anticheat this provides a similar effect.
if (needsChunkPositionSync) {
synchronizePositionAfterTeleport(getPosition(), RelativeFlags.NONE, true);
needsChunkPositionSync = false;
}
} 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.blockX(), pos.blockY(), pos.blockZ(), 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;
}
@Override
@SuppressWarnings({"UnstableApiUsage", "deprecation"})
public void sendMessage(final @NotNull Identity source, final @NotNull Component message, final @NotNull MessageType type) {
// Note to readers: this method may be deprecated, however it is in fact required.
Messenger.sendMessage(this, message, ChatPosition.fromMessageType(type), source.uuid());
}
/**
* 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));
}
@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 void sendTitlePart(@NotNull TitlePart part, @NotNull T value) {
sendPacket(AdventurePacketConvertor.createTitlePartPacket(part, value));
}
@Override
public void sendActionBar(@NotNull Component message) {
sendPacket(new ActionBarPacket(message));
}
@Override
public void resetTitle() {
sendPacket(new ClearTitlesPacket(true));
}
@Override
public void clearTitle() {
sendPacket(new ClearTitlesPacket(false));
}
@Override
public void showBossBar(@NotNull BossBar bar) {
MinecraftServer.getBossBarManager().addBossBar(this, bar);
}
@Override
public void hideBossBar(@NotNull BossBar bar) {
MinecraftServer.getBossBarManager().removeBossBar(this, bar);
}
@Override
public void openBook(@NotNull Book book) {
// Close the open inventory if there is one because the book will replace it.
if (getOpenInventory() != null) {
closeInventory();
}
// TODO: when adventure updates, delete this
String title = PlainTextComponentSerializer.plainText().serialize(book.title());
String author = PlainTextComponentSerializer.plainText().serialize(book.author());
final ItemStack writtenBook = ItemStack.builder(Material.WRITTEN_BOOK)
.set(ItemComponent.WRITTEN_BOOK_CONTENT, new WrittenBookContent(book.pages(), title, author, 0, false))
.build();
// Set book in offhand
sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFFHAND_SLOT, writtenBook));
// Open the book
sendPacket(new OpenBookPacket(Hand.OFF));
// Restore the item in offhand
sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFFHAND_SLOT, getItemInOffHand()));
}
@Override
public boolean isImmune(@NotNull DynamicRegistry.Key type) {
if (!getGameMode().canTakeDamage()) {
return !DamageType.OUT_OF_WORLD.equals(type);
}
return super.isImmune(type);
}
@Override
public void setHealth(float health) {
sendPacket(new UpdateHealthPacket(health, food, foodSaturation));
super.setHealth(health);
}
/**
* Gets the entity meta for the player.
*
* 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.
*/
public @NotNull PlayerMeta getPlayerMeta() {
return (PlayerMeta) super.getEntityMeta();
}
/**
* Gets the player additional hearts.
*
* Note that this function is uncallable if the player has their entity type switched
* with {@link #switchEntityType(EntityType)}.
*
* @return the player additional hearts
*/
public float getAdditionalHearts() {
return getPlayerMeta().getAdditionalHearts();
}
/**
* Changes the amount of additional hearts shown.
*
* Note that this function is uncallable if the player has their entity type switched
* with {@link #switchEntityType(EntityType)}.
*
* @param additionalHearts the count of additional hearts
*/
public void setAdditionalHearts(float additionalHearts) {
getPlayerMeta().setAdditionalHearts(additionalHearts);
}
/**
* Gets the player food.
*
* @return the player food
*/
public int getFood() {
return food;
}
/**
* Sets and refresh client food bar.
*
* @param food the new food value
* @throws IllegalArgumentException if {@code food} is not between 0 and 20
*/
public void setFood(int food) {
Check.argCondition(!MathUtils.isBetween(food, 0, 20),
"Food has to be between 0 and 20");
this.food = food;
sendPacket(new UpdateHealthPacket(getHealth(), food, foodSaturation));
}
public float getFoodSaturation() {
return foodSaturation;
}
/**
* Sets and refresh client food saturation.
*
* @param foodSaturation the food saturation
* @throws IllegalArgumentException if {@code foodSaturation} is not between 0 and 20
*/
public void setFoodSaturation(float foodSaturation) {
Check.argCondition(!MathUtils.isBetween(foodSaturation, 0, 20),
"Food saturation has to be between 0 and 20");
this.foodSaturation = foodSaturation;
sendPacket(new UpdateHealthPacket(getHealth(), food, foodSaturation));
}
/**
* Gets if the player is eating.
*
* @return true if the player is eating, false otherwise
*/
public boolean isEating() {
if (!isUsingItem()) return false;
final ItemStack itemStack = getItemInHand(itemUseHand);
return itemStack.has(ItemComponent.FOOD) || itemStack.material() == Material.POTION;
}
/**
* Gets if the player is using an item.
*
* @return true if the player is using an item, false otherwise
*/
public boolean isUsingItem() {
return itemUseHand != null;
}
/**
* Gets the hand which the player is using an item from.
*
* @return the item use hand, null if none
*/
public @Nullable Hand getItemUseHand() {
return itemUseHand;
}
/**
* Gets the amount of ticks which have passed since the player started using an item.
*
* @return the amount of ticks which have passed, or zero if the player is not using an item
*/
public long getCurrentItemUseTime() {
if (!isUsingItem()) return 0;
return getAliveTicks() - startItemUseTime;
}
@Override
public double getEyeHeight() {
return switch (getPose()) {
case SLEEPING -> 0.2;
case FALL_FLYING, SWIMMING, SPIN_ATTACK -> 0.4;
case SNEAKING -> 1.27;
default -> 1.62;
};
}
/**
* Gets the player display name in the tab-list.
*
* @return the player display name, null means that {@link #getUsername()} is displayed
*/
public @Nullable Component getDisplayName() {
return displayName;
}
/**
* Changes the player display name in the tab-list.
*
* 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(dimensionTypeId, 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 Pos position) {
setDeathLocation(getInstance().getDimensionName(), position);
}
public void setDeathLocation(@NotNull String dimension, @NotNull Pos position) {
this.deathLocation = new WorldPos(dimension, 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 (final ResourcePackInfo pack : request.packs()) {
sendPacket(new ResourcePackPushPacket(pack, request.required(), request.prompt()));
pendingResourcePacks.put(pack.id(), new PendingResourcePack(request.required(), 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 getResourcePackFuture() {
return resourcePackFuture;
}
@ApiStatus.Internal
public void onResourcePackStatus(@NotNull UUID id, @NotNull ResourcePackStatus status) {
var pendingPack = pendingResourcePacks.get(id);
if (pendingPack == null) return;
pendingPack.callback().packEventReceived(id, status, this);
if (!status.intermediate()) {
// Remove the callback and finish the future if relevant
pendingResourcePacks.remove(id);
// If the resource pack is required and failed to load, bye bye!
if (pendingPack.required() && status != ResourcePackStatus.SUCCESSFULLY_LOADED) {
kick(MISSING_REQUIRED_RESOURCE_PACK);
}
if (pendingResourcePacks.isEmpty() && resourcePackFuture != null) {
resourcePackFuture.complete(null);
resourcePackFuture = null;
}
}
}
/**
* Rotates the player to face {@code targetPosition}.
*
* @param facePoint the point from where the player should aim
* @param targetPosition the target position to face
*/
public void facePosition(@NotNull FacePoint facePoint, @NotNull Point targetPosition) {
facePosition(facePoint, targetPosition, null, null);
}
/**
* Rotates the player to face {@code entity}.
*
* @param facePoint the point from where the player should aim
* @param entity the entity to face
* @param targetPoint the point to aim at {@code entity} position
*/
public void facePosition(@NotNull FacePoint facePoint, Entity entity, FacePoint targetPoint) {
facePosition(facePoint, entity.getPosition(), entity, targetPoint);
}
private void facePosition(@NotNull FacePoint facePoint, @NotNull Point targetPosition,
@Nullable Entity entity, @Nullable FacePoint targetPoint) {
final int entityId = entity != null ? entity.getEntityId() : 0;
sendPacket(new FacePlayerPacket(
facePoint == FacePoint.EYE ?
FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET, targetPosition,
entityId,
targetPoint == FacePoint.EYE ?
FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET));
}
/**
* Sets the camera at {@code entity} eyes.
*
* @param entity the entity to spectate
*/
public void spectate(@NotNull Entity entity) {
sendPacket(new CameraPacket(entity));
}
/**
* Resets the camera at the player.
*/
public void stopSpectating() {
spectate(this);
}
/**
* Used to retrieve the default spawn point.
*
* 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
*/
public void sendPacket(@NotNull SendablePacket packet) {
this.playerConnection.sendPacket(packet);
}
public void sendPackets(@NotNull SendablePacket... packets) {
this.playerConnection.sendPackets(packets);
}
public void sendPackets(@NotNull Collection packets) {
this.playerConnection.sendPackets(packets);
}
/**
* Gets if the player is online or not.
*
* @return true if the player is online, false otherwise
*/
public boolean isOnline() {
return playerConnection.isOnline();
}
/**
* Gets the player settings.
*
* @return the player settings
*/
public @NotNull PlayerSettings getSettings() {
return settings;
}
/**
* Gets the player dimension.
*
* @return the player current dimension
*/
public DimensionType getDimensionType() {
return DIMENSION_TYPE_REGISTRY.get(dimensionTypeId);
}
public @NotNull PlayerInventory getInventory() {
return inventory;
}
/**
* Used to get the player latency,
* computed by seeing how long it takes the client to answer the {@link KeepAlivePacket} packet.
*
* @return the player latency
*/
public int getLatency() {
return latency;
}
/**
* Gets the player {@link GameMode}.
*
* @return the player current gamemode
*/
public GameMode getGameMode() {
return gameMode;
}
/**
* Changes the player {@link GameMode}
*
* @param gameMode the new player GameMode
* @return true if the gamemode was changed successfully, false otherwise (cancelled by event)
*/
public boolean setGameMode(@NotNull GameMode gameMode) {
PlayerGameModeChangeEvent playerGameModeChangeEvent = new PlayerGameModeChangeEvent(this, gameMode);
EventDispatcher.call(playerGameModeChangeEvent);
if (playerGameModeChangeEvent.isCancelled()) {
// Abort
return false;
}
gameMode = playerGameModeChangeEvent.getNewGameMode();
this.gameMode = gameMode;
// Condition to prevent sending the packets before spawning the player
if (isActive()) {
sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.id()));
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, infoEntry()));
}
// The client updates their abilities based on the GameMode as follows
switch (gameMode) {
case CREATIVE -> {
this.allowFlying = true;
this.instantBreak = true;
this.invulnerable = true;
}
case SPECTATOR -> {
this.allowFlying = true;
this.instantBreak = false;
this.invulnerable = true;
if (isActive()) {
refreshFlying(true);
} else {
this.flying = true;
}
}
default -> {
this.allowFlying = false;
this.instantBreak = false;
this.invulnerable = false;
if (isActive()) {
refreshFlying(false);
} else {
this.flying = false;
}
}
}
// Make sure that the player is in the PLAY state and synchronize their flight speed.
if (isActive()) {
refreshAbilities();
updateCollisions();
}
return true;
}
/**
* Changes the dimension of the player.
* Mostly unsafe since it requires sending chunks after.
*
* @param dimensionType the new player dimension
*/
protected void sendDimension(@NotNull DynamicRegistry.Key dimensionType, @NotNull String dimensionName) {
Check.argCondition(instance.getDimensionName().equals(dimensionName),
"The dimension needs to be different than the current one!");
this.dimensionTypeId = DIMENSION_TYPE_REGISTRY.getId(dimensionType);
sendPacket(new RespawnPacket(dimensionTypeId, dimensionName,
0, gameMode, gameMode, false, levelFlat,
deathLocation, portalCooldown, RespawnPacket.COPY_ALL));
refreshClientStateAfterRespawn();
}
/**
* Kicks the player with a reason.
*
* @param component the reason
*/
public void kick(@NotNull Component component) {
// Packet type depends on the current player connection state
final ServerPacket disconnectPacket;
if (playerConnection.getConnectionState() == ConnectionState.LOGIN) {
disconnectPacket = new LoginDisconnectPacket(component);
} else {
disconnectPacket = new DisconnectPacket(component);
}
sendPacket(disconnectPacket);
playerConnection.disconnect();
}
/**
* Kicks the player with a reason.
*
* @param message the kick reason
*/
public void kick(@NotNull String message) {
this.kick(Component.text(message));
}
/**
* Changes the current held slot for the player.
*
* @param slot the slot that the player has to held
* @throws IllegalArgumentException if {@code slot} is not between 0 and 8
*/
public void setHeldItemSlot(byte slot) {
Check.argCondition(!MathUtils.isBetween(slot, 0, 8), "Slot has to be between 0 and 8");
refreshHeldSlot(slot);
sendPacket(new HeldItemChangePacket(slot));
}
/**
* Gets the player held slot (0-8).
*
* @return the current held slot for the player
*/
public byte getHeldSlot() {
return heldSlot;
}
/**
* Changes the tag below the name.
*
* @param belowNameTag The new below name tag
*/
public void setBelowNameTag(BelowNameTag belowNameTag) {
if (this.belowNameTag == belowNameTag) return;
if (this.belowNameTag != null) {
this.belowNameTag.removeViewer(this);
}
this.belowNameTag = belowNameTag;
}
/**
* Gets the player open inventory.
*
* @return the currently open inventory, null if there is not (player inventory is not detected)
*/
public @Nullable Inventory getOpenInventory() {
return openInventory;
}
/**
* Opens the specified Inventory, close the previous inventory if existing.
*
* @param inventory the inventory to open
* @return true if the inventory has been opened/sent to the player, false otherwise (cancelled by event)
*/
public boolean openInventory(@NotNull Inventory inventory) {
InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this);
EventDispatcher.callCancellable(inventoryOpenEvent, () -> {
Inventory openInventory = getOpenInventory();
if (openInventory != null) {
openInventory.removeViewer(this);
}
Inventory newInventory = inventoryOpenEvent.getInventory();
if (newInventory == null) {
// just close the inventory
return;
}
sendPacket(new OpenWindowPacket(newInventory.getWindowId(),
newInventory.getInventoryType().getWindowType(), newInventory.getTitle()));
newInventory.addViewer(this);
this.openInventory = newInventory;
});
return !inventoryOpenEvent.isCancelled();
}
/**
* Closes the current inventory if there is any.
* It closes the player inventory (when opened) if {@link #getOpenInventory()} returns null.
*/
public void closeInventory() {
closeInventory(false);
}
@ApiStatus.Internal
public void closeInventory(boolean fromClient) {
Inventory openInventory = getOpenInventory();
// Drop cursor item when closing inventory
ItemStack cursorItem = getInventory().getCursorItem();
getInventory().setCursorItem(ItemStack.AIR);
if (!cursorItem.isAir()) {
// Add item to inventory if he hasn't been able to drop it
if (!dropItem(cursorItem)) {
getInventory().addItemStack(cursorItem);
}
}
if (openInventory == getOpenInventory()) {
CloseWindowPacket closeWindowPacket;
if (openInventory == null) {
closeWindowPacket = new CloseWindowPacket((byte) 0);
} else {
closeWindowPacket = new CloseWindowPacket(openInventory.getWindowId());
openInventory.removeViewer(this); // Clear cache
this.openInventory = null;
}
if (!fromClient) sendPacket(closeWindowPacket);
inventory.update();
this.didCloseInventory = true;
}
}
/**
* Used internally to determine when sending the close inventory packet should be skipped.
*/
public boolean didCloseInventory() {
return didCloseInventory;
}
/**
* Used internally to reset the skipClosePacket field, which determines when sending the close inventory packet
* should be skipped.
*
* 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) {
if (receivedTeleportId < 0) return;
this.receivedTeleportId = receivedTeleportId;
}
/**
* Used to synchronize player position with viewers on spawn or after {@link Entity#teleport(Pos, long[], int)}
* in properties 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}
* @param shouldConfirm if false, the teleportation will be done without confirmation
*/
@ApiStatus.Internal
void synchronizePositionAfterTeleport(@NotNull Pos position, int relativeFlags, boolean shouldConfirm) {
int teleportId = shouldConfirm ? getNextTeleportId() : -1;
sendPacket(new PlayerPositionAndLookPacket(position, (byte) relativeFlags, teleportId));
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 getStatisticValueMap() {
return statisticValueMap;
}
/**
* Gets the player vehicle information.
*
* @return the player vehicle information
*/
public @NotNull PlayerVehicleInformation getVehicleInformation() {
return vehicleInformation;
}
/**
* Sends to the player a {@link PlayerAbilitiesPacket} with all the updated fields.
*/
protected void refreshAbilities() {
byte flags = 0;
if (invulnerable)
flags |= PlayerAbilitiesPacket.FLAG_INVULNERABLE;
if (flying)
flags |= PlayerAbilitiesPacket.FLAG_FLYING;
if (allowFlying)
flags |= PlayerAbilitiesPacket.FLAG_ALLOW_FLYING;
if (instantBreak)
flags |= PlayerAbilitiesPacket.FLAG_INSTANT_BREAK;
sendPacket(new PlayerAbilitiesPacket(flags, flyingSpeed, fieldViewModifier));
}
/**
* All packets in the queue are executed in the {@link #update(long)} method
* It is used internally to add all received packet from the client.
* Could be used to "simulate" a received packet, but to use at your own risk.
*
* @param packet the packet to add in the queue
*/
public void addPacketToQueue(@NotNull ClientPacket packet) {
final boolean success = packets.offer(packet);
if (!success) {
kick(Component.text("Too Many Packets", NamedTextColor.RED));
}
}
@ApiStatus.Internal
public void interpretPacketQueue() {
final PacketListenerManager manager = MinecraftServer.getPacketListenerManager();
// This method is NOT thread-safe
this.packets.drain(packet -> manager.processClientPacket(packet, playerConnection), ServerFlag.PLAYER_PACKET_PER_TICK);
}
/**
* Changes the storage player latency and update its tab value.
*
* @param latency the new player latency
*/
public void refreshLatency(int latency) {
this.latency = latency;
if (getPlayerConnection().getConnectionState() == ConnectionState.PLAY) {
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LATENCY, infoEntry()));
}
}
public void refreshOnGround(boolean onGround) {
this.onGround = onGround;
if (this.onGround && this.isFlyingWithElytra()) {
this.setFlyingWithElytra(false);
EventDispatcher.call(new PlayerStopFlyingWithElytraEvent(this));
}
}
/**
* Used to change internally the last sent last keep alive id.
*
* 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 item usage if {@link #isUsingItem()} 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) {
byte oldHeldSlot = this.heldSlot;
this.heldSlot = slot;
syncEquipment(EquipmentSlot.MAIN_HAND);
updateEquipmentAttributes(inventory.getItemStack(oldHeldSlot), inventory.getItemStack(this.heldSlot), EquipmentSlot.MAIN_HAND);
clearItemUse();
}
public void refreshItemUse(@Nullable Hand itemUseHand, long itemUseTimeTicks) {
this.itemUseHand = itemUseHand;
if (itemUseHand != null) {
this.startItemUseTime = getAliveTicks();
this.itemUseTime = itemUseTimeTicks;
} else {
this.startItemUseTime = 0;
}
}
public void clearItemUse() {
refreshItemUse(null, 0);
}
/**
* Used to call {@link ItemUpdateStateEvent} with the proper item
* It does check which hand to get the item to update.
*
* @return the called {@link ItemUpdateStateEvent},
*/
public @NotNull ItemUpdateStateEvent callItemUpdateStateEvent(@NotNull Hand hand) {
ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(this, hand, getItemInHand(hand));
EventDispatcher.call(itemUpdateStateEvent);
return itemUpdateStateEvent;
}
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 asHoverEvent(@NotNull UnaryOperator op) {
return HoverEvent.showEntity(ShowEntity.showEntity(EntityType.PLAYER, getUuid(), this.displayName));
}
/**
* Gets the packet to add the player from the tab-list.
*
* @return a {@link PlayerInfoUpdatePacket} to add the player
*/
protected @NotNull PlayerInfoUpdatePacket getAddPlayerToList() {
return new PlayerInfoUpdatePacket(EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED),
List.of(infoEntry()));
}
/**
* Gets the packet to remove the player from the tab-list.
*
* @return a {@link PlayerInfoRemovePacket} to remove the player
*/
protected @NotNull PlayerInfoRemovePacket getRemovePlayerToList() {
return new PlayerInfoRemovePacket(getUuid());
}
private PlayerInfoUpdatePacket.Entry infoEntry() {
final PlayerSkin skin = this.skin;
List prop = skin != null ?
List.of(new PlayerInfoUpdatePacket.Property("textures", skin.textures(), skin.signature())) :
List.of();
return new PlayerInfoUpdatePacket.Entry(getUuid(), getUsername(), prop,
true, getLatency(), getGameMode(), displayName, null);
}
/**
* Sends all the related packet to have the player sent to another with related data
* (create player, spawn position, velocity, metadata, equipments, passengers, team).
*
* 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 getEquipment(@NotNull EquipmentSlot slot) {
return inventory.getEquipment(slot);
}
@Override
public void setEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemStack) {
inventory.setEquipment(slot, 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 boolean isPlayer() {
return true;
}
@Override
public Player asPlayer() {
return this;
}
@Override
protected void updateCollisions() {
preventBlockPlacement = gameMode != GameMode.SPECTATOR;
collidesWithEntities = gameMode != GameMode.SPECTATOR;
}
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 teleport(@NotNull Pos position, long @Nullable [] chunks, int flags) {
chunkUpdateLimitChecker.clearHistory();
return super.teleport(position, chunks, flags);
}
/**
* Represents the main or off hand of the player.
*/
public enum Hand {
MAIN,
OFF
}
public enum FacePoint {
FEET,
EYE
}
// Settings enum
/**
* Represents where is located the main hand of the player (can be changed in Minecraft option).
*/
public enum MainHand {
LEFT,
RIGHT
}
public class PlayerSettings {
private String locale;
private byte viewDistance;
private ChatMessageType chatMessageType;
private boolean chatColors;
private byte displayedSkinParts;
private MainHand mainHand;
private boolean enableTextFiltering;
private boolean allowServerListings;
public PlayerSettings() {
viewDistance = (byte) ServerFlag.CHUNK_VIEW_DISTANCE;
}
/**
* The player game language.
*
* @return the player locale
*/
public String getLocale() {
return locale;
}
/**
* Gets the player view distance.
*
* @return the player view distance
*/
public byte getViewDistance() {
return viewDistance;
}
public int getEffectiveViewDistance() {
return Math.min(getViewDistance(), ServerFlag.CHUNK_VIEW_DISTANCE);
}
/**
* Gets the messages this player wants to receive.
*
* @return the messages
*/
public @Nullable ChatMessageType getChatMessageType() {
return chatMessageType;
}
/**
* Gets if the player has chat colors enabled.
*
* @return true if chat colors are enabled, false otherwise
*/
public boolean hasChatColors() {
return chatColors;
}
public byte getDisplayedSkinParts() {
return displayedSkinParts;
}
/**
* Gets the player main hand.
*
* @return the player main hand
*/
public MainHand getMainHand() {
return mainHand;
}
public boolean enableTextFiltering() {
return enableTextFiltering;
}
public boolean allowServerListings() {
return allowServerListings;
}
/**
* Changes the player settings internally.
*
* 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
byte previousViewDistance = this.viewDistance;
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;
// Check to see if we're in an instance first, as this method is called when first logging in since the client sends the Settings packet during configuration
if (instance != null) {
// Load/unload chunks if necessary due to view distance changes
if (previousViewDistance < this.viewDistance) {
// View distance expanded, send chunks
ChunkUtils.forChunksInRange(position.chunkX(), position.chunkZ(), this.viewDistance, (chunkX, chunkZ) -> {
if (Math.abs(chunkX - position.chunkX()) > previousViewDistance || Math.abs(chunkZ - position.chunkZ()) > previousViewDistance) {
chunkAdder.accept(chunkX, chunkZ);
}
});
} else if (previousViewDistance > this.viewDistance) {
// View distance shrunk, unload chunks
ChunkUtils.forChunksInRange(position.chunkX(), position.chunkZ(), previousViewDistance, (chunkX, chunkZ) -> {
if (Math.abs(chunkX - position.chunkX()) > this.viewDistance || Math.abs(chunkZ - position.chunkZ()) > this.viewDistance) {
chunkRemover.accept(chunkX, chunkZ);
}
});
}
// Else previous and current are equal, do nothing
}
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);
}
@SuppressWarnings("unchecked")
@ApiStatus.Experimental
@Override
public @NotNull Acquirable extends Player> acquirable() {
return (Acquirable extends Player>) super.acquirable();
}
}