diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 218c11897..e81638a9f 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -1,18 +1,6 @@ package net.minestom.server.entity; import com.google.common.collect.Queues; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import net.minestom.server.MinecraftServer; import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.attribute.Attribute; @@ -34,16 +22,7 @@ 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.PlayerChunkLoadEvent; -import net.minestom.server.event.player.PlayerChunkUnloadEvent; -import net.minestom.server.event.player.PlayerDisconnectEvent; -import net.minestom.server.event.player.PlayerEatEvent; -import net.minestom.server.event.player.PlayerPreLoginEvent; -import net.minestom.server.event.player.PlayerRespawnEvent; -import net.minestom.server.event.player.PlayerSkinInitEvent; -import net.minestom.server.event.player.PlayerSpawnEvent; -import net.minestom.server.event.player.PlayerTickEvent; -import net.minestom.server.event.player.UpdateTagListEvent; +import net.minestom.server.event.player.*; import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; @@ -58,49 +37,7 @@ import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.network.packet.client.play.ClientChatMessagePacket; import net.minestom.server.network.packet.server.ServerPacket; -import net.minestom.server.network.packet.server.play.BlockBreakAnimationPacket; -import net.minestom.server.network.packet.server.play.CameraPacket; -import net.minestom.server.network.packet.server.play.ChangeGameStatePacket; -import net.minestom.server.network.packet.server.play.ChatMessagePacket; -import net.minestom.server.network.packet.server.play.CloseWindowPacket; -import net.minestom.server.network.packet.server.play.CombatEventPacket; -import net.minestom.server.network.packet.server.play.DeclareCommandsPacket; -import net.minestom.server.network.packet.server.play.DeclareRecipesPacket; -import net.minestom.server.network.packet.server.play.DestroyEntitiesPacket; -import net.minestom.server.network.packet.server.play.DisconnectPacket; -import net.minestom.server.network.packet.server.play.EffectPacket; -import net.minestom.server.network.packet.server.play.EntityEquipmentPacket; -import net.minestom.server.network.packet.server.play.EntityHeadLookPacket; -import net.minestom.server.network.packet.server.play.EntityMetaDataPacket; -import net.minestom.server.network.packet.server.play.EntityPositionAndRotationPacket; -import net.minestom.server.network.packet.server.play.EntityPositionPacket; -import net.minestom.server.network.packet.server.play.EntityRotationPacket; -import net.minestom.server.network.packet.server.play.EntitySoundEffectPacket; -import net.minestom.server.network.packet.server.play.FacePlayerPacket; -import net.minestom.server.network.packet.server.play.HeldItemChangePacket; -import net.minestom.server.network.packet.server.play.JoinGamePacket; -import net.minestom.server.network.packet.server.play.KeepAlivePacket; -import net.minestom.server.network.packet.server.play.NamedSoundEffectPacket; -import net.minestom.server.network.packet.server.play.OpenWindowPacket; -import net.minestom.server.network.packet.server.play.PlayerAbilitiesPacket; -import net.minestom.server.network.packet.server.play.PlayerInfoPacket; -import net.minestom.server.network.packet.server.play.PlayerListHeaderAndFooterPacket; -import net.minestom.server.network.packet.server.play.PlayerPositionAndLookPacket; -import net.minestom.server.network.packet.server.play.PluginMessagePacket; -import net.minestom.server.network.packet.server.play.ResourcePackSendPacket; -import net.minestom.server.network.packet.server.play.RespawnPacket; -import net.minestom.server.network.packet.server.play.ServerDifficultyPacket; -import net.minestom.server.network.packet.server.play.SetExperiencePacket; -import net.minestom.server.network.packet.server.play.SoundEffectPacket; -import net.minestom.server.network.packet.server.play.SpawnPlayerPacket; -import net.minestom.server.network.packet.server.play.SpawnPositionPacket; -import net.minestom.server.network.packet.server.play.StopSoundPacket; -import net.minestom.server.network.packet.server.play.TagsPacket; -import net.minestom.server.network.packet.server.play.TitlePacket; -import net.minestom.server.network.packet.server.play.UnloadChunkPacket; -import net.minestom.server.network.packet.server.play.UnlockRecipesPacket; -import net.minestom.server.network.packet.server.play.UpdateHealthPacket; -import net.minestom.server.network.packet.server.play.UpdateViewPositionPacket; +import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.player.NettyPlayerConnection; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.recipe.Recipe; @@ -114,7 +51,6 @@ import net.minestom.server.stat.PlayerStatistic; import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.MathUtils; -import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.Position; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.callback.OptionalCallback; @@ -129,2642 +65,2588 @@ import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + /** - * Those are the major actors of the server, they are not necessary backed by a {@link - * NettyPlayerConnection} as shown by {@link FakePlayer}. - * - *
You can easily create your own implementation of this and use it with {@link - * ConnectionManager#setPlayerProvider(PlayerProvider)}. + * Those are the major actors of the server, + * they are not necessary backed by a {@link NettyPlayerConnection} as shown by {@link FakePlayer}. + *
+ * You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}.
*/
public class Player extends LivingEntity implements CommandSender {
- /** @see #getPlayerSynchronizationGroup() */
- private static volatile int playerSynchronizationGroup = 75;
- protected final PlayerConnection playerConnection;
- // All the entities that this player can see
- protected final Set Used to prevent sending exponentially more packets and therefore reduce network load.
- *
- * @return the viewers count which would result in a 1 tick delay
- */
- public static int getPlayerSynchronizationGroup() {
- return playerSynchronizationGroup;
- }
-
- /**
- * Changes the viewers count resulting in an additional delay of 1 tick for the position
- * synchronization.
- *
- * @param playerSynchronizationGroup the new synchronization group size
- * @see #getPlayerSynchronizationGroup()
- */
- public static void setPlayerSynchronizationGroup(int playerSynchronizationGroup) {
- Player.playerSynchronizationGroup = playerSynchronizationGroup;
- }
-
- /**
- * Gets the number of tick between each position synchronization.
- *
- * @param viewersCount the player viewers count
- * @return the number of tick between each position synchronization.
- */
- public static int getPlayerSynchronizationTickDelay(int viewersCount) {
- return viewersCount / playerSynchronizationGroup + 1;
- }
-
- /**
- * Used when the player is created. Init the player and spawn him.
- *
- * WARNING: executed in the main update thread
- */
- protected void init() {
- JoinGamePacket joinGamePacket = new JoinGamePacket();
- joinGamePacket.entityId = getEntityId();
- joinGamePacket.gameMode = gameMode;
- joinGamePacket.dimensionType = dimensionType;
- joinGamePacket.maxPlayers = 0; // Unused
- joinGamePacket.viewDistance = MinecraftServer.getChunkViewDistance();
- joinGamePacket.reducedDebugInfo = false;
- joinGamePacket.isFlat = levelFlat;
- playerConnection.sendPacket(joinGamePacket);
-
- // Server brand name
- {
- playerConnection.sendPacket(PluginMessagePacket.getBrandPacket());
+ /**
+ * For the number of viewers that a player has, the position synchronization packet will be sent
+ * every 1 tick + (viewers/{@code playerSynchronizationGroup}).
+ * (eg with a value of 100, having 300 viewers means sending the synchronization packet every 3 ticks)
+ *
+ * Used to prevent sending exponentially more packets and therefore reduce network load.
+ *
+ * @return the viewers count which would result in a 1 tick delay
+ */
+ public static int getPlayerSynchronizationGroup() {
+ return playerSynchronizationGroup;
}
- ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket();
- serverDifficultyPacket.difficulty = MinecraftServer.getDifficulty();
- serverDifficultyPacket.locked = true;
- playerConnection.sendPacket(serverDifficultyPacket);
-
- SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket();
- spawnPositionPacket.x = (int) respawnPoint.getX();
- spawnPositionPacket.y = (int) respawnPoint.getY();
- spawnPositionPacket.z = (int) respawnPoint.getZ();
- playerConnection.sendPacket(spawnPositionPacket);
-
- // Add player to list with spawning skin
- PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this, skin);
- callEvent(PlayerSkinInitEvent.class, skinInitEvent);
- this.skin = skinInitEvent.getSkin();
- playerConnection.sendPacket(getAddPlayerToList());
-
- // Commands start
- {
- CommandManager commandManager = MinecraftServer.getCommandManager();
- DeclareCommandsPacket declareCommandsPacket =
- commandManager.createDeclareCommandsPacket(this);
-
- playerConnection.sendPacket(declareCommandsPacket);
- }
- // Commands end
-
- // Recipes start
- {
- RecipeManager recipeManager = MinecraftServer.getRecipeManager();
- DeclareRecipesPacket declareRecipesPacket = recipeManager.getDeclareRecipesPacket();
- if (declareRecipesPacket.recipes != null) {
- playerConnection.sendPacket(declareRecipesPacket);
- }
-
- List
+ * WARNING: executed in the main update thread
+ */
+ protected void init() {
+ JoinGamePacket joinGamePacket = new JoinGamePacket();
+ joinGamePacket.entityId = getEntityId();
+ joinGamePacket.gameMode = gameMode;
+ joinGamePacket.dimensionType = dimensionType;
+ joinGamePacket.maxPlayers = 0; // Unused
+ joinGamePacket.viewDistance = MinecraftServer.getChunkViewDistance();
+ joinGamePacket.reducedDebugInfo = false;
+ joinGamePacket.isFlat = levelFlat;
+ playerConnection.sendPacket(joinGamePacket);
- triggerStatus((byte) 9); // Mark item use as finished
- ItemUpdateStateEvent itemUpdateStateEvent = callItemUpdateStateEvent(true);
-
- Check.notNull(itemUpdateStateEvent, "#callItemUpdateStateEvent returned null.");
-
- // Refresh hand
- final boolean isOffHand = itemUpdateStateEvent.getHand() == Player.Hand.OFF;
- refreshActiveHand(false, isOffHand, false);
-
- final ItemStack foodItem = itemUpdateStateEvent.getItemStack();
- final boolean isFood = foodItem.getMaterial().isFood();
-
- if (isFood) {
- PlayerEatEvent playerEatEvent = new PlayerEatEvent(this, foodItem);
- callEvent(PlayerEatEvent.class, playerEatEvent);
- }
- }
- }
-
- // Tick event
- callEvent(PlayerTickEvent.class, playerTickEvent);
-
- // Multiplayer sync
- final boolean syncCooldown =
- CooldownUtils.hasCooldown(
- time,
- lastPlayerSynchronizationTime,
- TimeUnit.TICK,
- getPlayerSynchronizationTickDelay(viewers.size()));
- if (!viewers.isEmpty() && !syncCooldown) {
- this.lastPlayerSynchronizationTime = time;
-
- final boolean positionChanged =
- position.getX() != lastPlayerSyncX
- || position.getY() != lastPlayerSyncY
- || position.getZ() != lastPlayerSyncZ;
- final boolean viewChanged =
- position.getYaw() != lastPlayerSyncYaw || position.getPitch() != lastPlayerSyncPitch;
-
- if (positionChanged || viewChanged) {
- // Player moved since last time
-
- ServerPacket updatePacket;
- ServerPacket optionalUpdatePacket = null;
- if (positionChanged && viewChanged) {
- updatePacket =
- EntityPositionAndRotationPacket.getPacket(
- getEntityId(),
- position,
- new Position(lastPlayerSyncX, lastPlayerSyncY, lastPlayerSyncZ),
- onGround);
- } else if (positionChanged) {
- updatePacket =
- EntityPositionPacket.getPacket(
- getEntityId(),
- position,
- new Position(lastPlayerSyncX, lastPlayerSyncY, lastPlayerSyncZ),
- onGround);
- } else {
- // View changed
- EntityRotationPacket entityRotationPacket = new EntityRotationPacket();
- entityRotationPacket.entityId = getEntityId();
- entityRotationPacket.yaw = position.getYaw();
- entityRotationPacket.pitch = position.getPitch();
- entityRotationPacket.onGround = onGround;
-
- updatePacket = entityRotationPacket;
+ // Server brand name
+ {
+ playerConnection.sendPacket(PluginMessagePacket.getBrandPacket());
}
- if (viewChanged) {
- // Yaw from the rotation packet seems to be ignored, which is why this is required
- EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
- entityHeadLookPacket.entityId = getEntityId();
- entityHeadLookPacket.yaw = position.getYaw();
- optionalUpdatePacket = entityHeadLookPacket;
+ ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket();
+ serverDifficultyPacket.difficulty = MinecraftServer.getDifficulty();
+ serverDifficultyPacket.locked = true;
+ playerConnection.sendPacket(serverDifficultyPacket);
+
+ SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket();
+ spawnPositionPacket.x = (int) respawnPoint.getX();
+ spawnPositionPacket.y = (int) respawnPoint.getY();
+ spawnPositionPacket.z = (int) respawnPoint.getZ();
+ playerConnection.sendPacket(spawnPositionPacket);
+
+ // Add player to list with spawning skin
+ PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this, skin);
+ callEvent(PlayerSkinInitEvent.class, skinInitEvent);
+ this.skin = skinInitEvent.getSkin();
+ playerConnection.sendPacket(getAddPlayerToList());
+
+ // Commands start
+ {
+ CommandManager commandManager = MinecraftServer.getCommandManager();
+ DeclareCommandsPacket declareCommandsPacket = commandManager.createDeclareCommandsPacket(this);
+
+ playerConnection.sendPacket(declareCommandsPacket);
}
+ // Commands end
- // Send the update packet
- if (optionalUpdatePacket != null) {
- sendPacketsToViewers(updatePacket, optionalUpdatePacket);
- } else {
- sendPacketToViewers(updatePacket);
- }
- }
- // Update sync data
- if (positionChanged) {
- lastPlayerSyncX = position.getX();
- lastPlayerSyncY = position.getY();
- lastPlayerSyncZ = position.getZ();
- }
- if (viewChanged) {
- lastPlayerSyncYaw = position.getYaw();
- lastPlayerSyncPitch = position.getPitch();
- }
- }
- }
-
- @Override
- public void kill() {
- if (!isDead()) {
-
- // send death screen text to the killed player
- {
- ColoredText deathText;
- if (lastDamageSource != null) {
- deathText = lastDamageSource.buildDeathScreenText(this);
- } else { // may happen if killed by the server without applying damage
- deathText = ColoredText.of("Killed by poor programming.");
- }
-
- // #buildDeathScreenText can return null, check here
- if (deathText != null) {
- CombatEventPacket deathPacket =
- CombatEventPacket.death(this, Optional.empty(), deathText);
- playerConnection.sendPacket(deathPacket);
- }
- }
-
- // send death message to chat
- {
- JsonMessage chatMessage;
- if (lastDamageSource != null) {
- chatMessage = lastDamageSource.buildDeathMessage(this);
- } else { // may happen if killed by the server without applying damage
- chatMessage = ColoredText.of(getUsername() + " was killed by poor programming.");
- }
-
- // #buildDeathMessage can return null, check here
- if (chatMessage != null) {
- MinecraftServer.getConnectionManager().broadcastMessage(chatMessage);
- }
- }
- }
- super.kill();
- }
-
- /**
- * Respawns the player by sending a {@link RespawnPacket} to the player and teleporting him to
- * {@link #getRespawnPoint()}. It also resets fire and his health
- */
- public void respawn() {
- if (!isDead()) return;
-
- setFireForDuration(0);
- setOnFire(false);
- refreshHealth();
- RespawnPacket respawnPacket = new RespawnPacket();
- respawnPacket.dimensionType = getDimensionType();
- respawnPacket.gameMode = getGameMode();
- respawnPacket.isFlat = levelFlat;
- getPlayerConnection().sendPacket(respawnPacket);
- PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this);
- callEvent(PlayerRespawnEvent.class, respawnEvent);
- refreshIsDead(false);
-
- // Runnable called when teleportation is successful (after loading and sending necessary chunk)
- teleport(respawnEvent.getRespawnPosition(), this::refreshAfterTeleport);
- }
-
- @Override
- public void spawn() {}
-
- @Override
- public boolean isOnGround() {
- return onGround;
- }
-
- @Override
- public void remove() {
- callEvent(PlayerDisconnectEvent.class, new PlayerDisconnectEvent(this));
-
- super.remove();
- this.packets.clear();
- if (getOpenInventory() != null) getOpenInventory().removeViewer(this);
-
- // Boss bars cache
- {
- Set 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, can be null or {@link #getPosition()} if
- * you do not want to teleport the player
- */
- public void setInstance(@NotNull Instance instance, @Nullable Position spawnPosition) {
- Check.notNull(instance, "instance cannot be null!");
- Check.argCondition(
- this.instance == instance, "Instance should be different than the current one");
-
- // true if the chunks need to be sent to the client, can be false if the instances share the
- // same chunks (eg SharedInstance)
- final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance);
-
- if (needWorldRefresh) {
- final boolean firstSpawn =
- this.instance
- == null; // TODO: Handle player reconnections, must be false in that case too
-
- // Remove all previous viewable chunks (from the previous instance)
- for (Chunk viewableChunk : viewableChunks) {
- viewableChunk.removeViewer(this);
- }
-
- // Send the new dimension
- if (this.instance != null) {
- final DimensionType instanceDimensionType = instance.getDimensionType();
- if (dimensionType != instanceDimensionType) sendDimension(instanceDimensionType);
- }
-
- // Load all the required chunks
- final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, getChunkRange());
-
- final ChunkCallback eachCallback =
- chunk -> {
- if (chunk != null) {
- final int chunkX = ChunkUtils.getChunkCoordinate((int) spawnPosition.getX());
- final int chunkZ = ChunkUtils.getChunkCoordinate((int) spawnPosition.getZ());
- if (chunk.getChunkX() == chunkX && chunk.getChunkZ() == chunkZ) {
- updateViewPosition(chunkX, chunkZ);
- }
+ // Recipes start
+ {
+ RecipeManager recipeManager = MinecraftServer.getRecipeManager();
+ DeclareRecipesPacket declareRecipesPacket = recipeManager.getDeclareRecipesPacket();
+ if (declareRecipesPacket.recipes != null) {
+ playerConnection.sendPacket(declareRecipesPacket);
}
- };
- final ChunkCallback endCallback =
- chunk -> {
- // This is the last chunk to be loaded , spawn player
- spawnPlayer(instance, spawnPosition, firstSpawn);
- };
+ List Does add the player to {@code instance}, remove all viewable entities and call {@link
- * PlayerSpawnEvent}.
- *
- * UNSAFE: only called with {@link #setInstance(Instance, Position)}.
- *
- * @param spawnPosition the position to teleport the player
- * @param firstSpawn true if this is the player first spawn
- */
- private void spawnPlayer(
- @NotNull Instance instance, @Nullable Position spawnPosition, boolean firstSpawn) {
- this.viewableEntities.forEach(entity -> entity.removeViewer(this));
-
- super.setInstance(instance);
-
- if (spawnPosition != null && !position.isSimilar(spawnPosition)) {
- teleport(
- spawnPosition,
- position.inSameChunk(spawnPosition) ? () -> refreshVisibleChunks(getChunk()) : null);
- } else {
- refreshVisibleChunks(getChunk());
- }
-
- PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
- callEvent(PlayerSpawnEvent.class, spawnEvent);
- }
-
- @NotNull
- @Override
- public Consumer Sets to null to show the player username.
- *
- * @param displayName the display name, null to display the username
- */
- public void setDisplayName(@Nullable ColoredText displayName) {
- this.displayName = displayName;
-
- PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_DISPLAY_NAME);
- infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateDisplayName(getUuid(), displayName));
- sendPacketToViewersAndSelf(infoPacket);
- }
-
- /**
- * Gets the player skin.
- *
- * @return the player skin object, null means that the player has his {@link #getUuid()} default
- * skin
- */
- @Nullable
- public 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();
- destroyEntitiesPacket.entityIds = new int[] {getEntityId()};
-
- final PlayerInfoPacket removePlayerPacket = getRemovePlayerToList();
- final PlayerInfoPacket addPlayerPacket = getAddPlayerToList();
-
- RespawnPacket respawnPacket = new RespawnPacket();
- respawnPacket.dimensionType = getDimensionType();
- respawnPacket.gameMode = getGameMode();
- respawnPacket.isFlat = levelFlat;
-
- playerConnection.sendPacket(removePlayerPacket);
- playerConnection.sendPacket(destroyEntitiesPacket);
- playerConnection.sendPacket(respawnPacket);
- playerConnection.sendPacket(addPlayerPacket);
-
- {
- // Remove player
- sendPacketToViewers(removePlayerPacket);
- sendPacketToViewers(destroyEntitiesPacket);
-
- // Show player again
- getViewers().forEach(player -> showPlayer(player.getPlayerConnection()));
- }
-
- getInventory().update();
- teleport(getPosition());
- }
-
- /**
- * 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;
- sendChangeGameStatePacket(
- ChangeGameStatePacket.Reason.ENABLE_RESPAWN_SCREEN, enableRespawnScreen ? 0 : 1);
- }
-
- /**
- * Gets the player username.
- *
- * @return the player username
- */
- @NotNull
- public String getUsername() {
- return username;
- }
-
- /**
- * Changes the internal player name, used for the {@link PlayerPreLoginEvent} mostly unsafe
- * outside of it.
- *
- * @param username the new player name
- */
- protected void setUsername(@NotNull String username) {
- this.username = username;
- }
-
- private void sendChangeGameStatePacket(
- @NotNull ChangeGameStatePacket.Reason reason, float value) {
- ChangeGameStatePacket changeGameStatePacket = new ChangeGameStatePacket();
- changeGameStatePacket.reason = reason;
- changeGameStatePacket.value = value;
- playerConnection.sendPacket(changeGameStatePacket);
- }
-
- /**
- * 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);
- callEvent(ItemDropEvent.class, itemDropEvent);
- return !itemDropEvent.isCancelled();
- }
-
- /**
- * Sets the player resource pack.
- *
- * @param resourcePack the resource pack
- */
- public void setResourcePack(@NotNull ResourcePack resourcePack) {
- Check.notNull(resourcePack, "The resource pack cannot be null");
- final String url = resourcePack.getUrl();
- final String hash = resourcePack.getHash();
-
- ResourcePackSendPacket resourcePackSendPacket = new ResourcePackSendPacket();
- resourcePackSendPacket.url = url;
- resourcePackSendPacket.hash = hash;
- playerConnection.sendPacket(resourcePackSendPacket);
- }
-
- /**
- * 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 Position 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 Position targetPosition,
- @Nullable Entity entity,
- @Nullable FacePoint targetPoint) {
- FacePlayerPacket facePlayerPacket = new FacePlayerPacket();
- facePlayerPacket.entityFacePosition =
- facePoint == FacePoint.EYE
- ? FacePlayerPacket.FacePosition.EYES
- : FacePlayerPacket.FacePosition.FEET;
- facePlayerPacket.targetX = targetPosition.getX();
- facePlayerPacket.targetY = targetPosition.getY();
- facePlayerPacket.targetZ = targetPosition.getZ();
- if (entity != null) {
- facePlayerPacket.entityId = entity.getEntityId();
- facePlayerPacket.entityFacePosition =
- targetPoint == FacePoint.EYE
- ? FacePlayerPacket.FacePosition.EYES
- : FacePlayerPacket.FacePosition.FEET;
- }
- playerConnection.sendPacket(facePlayerPacket);
- }
-
- /**
- * Sets the camera at {@code entity} eyes.
- *
- * @param entity the entity to spectate
- */
- public void spectate(@NotNull Entity entity) {
- CameraPacket cameraPacket = new CameraPacket();
- cameraPacket.cameraId = entity.getEntityId();
- playerConnection.sendPacket(cameraPacket);
- }
-
- /** 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(Position)}.
- *
- * @return a copy of the default respawn point
- */
- @NotNull
- public Position getRespawnPoint() {
- return respawnPoint.clone();
- }
-
- /**
- * Changes the default spawn point.
- *
- * @param respawnPoint the player respawn point
- */
- public void setRespawnPoint(@NotNull Position respawnPoint) {
- this.respawnPoint = respawnPoint;
- }
-
- /**
- * Called after the player teleportation to refresh his position and send data to his new viewers.
- */
- protected void refreshAfterTeleport() {
- getInventory().update();
-
- SpawnPlayerPacket spawnPlayerPacket = new SpawnPlayerPacket();
- spawnPlayerPacket.entityId = getEntityId();
- spawnPlayerPacket.playerUuid = getUuid();
- spawnPlayerPacket.position = getPosition();
- sendPacketToViewers(spawnPlayerPacket);
-
- // Update for viewers
- sendPacketToViewersAndSelf(getVelocityPacket());
- sendPacketToViewersAndSelf(getMetadataPacket());
- playerConnection.sendPacket(getPropertiesPacket());
- syncEquipments();
-
- {
- // Send new chunks
- final BlockPosition pos = position.toBlockPosition();
- final Chunk chunk = instance.getChunk(pos.getX() >> 4, pos.getZ() >> 4);
- Check.notNull(chunk, "Tried to interact with an unloaded chunk.");
- refreshVisibleChunks(chunk);
- }
- }
-
- /** 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();
- }
-
- /**
- * Sends an {@link UpdateHealthPacket} to refresh client-side information about health and food.
- */
- protected void sendUpdateHealthPacket() {
- UpdateHealthPacket updateHealthPacket = new UpdateHealthPacket();
- updateHealthPacket.health = getHealth();
- updateHealthPacket.food = food;
- updateHealthPacket.foodSaturation = foodSaturation;
- playerConnection.sendPacket(updateHealthPacket);
- }
-
- /**
- * 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;
- sendExperienceUpdatePacket();
- }
-
- /**
- * 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;
- sendExperienceUpdatePacket();
- }
-
- /**
- * Sends a {@link SetExperiencePacket} to refresh client-side information about the experience
- * bar.
- */
- protected void sendExperienceUpdatePacket() {
- SetExperiencePacket setExperiencePacket = new SetExperiencePacket();
- setExperiencePacket.percentage = exp;
- setExperiencePacket.level = level;
- playerConnection.sendPacket(setExperiencePacket);
- }
-
- /**
- * Called when the player changes chunk (move from one to another). Can also be used to refresh
- * the list of chunks that the client should see based on {@link #getChunkRange()}.
- *
- * It does remove and add the player from the chunks viewers list when removed or added. It
- * also calls the events {@link PlayerChunkUnloadEvent} and {@link PlayerChunkLoadEvent}.
- *
- * @param newChunk the current/new player chunk (can be the current one)
- */
- public void refreshVisibleChunks(@NotNull Chunk newChunk) {
- // Previous chunks indexes
- final long[] lastVisibleChunks =
- viewableChunks.stream()
- .mapToLong(
- viewableChunks ->
- ChunkUtils.getChunkIndex(
- viewableChunks.getChunkX(), viewableChunks.getChunkZ()))
- .toArray();
-
- // New chunks indexes
- final long[] updatedVisibleChunks =
- ChunkUtils.getChunksInRange(newChunk.toPosition(), getChunkRange());
-
- // Find the difference between the two arrays
- final int[] oldChunks =
- ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks);
- final int[] newChunks =
- ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks);
-
- // Unload old chunks
- for (int index : oldChunks) {
- final long chunkIndex = lastVisibleChunks[index];
- final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
- final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
-
- UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
- unloadChunkPacket.chunkX = chunkX;
- unloadChunkPacket.chunkZ = chunkZ;
- playerConnection.sendPacket(unloadChunkPacket);
-
- final Chunk chunk = instance.getChunk(chunkX, chunkZ);
- if (chunk != null) chunk.removeViewer(this);
- }
-
- // Update client render distance
- updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
-
- // Load new chunks
- for (int index : newChunks) {
- final long chunkIndex = updatedVisibleChunks[index];
- final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
- final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
-
- instance.loadOptionalChunk(
- chunkX,
- chunkZ,
- chunk -> {
- if (chunk == null) {
- // Cannot load chunk (auto load is not enabled)
- return;
+ recipesIdentifier.add(recipe.getRecipeId());
}
- chunk.addViewer(this);
- });
+ if (!recipesIdentifier.isEmpty()) {
+ final String[] identifiers = recipesIdentifier.toArray(new String[0]);
+ UnlockRecipesPacket unlockRecipesPacket = new UnlockRecipesPacket();
+ unlockRecipesPacket.mode = 0;
+ unlockRecipesPacket.recipesId = identifiers;
+ unlockRecipesPacket.initRecipesId = identifiers;
+ playerConnection.sendPacket(unlockRecipesPacket);
+ }
+ }
+ // Recipes end
+
+ // Send server tags
+ TagsPacket tags = new TagsPacket();
+ TagManager tagManager = MinecraftServer.getTagManager();
+ tagManager.addRequiredTagsToPacket(tags);
+
+ UpdateTagListEvent event = new UpdateTagListEvent(tags);
+ callEvent(UpdateTagListEvent.class, event);
+
+ getPlayerConnection().sendPacket(tags);
+
+ // Some client update
+ playerConnection.sendPacket(getPropertiesPacket()); // Send default properties
+ refreshHealth(); // Heal and send health packet
+ refreshAbilities(); // Send abilities packet
+ getInventory().update();
}
- }
- /**
- * Refreshes the list of entities that the player should be able to see based on {@link
- * MinecraftServer#getEntityViewDistance()} and {@link Entity#isAutoViewable()}.
- *
- * @param newChunk the new chunk of the player (can be the current one)
- */
- public void refreshVisibleEntities(@NotNull Chunk newChunk) {
- final int entityViewDistance = MinecraftServer.getEntityViewDistance();
- final float maximalDistance = entityViewDistance * Chunk.CHUNK_SECTION_SIZE;
+ /**
+ * Used to initialize the player connection
+ */
+ protected void playerConnectionInit() {
+ this.playerConnection.setPlayer(this);
+ }
- // Manage already viewable entities
- this.viewableEntities.forEach(
- entity -> {
- final float distance = entity.getDistance(this);
- if (distance > maximalDistance) {
- // Entity shouldn't be viewable anymore
- if (isAutoViewable()) {
- entity.removeViewer(this);
+ @Override
+ public float getAttributeValue(@NotNull Attribute attribute) {
+ if (attribute == Attributes.MOVEMENT_SPEED) {
+ return walkingSpeed;
+ }
+ return super.getAttributeValue(attribute);
+ }
+
+ @Override
+ public void update(long time) {
+ // Network tick
+ this.playerConnection.update();
+
+ // Process received packets
+ ClientPlayPacket packet;
+ while ((packet = packets.poll()) != null) {
+ packet.process(this);
+ }
+
+ super.update(time); // Super update (item pickup/fire management)
+
+ // Target block stage
+ if (targetCustomBlock != null) {
+ this.targetBlockBreakCount++;
+
+ final boolean processStage = targetBreakDelay < 0 || targetBlockBreakCount >= targetBreakDelay;
+
+ // Check if the player did finish his current break delay
+ if (processStage) {
+
+ // Negative value should skip abs(value) stage
+ final byte stageIncrease = (byte) (targetBreakDelay > 0 ? 1 : Math.abs(targetBreakDelay));
+
+ // Should increment the target block stage
+ if (targetCustomBlock.enableMultiPlayerBreaking()) {
+ // Let the custom block object manages the breaking
+ final boolean canContinue = this.targetCustomBlock.processStage(instance, targetBlockPosition, this, stageIncrease);
+ if (canContinue) {
+ final Set Used to send packets and get stuff related to the connection.
- *
- * @return the player connection
- */
- @NotNull
- public PlayerConnection getPlayerConnection() {
- return playerConnection;
- }
-
- /**
- * 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
- */
- @NotNull
- public PlayerSettings getSettings() {
- return settings;
- }
-
- /**
- * Gets the player dimension.
- *
- * @return the player current dimension
- */
- public DimensionType getDimensionType() {
- return dimensionType;
- }
-
- @NotNull
- public 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
- */
- public void setGameMode(@NotNull GameMode gameMode) {
- Check.notNull(gameMode, "GameMode cannot be null");
- this.gameMode = gameMode;
- sendChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.getId());
-
- PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_GAMEMODE);
- infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateGamemode(getUuid(), gameMode));
- sendPacketToViewersAndSelf(infoPacket);
- }
-
- /**
- * Gets if this player is in creative. Used for code readability.
- *
- * @return true if the player is in creative mode
- */
- public boolean isCreative() {
- return gameMode == GameMode.CREATIVE;
- }
-
- /**
- * Changes the dimension of the player. Mostly unsafe since it requires sending chunks after.
- *
- * @param dimensionType the new player dimension
- */
- protected void sendDimension(@NotNull DimensionType dimensionType) {
- Check.notNull(dimensionType, "Dimension cannot be null!");
- Check.argCondition(
- dimensionType.equals(getDimensionType()),
- "The dimension needs to be different than the current one!");
-
- this.dimensionType = dimensionType;
- RespawnPacket respawnPacket = new RespawnPacket();
- respawnPacket.dimensionType = dimensionType;
- respawnPacket.gameMode = gameMode;
- respawnPacket.isFlat = levelFlat;
- playerConnection.sendPacket(respawnPacket);
- }
-
- /**
- * Kicks the player with a reason.
- *
- * @param text the kick reason
- */
- public void kick(@NotNull ColoredText text) {
- DisconnectPacket disconnectPacket = new DisconnectPacket();
- disconnectPacket.message = text;
- playerConnection.sendPacket(disconnectPacket);
- playerConnection.disconnect();
- playerConnection.refreshOnline(false);
- }
-
- /**
- * Kicks the player with a reason.
- *
- * @param message the kick reason
- */
- public void kick(@NotNull String message) {
- kick(ColoredText.of(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");
-
- HeldItemChangePacket heldItemChangePacket = new HeldItemChangePacket();
- heldItemChangePacket.slot = slot;
- playerConnection.sendPacket(heldItemChangePacket);
- refreshHeldSlot(slot);
- }
-
- /**
- * Gets the player held slot (0-8).
- *
- * @return the current held slot for the player
- */
- public byte getHeldSlot() {
- return heldSlot;
- }
-
- /** {@inheritDoc} */
- @Override
- public void setTeam(Team team) {
- super.setTeam(team);
- if (team != null)
- PacketUtils.sendGroupedPacket(
- MinecraftServer.getConnectionManager().getOnlinePlayers(),
- team.createTeamsCreationPacket());
- }
-
- /**
- * 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;
- }
+ @Override
+ public void kill() {
+ if (!isDead()) {
- /**
- * Gets if the player is sneaking.
- *
- * WARNING: this can be bypassed by hacked client, this is only what the client told the
- * server.
- *
- * @return true if the player is sneaking
- */
- public boolean isSneaking() {
- return crouched;
- }
+ // send death screen text to the killed player
+ {
+ ColoredText deathText;
+ if (lastDamageSource != null) {
+ deathText = lastDamageSource.buildDeathScreenText(this);
+ } else { // may happen if killed by the server without applying damage
+ deathText = ColoredText.of("Killed by poor programming.");
+ }
- /**
- * Gets if the player is sprinting.
- *
- * WARNING: this can be bypassed by hacked client, this is only what the client told the
- * server.
- *
- * @return true if the player is sprinting
- */
- public boolean isSprinting() {
- return sprinting;
- }
+ // #buildDeathScreenText can return null, check here
+ if (deathText != null) {
+ CombatEventPacket deathPacket = CombatEventPacket.death(this, Optional.empty(), deathText);
+ playerConnection.sendPacket(deathPacket);
+ }
+ }
- /**
- * Used to get the {@link CustomBlock} that the player is currently mining.
- *
- * @return the currently mined {@link CustomBlock} by the player, null if there is not
- */
- @Nullable
- public CustomBlock getCustomBlockTarget() {
- return targetCustomBlock;
- }
+ // send death message to chat
+ {
+ JsonMessage chatMessage;
+ if (lastDamageSource != null) {
+ chatMessage = lastDamageSource.buildDeathMessage(this);
+ } else { // may happen if killed by the server without applying damage
+ chatMessage = ColoredText.of(getUsername() + " was killed by poor programming.");
+ }
- /**
- * Gets the player open inventory.
- *
- * @return the currently open inventory, null if there is not (player inventory is not detected)
- */
- @Nullable
- public Inventory getOpenInventory() {
- return openInventory;
- }
+ // #buildDeathMessage can return null, check here
+ if (chatMessage != null) {
+ MinecraftServer.getConnectionManager().broadcastMessage(chatMessage);
+ }
+ }
+ }
+ super.kill();
+ }
- /**
- * 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) {
- Check.notNull(
- inventory, "Inventory cannot be null, use Player#closeInventory() to close current");
-
- InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(this, inventory);
-
- callCancellableEvent(
- InventoryOpenEvent.class,
- inventoryOpenEvent,
- () -> {
- if (getOpenInventory() != null) {
- closeInventory();
- }
-
- Inventory newInventory = inventoryOpenEvent.getInventory();
-
- if (newInventory == null) {
- // just close the inventory
+ /**
+ * Respawns the player by sending a {@link RespawnPacket} to the player and teleporting him
+ * to {@link #getRespawnPoint()}. It also resets fire and his health
+ */
+ public void respawn() {
+ if (!isDead())
return;
- }
- OpenWindowPacket openWindowPacket = new OpenWindowPacket();
- openWindowPacket.windowId = newInventory.getWindowId();
- openWindowPacket.windowType = newInventory.getInventoryType().getWindowType();
- openWindowPacket.title = newInventory.getTitle();
- playerConnection.sendPacket(openWindowPacket);
- newInventory.addViewer(this);
- this.openInventory = newInventory;
+ setFireForDuration(0);
+ setOnFire(false);
+ refreshHealth();
+ RespawnPacket respawnPacket = new RespawnPacket();
+ respawnPacket.dimensionType = getDimensionType();
+ respawnPacket.gameMode = getGameMode();
+ respawnPacket.isFlat = levelFlat;
+ getPlayerConnection().sendPacket(respawnPacket);
+ PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this);
+ callEvent(PlayerRespawnEvent.class, respawnEvent);
+ refreshIsDead(false);
+
+ // Runnable called when teleportation is successful (after loading and sending necessary chunk)
+ teleport(respawnEvent.getRespawnPosition(), this::refreshAfterTeleport);
+ }
+
+ @Override
+ public void spawn() {
+
+ }
+
+ @Override
+ public boolean isOnGround() {
+ return onGround;
+ }
+
+ @Override
+ public void remove() {
+ callEvent(PlayerDisconnectEvent.class, new PlayerDisconnectEvent(this));
+
+ super.remove();
+ this.packets.clear();
+ if (getOpenInventory() != null)
+ getOpenInventory().removeViewer(this);
+
+ // Boss bars cache
+ {
+ Set
+ * 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,
+ * can be null or {@link #getPosition()} if you do not want to teleport the player
+ */
+ public void setInstance(@NotNull Instance instance, @Nullable Position spawnPosition) {
+ Check.notNull(instance, "instance cannot be null!");
+ Check.argCondition(this.instance == instance, "Instance should be different than the current one");
+
+ // true if the chunks need to be sent to the client, can be false if the instances share the same chunks (eg SharedInstance)
+ final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance);
+
+ if (needWorldRefresh) {
+ final boolean firstSpawn = this.instance == null; // TODO: Handle player reconnections, must be false in that case too
+
+ // Remove all previous viewable chunks (from the previous instance)
+ for (Chunk viewableChunk : viewableChunks) {
+ viewableChunk.removeViewer(this);
+ }
+
+ // Send the new dimension
+ if (this.instance != null) {
+ final DimensionType instanceDimensionType = instance.getDimensionType();
+ if (dimensionType != instanceDimensionType)
+ sendDimension(instanceDimensionType);
+ }
+
+ // Load all the required chunks
+ final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, getChunkRange());
+
+ final ChunkCallback eachCallback = chunk -> {
+ if (chunk != null) {
+ final int chunkX = ChunkUtils.getChunkCoordinate((int) spawnPosition.getX());
+ final int chunkZ = ChunkUtils.getChunkCoordinate((int) spawnPosition.getZ());
+ if (chunk.getChunkX() == chunkX &&
+ chunk.getChunkZ() == chunkZ) {
+ updateViewPosition(chunkX, chunkZ);
+ }
+ }
+ };
+
+ final ChunkCallback endCallback = chunk -> {
+ // This is the last chunk to be loaded , spawn player
+ spawnPlayer(instance, spawnPosition, firstSpawn);
+ };
+
+ // Chunk 0;0 always needs to be loaded
+ instance.loadChunk(0, 0, chunk ->
+ // Load all the required chunks
+ ChunkUtils.optionalLoadAll(instance, visibleChunks, eachCallback, endCallback));
+
+ } else {
+ // 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);
+ }
+ }
+
+ /**
+ * 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
+ * @see #setInstance(Instance, Position)
+ */
+ @Override
+ public void setInstance(@NotNull Instance instance) {
+ 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, Position)}.
+ *
+ * @param spawnPosition the position to teleport the player
+ * @param firstSpawn true if this is the player first spawn
+ */
+ private void spawnPlayer(@NotNull Instance instance, @Nullable Position spawnPosition, boolean firstSpawn) {
+ this.viewableEntities.forEach(entity -> entity.removeViewer(this));
+
+ super.setInstance(instance);
+
+ if (spawnPosition != null && !position.isSimilar(spawnPosition)) {
+ teleport(spawnPosition,
+ position.inSameChunk(spawnPosition) ? () -> refreshVisibleChunks(getChunk()) : null);
+ } else {
+ refreshVisibleChunks(getChunk());
+ }
+
+ PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
+ callEvent(PlayerSpawnEvent.class, spawnEvent);
+ }
+
+ @NotNull
+ @Override
+ public Consumer
+ * Sets to null to show the player username.
+ *
+ * @param displayName the display name, null to display the username
+ */
+ public void setDisplayName(@Nullable ColoredText displayName) {
+ this.displayName = displayName;
+
+ PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_DISPLAY_NAME);
+ infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateDisplayName(getUuid(), displayName));
+ sendPacketToViewersAndSelf(infoPacket);
+ }
+
+ /**
+ * Gets the player skin.
+ *
+ * @return the player skin object,
+ * null means that the player has his {@link #getUuid()} default skin
+ */
+ @Nullable
+ public 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();
+ destroyEntitiesPacket.entityIds = new int[]{getEntityId()};
+
+ final PlayerInfoPacket removePlayerPacket = getRemovePlayerToList();
+ final PlayerInfoPacket addPlayerPacket = getAddPlayerToList();
+
+ RespawnPacket respawnPacket = new RespawnPacket();
+ respawnPacket.dimensionType = getDimensionType();
+ respawnPacket.gameMode = getGameMode();
+ respawnPacket.isFlat = levelFlat;
+
+ playerConnection.sendPacket(removePlayerPacket);
+ playerConnection.sendPacket(destroyEntitiesPacket);
+ playerConnection.sendPacket(respawnPacket);
+ playerConnection.sendPacket(addPlayerPacket);
+
+ {
+ // Remove player
+ sendPacketToViewers(removePlayerPacket);
+ sendPacketToViewers(destroyEntitiesPacket);
+
+ // Show player again
+ getViewers().forEach(player -> showPlayer(player.getPlayerConnection()));
+ }
+
+ getInventory().update();
+ teleport(getPosition());
+ }
+
+ /**
+ * 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;
+ sendChangeGameStatePacket(ChangeGameStatePacket.Reason.ENABLE_RESPAWN_SCREEN, enableRespawnScreen ? 0 : 1);
+ }
+
+ /**
+ * Gets the player username.
+ *
+ * @return the player username
+ */
+ @NotNull
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Changes the internal player name, used for the {@link PlayerPreLoginEvent}
+ * mostly unsafe outside of it.
+ *
+ * @param username the new player name
+ */
+ protected void setUsername(@NotNull String username) {
+ this.username = username;
+ }
+
+ private void sendChangeGameStatePacket(@NotNull ChangeGameStatePacket.Reason reason, float value) {
+ ChangeGameStatePacket changeGameStatePacket = new ChangeGameStatePacket();
+ changeGameStatePacket.reason = reason;
+ changeGameStatePacket.value = value;
+ playerConnection.sendPacket(changeGameStatePacket);
+ }
+
+ /**
+ * 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);
+ callEvent(ItemDropEvent.class, itemDropEvent);
+ return !itemDropEvent.isCancelled();
+ }
+
+ /**
+ * Sets the player resource pack.
+ *
+ * @param resourcePack the resource pack
+ */
+ public void setResourcePack(@NotNull ResourcePack resourcePack) {
+ Check.notNull(resourcePack, "The resource pack cannot be null");
+ final String url = resourcePack.getUrl();
+ final String hash = resourcePack.getHash();
+
+ ResourcePackSendPacket resourcePackSendPacket = new ResourcePackSendPacket();
+ resourcePackSendPacket.url = url;
+ resourcePackSendPacket.hash = hash;
+ playerConnection.sendPacket(resourcePackSendPacket);
+ }
+
+ /**
+ * 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 Position 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 Position targetPosition,
+ @Nullable Entity entity, @Nullable FacePoint targetPoint) {
+ FacePlayerPacket facePlayerPacket = new FacePlayerPacket();
+ facePlayerPacket.entityFacePosition = facePoint == FacePoint.EYE ?
+ FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET;
+ facePlayerPacket.targetX = targetPosition.getX();
+ facePlayerPacket.targetY = targetPosition.getY();
+ facePlayerPacket.targetZ = targetPosition.getZ();
+ if (entity != null) {
+ facePlayerPacket.entityId = entity.getEntityId();
+ facePlayerPacket.entityFacePosition = targetPoint == FacePoint.EYE ?
+ FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET;
+ }
+ playerConnection.sendPacket(facePlayerPacket);
+ }
+
+ /**
+ * Sets the camera at {@code entity} eyes.
+ *
+ * @param entity the entity to spectate
+ */
+ public void spectate(@NotNull Entity entity) {
+ CameraPacket cameraPacket = new CameraPacket();
+ cameraPacket.cameraId = entity.getEntityId();
+ playerConnection.sendPacket(cameraPacket);
+ }
+
+ /**
+ * 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(Position)}.
+ *
+ * @return a copy of the default respawn point
+ */
+ @NotNull
+ public Position getRespawnPoint() {
+ return respawnPoint.clone();
+ }
+
+ /**
+ * Changes the default spawn point.
+ *
+ * @param respawnPoint the player respawn point
+ */
+ public void setRespawnPoint(@NotNull Position respawnPoint) {
+ this.respawnPoint = respawnPoint;
+ }
+
+ /**
+ * Called after the player teleportation to refresh his position
+ * and send data to his new viewers.
+ */
+ protected void refreshAfterTeleport() {
+ getInventory().update();
+
+ SpawnPlayerPacket spawnPlayerPacket = new SpawnPlayerPacket();
+ spawnPlayerPacket.entityId = getEntityId();
+ spawnPlayerPacket.playerUuid = getUuid();
+ spawnPlayerPacket.position = getPosition();
+ sendPacketToViewers(spawnPlayerPacket);
+
+ // Update for viewers
+ sendPacketToViewersAndSelf(getVelocityPacket());
+ sendPacketToViewersAndSelf(getMetadataPacket());
+ playerConnection.sendPacket(getPropertiesPacket());
+ syncEquipments();
+
+ {
+ // Send new chunks
+ final BlockPosition pos = position.toBlockPosition();
+ final Chunk chunk = instance.getChunk(pos.getX() >> 4, pos.getZ() >> 4);
+ Check.notNull(chunk, "Tried to interact with an unloaded chunk.");
+ refreshVisibleChunks(chunk);
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Sends an {@link UpdateHealthPacket} to refresh client-side information about health and food.
+ */
+ protected void sendUpdateHealthPacket() {
+ UpdateHealthPacket updateHealthPacket = new UpdateHealthPacket();
+ updateHealthPacket.health = getHealth();
+ updateHealthPacket.food = food;
+ updateHealthPacket.foodSaturation = foodSaturation;
+ playerConnection.sendPacket(updateHealthPacket);
+ }
+
+ /**
+ * 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;
+ sendExperienceUpdatePacket();
+ }
+
+ /**
+ * 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;
+ sendExperienceUpdatePacket();
+ }
+
+ /**
+ * Sends a {@link SetExperiencePacket} to refresh client-side information about the experience bar.
+ */
+ protected void sendExperienceUpdatePacket() {
+ SetExperiencePacket setExperiencePacket = new SetExperiencePacket();
+ setExperiencePacket.percentage = exp;
+ setExperiencePacket.level = level;
+ playerConnection.sendPacket(setExperiencePacket);
+ }
+
+ /**
+ * Called when the player changes chunk (move from one to another).
+ * Can also be used to refresh the list of chunks that the client should see based on {@link #getChunkRange()}.
+ *
+ * It does remove and add the player from the chunks viewers list when removed or added.
+ * It also calls the events {@link PlayerChunkUnloadEvent} and {@link PlayerChunkLoadEvent}.
+ *
+ * @param newChunk the current/new player chunk (can be the current one)
+ */
+ public void refreshVisibleChunks(@NotNull Chunk newChunk) {
+ // Previous chunks indexes
+ final long[] lastVisibleChunks = viewableChunks.stream().mapToLong(viewableChunks ->
+ ChunkUtils.getChunkIndex(viewableChunks.getChunkX(), viewableChunks.getChunkZ())
+ ).toArray();
+
+ // New chunks indexes
+ final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunk.toPosition(), getChunkRange());
+
+ // Find the difference between the two arrays
+ final int[] oldChunks = ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks);
+ final int[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks);
+
+ // Unload old chunks
+ for (int index : oldChunks) {
+ final long chunkIndex = lastVisibleChunks[index];
+ final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
+ final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
+
+ UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
+ unloadChunkPacket.chunkX = chunkX;
+ unloadChunkPacket.chunkZ = chunkZ;
+ playerConnection.sendPacket(unloadChunkPacket);
+
+ final Chunk chunk = instance.getChunk(chunkX, chunkZ);
+ if (chunk != null)
+ chunk.removeViewer(this);
+ }
+
+ // Update client render distance
+ updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
+
+ // Load new chunks
+ for (int index : newChunks) {
+ final long chunkIndex = updatedVisibleChunks[index];
+ final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
+ final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
+
+ instance.loadOptionalChunk(chunkX, chunkZ, chunk -> {
+ if (chunk == null) {
+ // Cannot load chunk (auto load is not enabled)
+ return;
+ }
+ chunk.addViewer(this);
+ });
+ }
+ }
+
+ /**
+ * Refreshes the list of entities that the player should be able to see based on {@link MinecraftServer#getEntityViewDistance()}
+ * and {@link Entity#isAutoViewable()}.
+ *
+ * @param newChunk the new chunk of the player (can be the current one)
+ */
+ public void refreshVisibleEntities(@NotNull Chunk newChunk) {
+ final int entityViewDistance = MinecraftServer.getEntityViewDistance();
+ final float maximalDistance = entityViewDistance * Chunk.CHUNK_SECTION_SIZE;
+
+ // Manage already viewable entities
+ this.viewableEntities.forEach(entity -> {
+ final float distance = entity.getDistance(this);
+ if (distance > maximalDistance) {
+ // Entity shouldn't be viewable anymore
+ if (isAutoViewable()) {
+ entity.removeViewer(this);
+ }
+ if (entity instanceof Player && entity.isAutoViewable()) {
+ removeViewer((Player) entity);
+ }
+ }
});
- 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() {
- Inventory openInventory = getOpenInventory();
-
- // Drop cursor item when closing inventory
- ItemStack cursorItem;
- if (openInventory == null) {
- cursorItem = getInventory().getCursorItem();
- getInventory().setCursorItem(ItemStack.getAirItem());
- } else {
- cursorItem = openInventory.getCursorItem(this);
- openInventory.setCursorItem(this, ItemStack.getAirItem());
- }
- if (!cursorItem.isAir()) {
- // Add item to inventory if he hasn't been able to drop it
- if (!dropItem(cursorItem)) {
- getInventory().addItemStack(cursorItem);
- }
- }
-
- CloseWindowPacket closeWindowPacket = new CloseWindowPacket();
- if (openInventory == null) {
- closeWindowPacket.windowId = 0;
- } else {
- closeWindowPacket.windowId = openInventory.getWindowId();
- openInventory.removeViewer(this); // Clear cache
- this.openInventory = null;
- }
- playerConnection.sendPacket(closeWindowPacket);
- inventory.update();
- this.didCloseInventory = true;
- }
-
- /**
- * Used internally to prevent an inventory click to be processed when the inventory listeners
- * closed the inventory.
- *
- * 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
- */
- public void UNSAFE_changeDidCloseInventory(boolean didCloseInventory) {
- this.didCloseInventory = didCloseInventory;
- }
-
- /**
- * Gets the player viewable chunks.
- *
- * WARNING: adding or removing a chunk there will not load/unload it, use {@link
- * Chunk#addViewer(Player)} or {@link Chunk#removeViewer(Player)}.
- *
- * @return a {@link Set} containing all the chunks that the player sees
- */
- public Set 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) {
- 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". see
- *
- * WARNING: this has nothing to do with {@link CustomBlock#getBreakDelay(Player, BlockPosition,
- * byte, Set)}.
- *
- * @param instantBreak true to allow instant break
- */
- 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 getWalkingSpeed() {
- return walkingSpeed;
- }
-
- public void setWalkingSpeed(float walkingSpeed) {
- this.walkingSpeed = walkingSpeed;
- 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
- */
- @NotNull
- public 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(EntityEquipmentPacket.Slot.MAIN_HAND);
-
- refreshEating(false);
- }
-
- public void refreshEating(boolean isEating, long eatingTime) {
- this.isEating = isEating;
- if (isEating) {
- this.startEatingTime = System.currentTimeMillis();
- this.eatingTime = eatingTime;
- } else {
- this.startEatingTime = 0;
- }
- }
-
- public void refreshEating(boolean isEating) {
- refreshEating(isEating, 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
- */
- @Nullable
- public ItemUpdateStateEvent callItemUpdateStateEvent(boolean allowFood) {
- final Material mainHandMat = getItemInMainHand().getMaterial();
- final Material offHandMat = getItemInOffHand().getMaterial();
- final boolean isOffhand = offHandMat.hasState();
-
- final ItemStack updatedItem =
- isOffhand ? getItemInOffHand() : mainHandMat.hasState() ? getItemInMainHand() : null;
- if (updatedItem == null) // No item with state, cancel
- return null;
-
- final boolean isFood = updatedItem.getMaterial().isFood();
-
- if (isFood && !allowFood) return null;
-
- final Hand hand = isOffhand ? Hand.OFF : Hand.MAIN;
- ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(this, hand, updatedItem);
- callEvent(ItemUpdateStateEvent.class, itemUpdateStateEvent);
-
- return itemUpdateStateEvent;
- }
-
- /**
- * Makes the player digging a custom block, see {@link #resetTargetBlock()} to rewind.
- *
- * @param targetCustomBlock the custom block to dig
- * @param targetBlockPosition the custom block position
- * @param breakers the breakers of the block, can be null if {@code this} is the only breaker
- */
- public void setTargetBlock(
- @NotNull CustomBlock targetCustomBlock,
- @NotNull BlockPosition targetBlockPosition,
- @Nullable Set 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) {
- SpawnPlayerPacket spawnPlayerPacket = new SpawnPlayerPacket();
- spawnPlayerPacket.entityId = getEntityId();
- spawnPlayerPacket.playerUuid = getUuid();
- spawnPlayerPacket.position = getPosition();
-
- connection.sendPacket(getAddPlayerToList());
-
- connection.sendPacket(spawnPlayerPacket);
- connection.sendPacket(getVelocityPacket());
- connection.sendPacket(getMetadataPacket());
-
- // Equipments synchronization
- syncEquipments(connection);
-
- if (hasPassenger()) {
- connection.sendPacket(getPassengersPacket());
- }
-
- // Team
- if (this.getTeam() != null) connection.sendPacket(this.getTeam().createTeamsCreationPacket());
-
- EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
- entityHeadLookPacket.entityId = getEntityId();
- entityHeadLookPacket.yaw = position.getYaw();
- connection.sendPacket(entityHeadLookPacket);
- }
-
- @NotNull
- @Override
- public ItemStack getItemInMainHand() {
- return inventory.getItemInMainHand();
- }
-
- @Override
- public void setItemInMainHand(@NotNull ItemStack itemStack) {
- inventory.setItemInMainHand(itemStack);
- }
-
- @NotNull
- @Override
- public ItemStack getItemInOffHand() {
- return inventory.getItemInOffHand();
- }
-
- @Override
- public void setItemInOffHand(@NotNull ItemStack itemStack) {
- inventory.setItemInOffHand(itemStack);
- }
-
- @NotNull
- @Override
- public ItemStack getHelmet() {
- return inventory.getHelmet();
- }
-
- @Override
- public void setHelmet(@NotNull ItemStack itemStack) {
- inventory.setHelmet(itemStack);
- }
-
- @NotNull
- @Override
- public ItemStack getChestplate() {
- return inventory.getChestplate();
- }
-
- @Override
- public void setChestplate(@NotNull ItemStack itemStack) {
- inventory.setChestplate(itemStack);
- }
-
- @NotNull
- @Override
- public ItemStack getLeggings() {
- return inventory.getLeggings();
- }
-
- @Override
- public void setLeggings(@NotNull ItemStack itemStack) {
- inventory.setLeggings(itemStack);
- }
-
- @NotNull
- @Override
- public ItemStack getBoots() {
- return inventory.getBoots();
- }
-
- @Override
- public void setBoots(@NotNull ItemStack itemStack) {
- inventory.setBoots(itemStack);
- }
-
- /** 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 enum ChatMode {
- ENABLED,
- COMMANDS_ONLY,
- HIDDEN
- }
-
- public class PlayerSettings {
-
- private String locale;
- private byte viewDistance;
- private ChatMode chatMode;
- private boolean chatColors;
- private byte displayedSkinParts;
- private MainHand mainHand;
-
- private boolean firstRefresh = true;
-
- /**
- * 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;
- }
-
- /**
- * Gets the player chat mode.
- *
- * @return the player chat mode
- */
- public ChatMode getChatMode() {
- return chatMode;
- }
-
- /**
- * 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;
- }
-
- /**
- * 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 chatMode the player chat mode
- * @param chatColors the player chat colors
- * @param displayedSkinParts the player displayed skin parts
- * @param mainHand the player main hand
- */
- public void refresh(
- String locale,
- byte viewDistance,
- ChatMode chatMode,
- boolean chatColors,
- byte displayedSkinParts,
- MainHand mainHand) {
-
- final boolean viewDistanceChanged = !firstRefresh && this.viewDistance != viewDistance;
-
- this.locale = locale;
- this.viewDistance = viewDistance;
- this.chatMode = chatMode;
- this.chatColors = chatColors;
- this.displayedSkinParts = displayedSkinParts;
- this.mainHand = mainHand;
- sendMetadataIndex(16);
-
- this.firstRefresh = false;
-
- // Client changed his view distance in the settings
- if (viewDistanceChanged) {
- final Chunk playerChunk = getChunk();
- if (playerChunk != null) {
- refreshVisibleChunks(playerChunk);
+ // Manage entities in unchecked chunks
+ final long[] chunksInRange = ChunkUtils.getChunksInRange(newChunk.toPosition(), entityViewDistance);
+
+ for (long chunkIndex : chunksInRange) {
+ final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
+ final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
+ final Chunk chunk = instance.getChunk(chunkX, chunkZ);
+ if (chunk == null)
+ continue;
+ instance.getChunkEntities(chunk).forEach(entity -> {
+ if (isAutoViewable() && !entity.viewers.contains(this)) {
+ entity.addViewer(this);
+ }
+
+ if (entity instanceof Player && entity.isAutoViewable() && !viewers.contains(entity)) {
+ addViewer((Player) entity);
+ }
+ });
}
- }
+
}
- }
+
+ @Override
+ public void teleport(@NotNull Position position, @Nullable long[] chunks, @Nullable Runnable callback) {
+ super.teleport(position, chunks, () -> {
+ updatePlayerPosition();
+ OptionalCallback.execute(callback);
+ });
+ }
+
+ @Override
+ public void teleport(@NotNull Position position, @Nullable Runnable callback) {
+ final boolean sameChunk = getPosition().inSameChunk(position);
+ final long[] chunks = sameChunk ? null :
+ ChunkUtils.getChunksInRange(position, getChunkRange());
+ teleport(position, chunks, callback);
+ }
+
+ @Override
+ public void teleport(@NotNull Position position) {
+ teleport(position, null);
+ }
+
+ /**
+ * Gets the player connection.
+ *
+ * Used to send packets and get stuff related to the connection.
+ *
+ * @return the player connection
+ */
+ @NotNull
+ public PlayerConnection getPlayerConnection() {
+ return playerConnection;
+ }
+
+ /**
+ * 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
+ */
+ @NotNull
+ public PlayerSettings getSettings() {
+ return settings;
+ }
+
+ /**
+ * Gets the player dimension.
+ *
+ * @return the player current dimension
+ */
+ public DimensionType getDimensionType() {
+ return dimensionType;
+ }
+
+ @NotNull
+ public 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
+ */
+ public void setGameMode(@NotNull GameMode gameMode) {
+ Check.notNull(gameMode, "GameMode cannot be null");
+ this.gameMode = gameMode;
+ sendChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.getId());
+
+ PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_GAMEMODE);
+ infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateGamemode(getUuid(), gameMode));
+ sendPacketToViewersAndSelf(infoPacket);
+ }
+
+ /**
+ * Gets if this player is in creative. Used for code readability.
+ *
+ * @return true if the player is in creative mode
+ */
+ public boolean isCreative() {
+ return gameMode == GameMode.CREATIVE;
+ }
+
+ /**
+ * Changes the dimension of the player.
+ * Mostly unsafe since it requires sending chunks after.
+ *
+ * @param dimensionType the new player dimension
+ */
+ protected void sendDimension(@NotNull DimensionType dimensionType) {
+ Check.notNull(dimensionType, "Dimension cannot be null!");
+ Check.argCondition(dimensionType.equals(getDimensionType()), "The dimension needs to be different than the current one!");
+
+ this.dimensionType = dimensionType;
+ RespawnPacket respawnPacket = new RespawnPacket();
+ respawnPacket.dimensionType = dimensionType;
+ respawnPacket.gameMode = gameMode;
+ respawnPacket.isFlat = levelFlat;
+ playerConnection.sendPacket(respawnPacket);
+ }
+
+ /**
+ * Kicks the player with a reason.
+ *
+ * @param text the kick reason
+ */
+ public void kick(@NotNull ColoredText text) {
+ DisconnectPacket disconnectPacket = new DisconnectPacket();
+ disconnectPacket.message = text;
+ playerConnection.sendPacket(disconnectPacket);
+ playerConnection.disconnect();
+ playerConnection.refreshOnline(false);
+ }
+
+ /**
+ * Kicks the player with a reason.
+ *
+ * @param message the kick reason
+ */
+ public void kick(@NotNull String message) {
+ kick(ColoredText.of(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");
+
+ HeldItemChangePacket heldItemChangePacket = new HeldItemChangePacket();
+ heldItemChangePacket.slot = slot;
+ playerConnection.sendPacket(heldItemChangePacket);
+ refreshHeldSlot(slot);
+ }
+
+ /**
+ * Gets the player held slot (0-8).
+ *
+ * @return the current held slot for the player
+ */
+ public byte getHeldSlot() {
+ return heldSlot;
+ }
+
+ public void setTeam(Team team) {
+ super.setTeam(team);
+ if (team != null)
+ getPlayerConnection().sendPacket(team.createTeamsCreationPacket());
+ }
+
+ /**
+ * 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 if the player is sneaking.
+ *
+ * WARNING: this can be bypassed by hacked client, this is only what the client told the server.
+ *
+ * @return true if the player is sneaking
+ */
+ public boolean isSneaking() {
+ return crouched;
+ }
+
+ /**
+ * Gets if the player is sprinting.
+ *
+ * WARNING: this can be bypassed by hacked client, this is only what the client told the server.
+ *
+ * @return true if the player is sprinting
+ */
+ public boolean isSprinting() {
+ return sprinting;
+ }
+
+ /**
+ * Used to get the {@link CustomBlock} that the player is currently mining.
+ *
+ * @return the currently mined {@link CustomBlock} by the player, null if there is not
+ */
+ @Nullable
+ public CustomBlock getCustomBlockTarget() {
+ return targetCustomBlock;
+ }
+
+ /**
+ * Gets the player open inventory.
+ *
+ * @return the currently open inventory, null if there is not (player inventory is not detected)
+ */
+ @Nullable
+ public 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) {
+ Check.notNull(inventory, "Inventory cannot be null, use Player#closeInventory() to close current");
+
+ InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(this, inventory);
+
+ callCancellableEvent(InventoryOpenEvent.class, inventoryOpenEvent, () -> {
+
+ if (getOpenInventory() != null) {
+ closeInventory();
+ }
+
+ Inventory newInventory = inventoryOpenEvent.getInventory();
+
+ if (newInventory == null) {
+ // just close the inventory
+ return;
+ }
+
+ OpenWindowPacket openWindowPacket = new OpenWindowPacket();
+ openWindowPacket.windowId = newInventory.getWindowId();
+ openWindowPacket.windowType = newInventory.getInventoryType().getWindowType();
+ openWindowPacket.title = newInventory.getTitle();
+ playerConnection.sendPacket(openWindowPacket);
+ 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() {
+ Inventory openInventory = getOpenInventory();
+
+ // Drop cursor item when closing inventory
+ ItemStack cursorItem;
+ if (openInventory == null) {
+ cursorItem = getInventory().getCursorItem();
+ getInventory().setCursorItem(ItemStack.getAirItem());
+ } else {
+ cursorItem = openInventory.getCursorItem(this);
+ openInventory.setCursorItem(this, ItemStack.getAirItem());
+ }
+ if (!cursorItem.isAir()) {
+ // Add item to inventory if he hasn't been able to drop it
+ if (!dropItem(cursorItem)) {
+ getInventory().addItemStack(cursorItem);
+ }
+ }
+
+ CloseWindowPacket closeWindowPacket = new CloseWindowPacket();
+ if (openInventory == null) {
+ closeWindowPacket.windowId = 0;
+ } else {
+ closeWindowPacket.windowId = openInventory.getWindowId();
+ openInventory.removeViewer(this); // Clear cache
+ this.openInventory = null;
+ }
+ playerConnection.sendPacket(closeWindowPacket);
+ inventory.update();
+ this.didCloseInventory = true;
+ }
+
+ /**
+ * Used internally to prevent an inventory click to be processed
+ * when the inventory listeners closed the inventory.
+ *
+ * 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
+ */
+ public void UNSAFE_changeDidCloseInventory(boolean didCloseInventory) {
+ this.didCloseInventory = didCloseInventory;
+ }
+
+ /**
+ * Gets the player viewable chunks.
+ *
+ * WARNING: adding or removing a chunk there will not load/unload it,
+ * use {@link Chunk#addViewer(Player)} or {@link Chunk#removeViewer(Player)}.
+ *
+ * @return a {@link Set} containing all the chunks that the player sees
+ */
+ public Set
+ * 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) {
+ 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".
+ * see
+ *
+ * WARNING: this has nothing to do with {@link CustomBlock#getBreakDelay(Player, BlockPosition, byte, Set)}.
+ *
+ * @param instantBreak true to allow instant break
+ */
+ 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 getWalkingSpeed() {
+ return walkingSpeed;
+ }
+
+ public void setWalkingSpeed(float walkingSpeed) {
+ this.walkingSpeed = walkingSpeed;
+ 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
+ */
+ @NotNull
+ public 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(EntityEquipmentPacket.Slot.MAIN_HAND);
+
+ refreshEating(false);
+ }
+
+ public void refreshEating(boolean isEating, long eatingTime) {
+ this.isEating = isEating;
+ if (isEating) {
+ this.startEatingTime = System.currentTimeMillis();
+ this.eatingTime = eatingTime;
+ } else {
+ this.startEatingTime = 0;
+ }
+ }
+
+ public void refreshEating(boolean isEating) {
+ refreshEating(isEating, 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
+ */
+ @Nullable
+ public ItemUpdateStateEvent callItemUpdateStateEvent(boolean allowFood) {
+ final Material mainHandMat = getItemInMainHand().getMaterial();
+ final Material offHandMat = getItemInOffHand().getMaterial();
+ final boolean isOffhand = offHandMat.hasState();
+
+ final ItemStack updatedItem = isOffhand ? getItemInOffHand() :
+ mainHandMat.hasState() ? getItemInMainHand() : null;
+ if (updatedItem == null) // No item with state, cancel
+ return null;
+
+ final boolean isFood = updatedItem.getMaterial().isFood();
+
+ if (isFood && !allowFood)
+ return null;
+
+ final Hand hand = isOffhand ? Hand.OFF : Hand.MAIN;
+ ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(this, hand, updatedItem);
+ callEvent(ItemUpdateStateEvent.class, itemUpdateStateEvent);
+
+ return itemUpdateStateEvent;
+ }
+
+ /**
+ * Makes the player digging a custom block, see {@link #resetTargetBlock()} to rewind.
+ *
+ * @param targetCustomBlock the custom block to dig
+ * @param targetBlockPosition the custom block position
+ * @param breakers the breakers of the block, can be null if {@code this} is the only breaker
+ */
+ public void setTargetBlock(@NotNull CustomBlock targetCustomBlock, @NotNull BlockPosition targetBlockPosition,
+ @Nullable Set
+ * 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) {
+ SpawnPlayerPacket spawnPlayerPacket = new SpawnPlayerPacket();
+ spawnPlayerPacket.entityId = getEntityId();
+ spawnPlayerPacket.playerUuid = getUuid();
+ spawnPlayerPacket.position = getPosition();
+
+ connection.sendPacket(getAddPlayerToList());
+
+ connection.sendPacket(spawnPlayerPacket);
+ connection.sendPacket(getVelocityPacket());
+ connection.sendPacket(getMetadataPacket());
+
+ // Equipments synchronization
+ syncEquipments(connection);
+
+ if (hasPassenger()) {
+ connection.sendPacket(getPassengersPacket());
+ }
+
+ // Team
+ if (this.getTeam() != null)
+ connection.sendPacket(this.getTeam().createTeamsCreationPacket());
+
+ EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
+ entityHeadLookPacket.entityId = getEntityId();
+ entityHeadLookPacket.yaw = position.getYaw();
+ connection.sendPacket(entityHeadLookPacket);
+ }
+
+ @NotNull
+ @Override
+ public ItemStack getItemInMainHand() {
+ return inventory.getItemInMainHand();
+ }
+
+ @Override
+ public void setItemInMainHand(@NotNull ItemStack itemStack) {
+ inventory.setItemInMainHand(itemStack);
+ }
+
+ @NotNull
+ @Override
+ public ItemStack getItemInOffHand() {
+ return inventory.getItemInOffHand();
+ }
+
+ @Override
+ public void setItemInOffHand(@NotNull ItemStack itemStack) {
+ inventory.setItemInOffHand(itemStack);
+ }
+
+ @NotNull
+ @Override
+ public ItemStack getHelmet() {
+ return inventory.getHelmet();
+ }
+
+ @Override
+ public void setHelmet(@NotNull ItemStack itemStack) {
+ inventory.setHelmet(itemStack);
+ }
+
+ @NotNull
+ @Override
+ public ItemStack getChestplate() {
+ return inventory.getChestplate();
+ }
+
+ @Override
+ public void setChestplate(@NotNull ItemStack itemStack) {
+ inventory.setChestplate(itemStack);
+ }
+
+ @NotNull
+ @Override
+ public ItemStack getLeggings() {
+ return inventory.getLeggings();
+ }
+
+ @Override
+ public void setLeggings(@NotNull ItemStack itemStack) {
+ inventory.setLeggings(itemStack);
+ }
+
+ @NotNull
+ @Override
+ public ItemStack getBoots() {
+ return inventory.getBoots();
+ }
+
+ @Override
+ public void setBoots(@NotNull ItemStack itemStack) {
+ inventory.setBoots(itemStack);
+ }
+
+ /**
+ * 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 enum ChatMode {
+ ENABLED,
+ COMMANDS_ONLY,
+ HIDDEN
+ }
+
+ public class PlayerSettings {
+
+ private String locale;
+ private byte viewDistance;
+ private ChatMode chatMode;
+ private boolean chatColors;
+ private byte displayedSkinParts;
+ private MainHand mainHand;
+
+ private boolean firstRefresh = true;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Gets the player chat mode.
+ *
+ * @return the player chat mode
+ */
+ public ChatMode getChatMode() {
+ return chatMode;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 chatMode the player chat mode
+ * @param chatColors the player chat colors
+ * @param displayedSkinParts the player displayed skin parts
+ * @param mainHand the player main hand
+ */
+ public void refresh(String locale, byte viewDistance, ChatMode chatMode, boolean chatColors,
+ byte displayedSkinParts, MainHand mainHand) {
+
+ final boolean viewDistanceChanged = !firstRefresh && this.viewDistance != viewDistance;
+
+ this.locale = locale;
+ this.viewDistance = viewDistance;
+ this.chatMode = chatMode;
+ this.chatColors = chatColors;
+ this.displayedSkinParts = displayedSkinParts;
+ this.mainHand = mainHand;
+ sendMetadataIndex(16);
+
+ this.firstRefresh = false;
+
+ // Client changed his view distance in the settings
+ if (viewDistanceChanged) {
+ final Chunk playerChunk = getChunk();
+ if (playerChunk != null) {
+ refreshVisibleChunks(playerChunk);
+ }
+ }
+ }
+
+ }
+
}