package net.minestom.server.entity; import com.google.common.collect.Queues; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.inventory.Book; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent.ShowEntity; import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.title.Title; import net.minestom.server.MinecraftServer; import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.adventure.AdventurePacketConvertor; import net.minestom.server.adventure.Localizable; import net.minestom.server.adventure.audience.Audiences; import net.minestom.server.attribute.Attribute; import net.minestom.server.chat.ChatParser; import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.JsonMessage; import net.minestom.server.collision.BoundingBox; import net.minestom.server.command.CommandManager; import net.minestom.server.command.CommandSender; import net.minestom.server.effects.Effects; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.entity.fakeplayer.FakePlayer; import net.minestom.server.entity.vehicle.PlayerVehicleInformation; import net.minestom.server.event.inventory.InventoryOpenEvent; import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.ItemUpdateStateEvent; import net.minestom.server.event.item.PickupExperienceEvent; import net.minestom.server.event.player.*; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.metadata.WrittenBookMeta; import net.minestom.server.listener.PlayerDiggingListener; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.packet.client.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.login.LoginDisconnectPacket; 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; import net.minestom.server.recipe.RecipeManager; import net.minestom.server.resourcepack.ResourcePack; import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.Team; import net.minestom.server.sound.SoundEvent; import net.minestom.server.sound.SoundCategory; import net.minestom.server.stat.PlayerStatistic; import net.minestom.server.utils.*; import net.minestom.server.utils.callback.OptionalCallback; import net.minestom.server.utils.chunk.ChunkCallback; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.instance.InstanceUtils; import net.minestom.server.utils.time.CooldownUtils; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.UpdateOption; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.UnaryOperator; /** * 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, Localizable, HoverEventSource, Identified { private long lastKeepAlive; private boolean answerKeepAlive; private String username; protected final PlayerConnection playerConnection; // All the entities that this player can see protected final Set viewableEntities = ConcurrentHashMap.newKeySet(); private int latency; private Component displayName; private PlayerSkin skin; private DimensionType dimensionType; private GameMode gameMode; // Chunks that the player can view protected final Set viewableChunks = new CopyOnWriteArraySet<>(); private final AtomicInteger teleportId = new AtomicInteger(); private int receivedTeleportId; private final Queue packets = Queues.newConcurrentLinkedQueue(); private final boolean levelFlat; private final PlayerSettings settings; private float exp; private int level; protected PlayerInventory inventory; private Inventory openInventory; // Used internally to allow the closing of inventory within the inventory listener private boolean didCloseInventory; private byte heldSlot; private Position respawnPoint; private int food; private float foodSaturation; private long startEatingTime; private long defaultEatingTime = 1000L; private long eatingTime; private boolean isEating; // Game state (https://wiki.vg/Protocol#Change_Game_State) private boolean enableRespawnScreen; // CustomBlock break delay private CustomBlock targetCustomBlock; private BlockPosition targetBlockPosition; // The last break delay requested private long targetBreakDelay; // Number of tick since the last stage change private long targetBlockBreakCount; // The current stage of the target block, only if multi player breaking is disabled private byte targetStage; // Only used if multi player breaking is disabled, contains only this player private final Set targetBreakers = Collections.singleton(this); // Position synchronization with viewers private double lastPlayerSyncX, lastPlayerSyncY, lastPlayerSyncZ; private float lastPlayerSyncYaw, lastPlayerSyncPitch; // Experience orb pickup protected UpdateOption experiencePickupCooldown = new UpdateOption(10, TimeUnit.TICK); private long lastExperiencePickupCheckTime; private BelowNameTag belowNameTag; private int permissionLevel; private boolean reducedDebugScreenInformation; // Abilities private boolean flying; private boolean allowFlying; private boolean instantBreak; private float flyingSpeed = 0.05f; private float fieldViewModifier = 0.1f; // Statistics private final Map statisticValueMap = new Hashtable<>(); // Vehicle private final PlayerVehicleInformation vehicleInformation = new PlayerVehicleInformation(); // Tick related private final PlayerTickEvent playerTickEvent = new PlayerTickEvent(this); // Adventure private Identity identity; public Player(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection playerConnection) { super(EntityType.PLAYER, uuid); this.username = username; this.playerConnection = playerConnection; setBoundingBox(0.6f, 1.8f, 0.6f); setRespawnPoint(new Position(0, 0, 0)); this.settings = new PlayerSettings(); this.inventory = new PlayerInventory(this); setCanPickupItem(true); // By default // Allow the server to send the next keep alive packet refreshAnswerKeepAlive(true); this.gameMode = GameMode.SURVIVAL; this.dimensionType = DimensionType.OVERWORLD; // Default dimension this.levelFlat = true; getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.1f); // FakePlayer init its connection there playerConnectionInit(); this.identity = Identity.identity(uuid); } /** * Used when the player is created. * Init the player and spawn him. *

* WARNING: executed in the main update thread * UNSAFE: Only meant to be used when a netty player connects through the server. * * @param spawnInstance the player spawn instance (defined in {@link PlayerLoginEvent}) */ public void UNSAFE_init(@NotNull Instance spawnInstance) { this.dimensionType = spawnInstance.getDimensionType(); 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()); } 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(); // FIXME: when using Geyser, this line remove the skin of the client 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 recipesIdentifier = new ArrayList<>(); for (Recipe recipe : recipeManager.getRecipes()) { if (!recipe.shouldShow(this)) continue; recipesIdentifier.add(recipe.getRecipeId()); } 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 // Tags start { TagsPacket tags = TagsPacket.getRequiredTagsPacket(); UpdateTagListEvent event = new UpdateTagListEvent(tags); callEvent(UpdateTagListEvent.class, event); this.playerConnection.sendPacket(tags); } // Tags end // Some client update this.playerConnection.sendPacket(getPropertiesPacket()); // Send default properties refreshHealth(); // Heal and send health packet refreshAbilities(); // Send abilities packet getInventory().update(); } /** * Used to initialize the player connection */ protected void playerConnectionInit() { this.playerConnection.setPlayer(this); } @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 = targetCustomBlock.processStage(instance, targetBlockPosition, this, stageIncrease); if (canContinue) { final Set breakers = targetCustomBlock.getBreakers(instance, targetBlockPosition); refreshBreakDelay(breakers); } else { resetTargetBlock(); } } else { // Let the player object manages the breaking // The custom block doesn't support multi player breaking if (targetStage + stageIncrease >= CustomBlock.MAX_STAGE) { // Break the block instance.breakBlock(this, targetBlockPosition); resetTargetBlock(); } else { // Send the new block break animation packet and refresh data final Chunk chunk = instance.getChunkAt(targetBlockPosition); final int entityId = targetCustomBlock.getBreakEntityId(this); final BlockBreakAnimationPacket blockBreakAnimationPacket = new BlockBreakAnimationPacket(entityId, targetBlockPosition, targetStage); Check.notNull(chunk, "Tried to interact with an unloaded chunk."); chunk.sendPacketToViewers(blockBreakAnimationPacket); refreshBreakDelay(targetBreakers); this.targetStage += stageIncrease; } } } } // Experience orb pickup if (!CooldownUtils.hasCooldown(time, lastExperiencePickupCheckTime, experiencePickupCooldown)) { this.lastExperiencePickupCheckTime = time; final Chunk chunk = getChunk(); // TODO check surrounding chunks final Set entities = instance.getChunkEntities(chunk); for (Entity entity : entities) { if (entity instanceof ExperienceOrb) { final ExperienceOrb experienceOrb = (ExperienceOrb) entity; final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox(); if (expandedBoundingBox.intersect(itemBoundingBox)) { if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled()) continue; PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(experienceOrb); callCancellableEvent(PickupExperienceEvent.class, pickupExperienceEvent, () -> { short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player entity.remove(); }); } } } } // Eating animation if (isEating()) { if (time - startEatingTime >= eatingTime) { refreshEating(false); 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 if (!viewers.isEmpty()) { 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 updatePacket = EntityRotationPacket.getPacket(getEntityId(), position.getYaw(), position.getPitch(), onGround); } 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; } // 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()) { Component deathText; Component chatMessage; // get death screen text to the killed player { if (lastDamageSource != null) { deathText = lastDamageSource.buildDeathScreenText(this); } else { // may happen if killed by the server without applying damage deathText = Component.text("Killed by poor programming."); } } // get death message to chat { if (lastDamageSource != null) { chatMessage = lastDamageSource.buildDeathMessage(this); } else { // may happen if killed by the server without applying damage chatMessage = Component.text(getUsername() + " was killed by poor programming."); } } // Call player death event PlayerDeathEvent playerDeathEvent = new PlayerDeathEvent(this, deathText, chatMessage); callEvent(PlayerDeathEvent.class, playerDeathEvent); deathText = playerDeathEvent.getDeathText(); chatMessage = playerDeathEvent.getChatMessage(); // #buildDeathScreenText can return null, check here if (deathText != null) { CombatEventPacket deathPacket = CombatEventPacket.death(this, null, deathText); playerConnection.sendPacket(deathPacket); } // #buildDeathMessage can return null, check here if (chatMessage != null) { Audiences.players().sendMessage(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 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 bossBars = net.minestom.server.bossbar.BossBar.getBossBars(this); if (bossBars != null) { for (net.minestom.server.bossbar.BossBar bossBar : bossBars) { bossBar.removeViewer(this); } } } MinecraftServer.getBossBarManager().removeAllBossBars(this); // Advancement tabs cache { Set advancementTabs = AdvancementTab.getTabs(this); if (advancementTabs != null) { for (AdvancementTab advancementTab : advancementTabs) { advancementTab.removeViewer(this); } } } // Item ownership cache { ItemStack[] itemStacks = inventory.getItemStacks(); for (ItemStack itemStack : itemStacks) { ItemStack.DATA_OWNERSHIP.clearCache(itemStack.getIdentifier()); } } // Clear all viewable entities this.viewableEntities.forEach(entity -> entity.removeViewer(this)); // Clear all viewable chunks this.viewableChunks.forEach(chunk -> { if (chunk.isLoaded()) chunk.removeViewer(this); }); resetTargetBlock(); playerConnection.disconnect(); } @Override public boolean addViewer0(@NotNull Player player) { if (player == this) { return false; } PlayerConnection viewerConnection = player.getPlayerConnection(); viewerConnection.sendPacket(getAddPlayerToList()); return super.addViewer0(player); } @Override public boolean removeViewer0(@NotNull Player player) { if (player == this || !super.removeViewer0(player)) { return false; } PlayerConnection viewerConnection = player.getPlayerConnection(); viewerConnection.sendPacket(getRemovePlayerToList()); // Team if (this.getTeam() != null && this.getTeam().getMembers().size() == 1) {// If team only contains "this" player viewerConnection.sendPacket(this.getTeam().createTeamDestructionPacket()); } return true; } @Override public void sendPacketToViewersAndSelf(@NotNull ServerPacket packet) { this.playerConnection.sendPacket(packet); super.sendPacketToViewersAndSelf(packet); } /** * Changes the player instance and load surrounding chunks if needed. *

* Be aware that because chunk operations are expensive, * it is possible for this method to be non-blocking when retrieving chunks is required. * * @param instance the new player instance * @param spawnPosition the new position of the player */ @Override public void setInstance(@NotNull Instance instance, @NotNull Position spawnPosition) { 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) || !spawnPosition.inSameChunk(this.position); if (needWorldRefresh) { // TODO: Handle player reconnections, must be false in that case too final boolean firstSpawn = this.instance == null; // Send the new dimension if player isn't in any instance or if the dimension is different final DimensionType instanceDimensionType = instance.getDimensionType(); final boolean dimensionChange = dimensionType != instanceDimensionType; if (dimensionChange) { 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(spawnPosition.getX()); final int chunkZ = ChunkUtils.getChunkCoordinate(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, true, dimensionChange); }; // 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, false, 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, @NotNull Position spawnPosition, boolean firstSpawn, boolean updateChunks, boolean dimensionChange) { // Clear previous instance elements if (!firstSpawn) { this.viewableChunks.forEach(chunk -> chunk.removeViewer(this)); this.viewableEntities.forEach(entity -> entity.removeViewer(this)); } super.setInstance(instance, spawnPosition); if (!position.isSimilar(spawnPosition) && !firstSpawn) { // Player changed instance at a different position teleport(spawnPosition); } else if (updateChunks) { // Send newly visible chunks to player once spawned in the instance refreshVisibleChunks(); } if (dimensionChange || firstSpawn) { updatePlayerPosition(); // So the player doesn't get stuck this.inventory.update(); } PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn); callEvent(PlayerSpawnEvent.class, spawnEvent); } /** * Sends a plugin message to the player. * * @param channel the message channel * @param data the message data */ public void sendPluginMessage(@NotNull String channel, @NotNull byte[] data) { PluginMessagePacket pluginMessagePacket = new PluginMessagePacket(); pluginMessagePacket.channel = channel; pluginMessagePacket.data = data; playerConnection.sendPacket(pluginMessagePacket); } /** * Sends a plugin message to the player. *

* Message encoded to UTF-8. * * @param channel the message channel * @param message the message */ public void sendPluginMessage(@NotNull String channel, @NotNull String message) { final byte[] bytes = message.getBytes(StandardCharsets.UTF_8); sendPluginMessage(channel, bytes); } /** * Sends a legacy message with the specified color char. * * @param text the text with the legacy color formatting * @param colorChar the color character * @deprecated Use {@link #sendMessage(Component)} */ @Deprecated public void sendLegacyMessage(@NotNull String text, char colorChar) { ColoredText coloredText = ColoredText.ofLegacy(text, colorChar); sendJsonMessage(coloredText.toString()); } /** * Sends a legacy message with the default color char {@link ChatParser#COLOR_CHAR}. * * @param text the text with the legacy color formatting * @deprecated Use {@link #sendMessage(Component)} */ @Deprecated public void sendLegacyMessage(@NotNull String text) { ColoredText coloredText = ColoredText.ofLegacy(text, ChatParser.COLOR_CHAR); sendJsonMessage(coloredText.toString()); } /** * @deprecated Use {@link #sendMessage(Component)} */ @Deprecated public void sendJsonMessage(@NotNull String json) { this.sendMessage(json); } @Override public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) { ChatMessagePacket chatMessagePacket = new ChatMessagePacket(message, ChatMessagePacket.Position.fromMessageType(type), source.uuid()); playerConnection.sendPacket(chatMessagePacket); } /** * Makes the player send a message (can be used for commands). * * @param message the message that the player will send */ public void chat(@NotNull String message) { ClientChatMessagePacket chatMessagePacket = new ClientChatMessagePacket(); chatMessagePacket.message = message; addPacketToQueue(chatMessagePacket); } /** * Plays a sound from the {@link SoundEvent} enum. * * @param sound the sound to play * @param soundCategory the sound category * @param x the effect X * @param y the effect Y * @param z the effect Z * @param volume the volume of the sound (1 is 100%) * @param pitch the pitch of the sound, between 0.5 and 2.0 * @deprecated Use {@link #playSound(net.kyori.adventure.sound.Sound, double, double, double)} */ @Deprecated public void playSound(@NotNull SoundEvent sound, @NotNull SoundCategory soundCategory, int x, int y, int z, float volume, float pitch) { SoundEffectPacket soundEffectPacket = new SoundEffectPacket(); soundEffectPacket.soundId = sound.getId(); soundEffectPacket.soundSource = soundCategory.asSource(); soundEffectPacket.x = x; soundEffectPacket.y = y; soundEffectPacket.z = z; soundEffectPacket.volume = volume; soundEffectPacket.pitch = pitch; playerConnection.sendPacket(soundEffectPacket); } /** * Plays a sound from the {@link SoundEvent} enum. * * @see #playSound(SoundEvent, SoundCategory, int, int, int, float, float) * @deprecated Use {@link #playSound(net.kyori.adventure.sound.Sound, double, double, double)} */ @Deprecated public void playSound(@NotNull SoundEvent sound, @NotNull SoundCategory soundCategory, BlockPosition position, float volume, float pitch) { playSound(sound, soundCategory, position.getX(), position.getY(), position.getZ(), volume, pitch); } /** * Plays a sound from an identifier (represents a custom sound in a resource pack). * * @param identifier the identifier of the sound to play * @param soundCategory the sound category * @param x the effect X * @param y the effect Y * @param z the effect Z * @param volume the volume of the sound (1 is 100%) * @param pitch the pitch of the sound, between 0.5 and 2.0 * @deprecated Use {@link #playSound(net.kyori.adventure.sound.Sound, double, double, double)} */ @Deprecated public void playSound(@NotNull String identifier, @NotNull SoundCategory soundCategory, int x, int y, int z, float volume, float pitch) { NamedSoundEffectPacket namedSoundEffectPacket = new NamedSoundEffectPacket(); namedSoundEffectPacket.soundName = identifier; namedSoundEffectPacket.soundSource = soundCategory.asSource(); namedSoundEffectPacket.x = x; namedSoundEffectPacket.y = y; namedSoundEffectPacket.z = z; namedSoundEffectPacket.volume = volume; namedSoundEffectPacket.pitch = pitch; playerConnection.sendPacket(namedSoundEffectPacket); } /** * Plays a sound from an identifier (represents a custom sound in a resource pack). * * @see #playSound(String, SoundCategory, int, int, int, float, float) * @deprecated Use {@link #playSound(net.kyori.adventure.sound.Sound, double, double, double)} */ @Deprecated public void playSound(@NotNull String identifier, @NotNull SoundCategory soundCategory, BlockPosition position, float volume, float pitch) { playSound(identifier, soundCategory, position.getX(), position.getY(), position.getZ(), volume, pitch); } /** * Plays a sound directly to the player (constant volume). * * @param sound the sound to play * @param soundCategory the sound category * @param volume the volume of the sound (1 is 100%) * @param pitch the pitch of the sound, between 0.5 and 2.0 * @deprecated Use {@link #playSound(net.kyori.adventure.sound.Sound)} */ @Deprecated public void playSound(@NotNull SoundEvent sound, @NotNull SoundCategory soundCategory, float volume, float pitch) { EntitySoundEffectPacket entitySoundEffectPacket = new EntitySoundEffectPacket(); entitySoundEffectPacket.entityId = getEntityId(); entitySoundEffectPacket.soundId = sound.getId(); entitySoundEffectPacket.soundSource = soundCategory.asSource(); entitySoundEffectPacket.volume = volume; entitySoundEffectPacket.pitch = pitch; playerConnection.sendPacket(entitySoundEffectPacket); } @Override public void playSound(@NotNull Sound sound) { playerConnection.sendPacket(AdventurePacketConvertor.createEntitySoundPacket(sound, this)); } @Override public void playSound(@NotNull Sound sound, double x, double y, double z) { playerConnection.sendPacket(AdventurePacketConvertor.createSoundPacket(sound, x, y, z)); } @Override public void stopSound(@NotNull SoundStop stop) { playerConnection.sendPacket(AdventurePacketConvertor.createSoundStopPacket(stop)); } /** * Plays a given effect at the given position for this player. * * @param effect the effect to play * @param x x position of the effect * @param y y position of the effect * @param z z position of the effect * @param data data for the effect * @param disableRelativeVolume disable volume scaling based on distance */ public void playEffect(@NotNull Effects effect, int x, int y, int z, int data, boolean disableRelativeVolume) { EffectPacket packet = new EffectPacket(); packet.effectId = effect.getId(); packet.position = new BlockPosition(x, y, z); packet.data = data; packet.disableRelativeVolume = disableRelativeVolume; playerConnection.sendPacket(packet); } /** * Sends a {@link StopSoundPacket} packet. * * @deprecated Use {@link #stopSound(SoundStop)} with {@link SoundStop#all()} */ @Deprecated public void stopSound() { StopSoundPacket stopSoundPacket = new StopSoundPacket(); stopSoundPacket.flags = 0x00; playerConnection.sendPacket(stopSoundPacket); } /** * Sets the header and footer of a player which will be displayed in his tab window. * * @param header the header text, null to set empty * @param footer the footer text, null to set empty * @deprecated Use {@link #sendPlayerListHeaderAndFooter(Component, Component)} */ @Deprecated public void sendHeaderFooter(@Nullable JsonMessage header, @Nullable JsonMessage footer) { this.sendPlayerListHeaderAndFooter(header == null ? Component.empty() : header.asComponent(), footer == null ? Component.empty() : footer.asComponent()); } @Override public void sendPlayerListHeaderAndFooter(@NotNull Component header, @NotNull Component footer) { PlayerListHeaderAndFooterPacket packet = new PlayerListHeaderAndFooterPacket(header, footer); playerConnection.sendPacket(packet); } /** * Common method to send a title. * * @param text the text of the title * @param action the action of the title (where to show it) * @see #sendTitleTime(int, int, int) to specify the display time * @deprecated Use {@link #showTitle(Title)} and {@link #sendActionBar(Component)} */ @Deprecated private void sendTitle(@NotNull JsonMessage text, @NotNull TitlePacket.Action action) { TitlePacket titlePacket = new TitlePacket(action, text.asComponent()); playerConnection.sendPacket(titlePacket); } /** * Sends a title and subtitle message. * * @param title the title message * @param subtitle the subtitle message * @see #sendTitleTime(int, int, int) to specify the display time * @deprecated Use {@link #showTitle(Title)} */ @Deprecated public void sendTitleSubtitleMessage(@NotNull JsonMessage title, @NotNull JsonMessage subtitle) { this.showTitle(Title.title(title.asComponent(), subtitle.asComponent())); } /** * Sends a title message. * * @param title the title message * @see #sendTitleTime(int, int, int) to specify the display time * @deprecated Use {@link #showTitle(Title)} */ @Deprecated public void sendTitleMessage(@NotNull JsonMessage title) { this.showTitle(Title.title(title.asComponent(), Component.empty())); } /** * Sends a subtitle message. * * @param subtitle the subtitle message * @see #sendTitleTime(int, int, int) to specify the display time * @deprecated Use {@link #showTitle(Title)} */ @Deprecated public void sendSubtitleMessage(@NotNull JsonMessage subtitle) { this.showTitle(Title.title(Component.empty(), subtitle.asComponent())); } /** * Sends an action bar message. * * @param actionBar the action bar message * @see #sendTitleTime(int, int, int) to specify the display time * @deprecated Use {@link #sendActionBar(Component)} */ @Deprecated public void sendActionBarMessage(@NotNull JsonMessage actionBar) { this.sendActionBar(actionBar.asComponent()); } @Override public void showTitle(@NotNull Title title) { Collection packet = TitlePacket.of(Title.title(title.title(), title.subtitle(), title.times())); for (TitlePacket titlePacket : packet) { playerConnection.sendPacket(titlePacket); } } @Override public void sendActionBar(@NotNull Component message) { TitlePacket titlePacket = new TitlePacket(TitlePacket.Action.SET_ACTION_BAR, message); playerConnection.sendPacket(titlePacket); } /** * Specifies the display time of a title. * * @param fadeIn ticks to spend fading in * @param stay ticks to keep the title displayed * @param fadeOut ticks to spend out, not when to start fading out * @deprecated Use {@link #showTitle(Title)}. Note that this will overwrite the * existing title. This is expected behavior and will be the case in 1.17. */ @Deprecated public void sendTitleTime(int fadeIn, int stay, int fadeOut) { TitlePacket titlePacket = new TitlePacket(fadeIn, stay, fadeOut); playerConnection.sendPacket(titlePacket); } /** * Hides the previous title. * * @deprecated Use {@link #clearTitle()} */ @Deprecated public void hideTitle() { TitlePacket titlePacket = new TitlePacket(TitlePacket.Action.HIDE); playerConnection.sendPacket(titlePacket); } @Override public void resetTitle() { TitlePacket titlePacket = new TitlePacket(TitlePacket.Action.RESET); playerConnection.sendPacket(titlePacket); } @Override public void clearTitle() { TitlePacket titlePacket = new TitlePacket(TitlePacket.Action.HIDE); playerConnection.sendPacket(titlePacket); } @Override public void showBossBar(@NotNull BossBar bar) { MinecraftServer.getBossBarManager().addBossBar(this, bar); } @Override public void hideBossBar(@NotNull BossBar bar) { MinecraftServer.getBossBarManager().removeBossBar(this, bar); } /** * Opens a book ui for the player with the given book metadata. * * @param bookMeta The metadata of the book to open * @deprecated Use {@link #openBook(Book)} */ @Deprecated public void openBook(@NotNull WrittenBookMeta bookMeta) { // Set book in offhand final ItemStack writtenBook = new ItemStack(Material.WRITTEN_BOOK, (byte) 1); writtenBook.setItemMeta(bookMeta); final SetSlotPacket setSlotPacket = new SetSlotPacket(); setSlotPacket.windowId = 0; setSlotPacket.slot = 45; setSlotPacket.itemStack = writtenBook; this.playerConnection.sendPacket(setSlotPacket); // Open the book final OpenBookPacket openBookPacket = new OpenBookPacket(); openBookPacket.hand = Hand.OFF; this.playerConnection.sendPacket(openBookPacket); // Update inventory to remove book (which the actual inventory does not have) this.inventory.update(); } @Override public void openBook(@NotNull Book book) { // make the book ItemStack writtenBook = new ItemStack(Material.WRITTEN_BOOK, (byte) 1); writtenBook.setItemMeta(WrittenBookMeta.fromAdventure(book, this)); // Set book in offhand SetSlotPacket setBookPacket = new SetSlotPacket(); setBookPacket.windowId = 0; setBookPacket.slot = 45; setBookPacket.itemStack = writtenBook; playerConnection.sendPacket(setBookPacket); // Open the book OpenBookPacket openBookPacket = new OpenBookPacket(); openBookPacket.hand = Hand.OFF; playerConnection.sendPacket(openBookPacket); // Restore the item in offhand SetSlotPacket restoreItemPacket = new SetSlotPacket(); restoreItemPacket.windowId = 0; restoreItemPacket.slot = 45; restoreItemPacket.itemStack = getItemInOffHand(); playerConnection.sendPacket(restoreItemPacket); } @Override public boolean isImmune(@NotNull DamageType type) { if (!getGameMode().canTakeDamage()) { return type != DamageType.VOID; } return super.isImmune(type); } @Override public void setHealth(float health) { super.setHealth(health); sendUpdateHealthPacket(); } /** * Gets the player additional hearts. * * @return the player additional hearts */ public float getAdditionalHearts() { return metadata.getIndex((byte) 14, 0f); } /** * Changes the amount of additional hearts shown. * * @param additionalHearts the count of additional hearts */ public void setAdditionalHearts(float additionalHearts) { this.metadata.setIndex((byte) 14, Metadata.Float(additionalHearts)); } /** * Gets the player food. * * @return the player food */ public int getFood() { return food; } /** * Sets and refresh client food bar. * * @param food the new food value * @throws IllegalArgumentException if {@code food} is not between 0 and 20 */ public void setFood(int food) { Check.argCondition(!MathUtils.isBetween(food, 0, 20), "Food has to be between 0 and 20"); this.food = food; sendUpdateHealthPacket(); } public float getFoodSaturation() { return foodSaturation; } /** * Sets and refresh client food saturation. * * @param foodSaturation the food saturation * @throws IllegalArgumentException if {@code foodSaturation} is not between 0 and 20 */ public void setFoodSaturation(float foodSaturation) { Check.argCondition(!MathUtils.isBetween(foodSaturation, 0, 20), "Food saturation has to be between 0 and 20"); this.foodSaturation = foodSaturation; sendUpdateHealthPacket(); } /** * Gets if the player is eating. * * @return true if the player is eating, false otherwise */ public boolean isEating() { return isEating; } /** * Gets the player default eating time. * * @return the player default eating time */ public long getDefaultEatingTime() { return defaultEatingTime; } /** * Used to change the default eating time animation. * * @param defaultEatingTime the default eating time in milliseconds */ public void setDefaultEatingTime(long defaultEatingTime) { this.defaultEatingTime = defaultEatingTime; } /** * Gets the player display name in the tab-list. * * @return the player display name, null means that {@link #getUsername()} is displayed * @deprecated Use {@link #getDisplayName()} */ @Nullable @Deprecated public JsonMessage getDisplayNameJson() { return JsonMessage.fromComponent(displayName); } /** * Gets the player display name in the tab-list. * * @return the player display name, null means that {@link #getUsername()} is displayed */ @Nullable public Component getDisplayName() { return displayName; } /** * Changes the player display name in the tab-list. *

* Sets to null to show the player username. * * @param displayName the display name, null to display the username * @deprecated Use {@link #setDisplayName(Component)} */ @Deprecated public void setDisplayName(@Nullable JsonMessage displayName) { this.setDisplayName(displayName == null ? null : displayName.asComponent()); } /** * Changes the player display name in the tab-list. *

* Sets to null to show the player username. * * @param displayName the display name, null to display the username */ public void setDisplayName(@Nullable Component displayName) { this.displayName = displayName; 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 AsyncPlayerPreLoginEvent} * mostly unsafe outside of it. * * @param username the new player name */ public void setUsernameField(@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) { 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(); sendPacketsToViewers(getEntityType().getSpawnType().getSpawnPacket(this)); // Update for viewers sendPacketToViewersAndSelf(getVelocityPacket()); sendPacketToViewersAndSelf(getMetadataPacket()); sendPacketToViewersAndSelf(getPropertiesPacket()); sendPacketToViewersAndSelf(getEquipmentsPacket()); { // 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); // TODO prevent the client from getting lag spikes when re-loading large chunks // Probably by having a distinction between visible and loaded (cache) chunks /*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); this.instance.loadOptionalChunk(chunkX, chunkZ, chunk -> { if (chunk == null) { // Cannot load chunk (auto load is not enabled) return; } chunk.addViewer(this); }); } } public void refreshVisibleChunks() { final Chunk chunk = getChunk(); if (chunk != null) { refreshVisibleChunks(chunk); } } /** * Refreshes the list of entities that the player should be able to see based * on {@link MinecraftServer#getEntityViewDistance()} and {@link Entity#isAutoViewable()}. * * @param newChunk the new chunk of the player (can be the current one) */ public void refreshVisibleEntities(@NotNull Chunk newChunk) { final int entityViewDistance = MinecraftServer.getEntityViewDistance(); final float maximalDistance = entityViewDistance * Chunk.CHUNK_SECTION_SIZE; // Manage already viewable entities this.viewableEntities.forEach(entity -> { final double 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); } } }); // Manage entities in unchecked chunks EntityUtils.forEachRange(instance, newChunk.toPosition(), entityViewDistance, entity -> { if (entity.isAutoViewable() && !entity.viewers.contains(this)) { entity.addViewer(this); } if (entity instanceof Player && isAutoViewable() && !viewers.contains(entity)) { addViewer((Player) entity); } }); } @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) { this.gameMode = gameMode; // Condition to prevent sending the packets before spawning the player if (isActive()) { 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.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 * @deprecated Use {@link #kick(Component)} */ @Deprecated public void kick(@NotNull JsonMessage text) { this.kick(text.asComponent()); } /** * Kicks the player with a reason. * * @param message the kick reason * @deprecated Use {@link #kick(Component)} */ @Deprecated public void kick(@NotNull String message) { this.kick(Component.text(message)); } /** * Kicks the player with a reason. * * @param component the reason */ public void kick(@NotNull Component component) { final ConnectionState connectionState = playerConnection.getConnectionState(); // Packet type depends on the current player connection state final ServerPacket disconnectPacket; if (connectionState == ConnectionState.LOGIN) { disconnectPacket = new LoginDisconnectPacket(component); } else { disconnectPacket = new DisconnectPacket(component); } if (playerConnection instanceof NettyPlayerConnection) { ((NettyPlayerConnection) playerConnection).writeAndFlush(disconnectPacket); playerConnection.disconnect(); } else { playerConnection.sendPacket(disconnectPacket); playerConnection.refreshOnline(false); } } /** * 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) { var players = MinecraftServer.getConnectionManager().getOnlinePlayers(); PacketUtils.sendGroupedPacket(players, 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; } /** * 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) { InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this); callCancellableEvent(InventoryOpenEvent.class, inventoryOpenEvent, () -> { if (getOpenInventory() != null) { getOpenInventory().removeViewer(this); } 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 getViewableChunks() { return viewableChunks; } /** * Sends a {@link UpdateViewPositionPacket} to the player. * * @param chunkX the chunk X * @param chunkZ the chunk Z */ public void updateViewPosition(int chunkX, int chunkZ) { UpdateViewPositionPacket updateViewPositionPacket = new UpdateViewPositionPacket(); updateViewPositionPacket.chunkX = chunkX; updateViewPositionPacket.chunkZ = chunkZ; playerConnection.sendPacket(updateViewPositionPacket); } public int getLastSentTeleportId() { return teleportId.get(); } public int getLastReceivedTeleportId() { return receivedTeleportId; } public void refreshReceivedTeleportId(int receivedTeleportId) { this.receivedTeleportId = receivedTeleportId; } /** * Used for synchronization purpose, mainly for teleportation */ protected void updatePlayerPosition() { PlayerPositionAndLookPacket positionAndLookPacket = new PlayerPositionAndLookPacket(); positionAndLookPacket.position = position.clone(); // clone needed to prevent synchronization issue positionAndLookPacket.flags = 0x00; positionAndLookPacket.teleportId = teleportId.incrementAndGet(); playerConnection.sendPacket(positionAndLookPacket); } /** * Gets the player permission level. * * @return the player permission level */ public int getPermissionLevel() { return permissionLevel; } /** * Changes the player permission level. * * @param permissionLevel the new player permission level * @throws IllegalArgumentException if {@code permissionLevel} is not between 0 and 4 */ public void setPermissionLevel(int permissionLevel) { Check.argCondition(!MathUtils.isBetween(permissionLevel, 0, 4), "permissionLevel has to be between 0 and 4"); this.permissionLevel = permissionLevel; // Magic values: https://wiki.vg/Entity_statuses#Player // TODO remove magic values final byte permissionLevelStatus = (byte) (24 + permissionLevel); triggerStatus(permissionLevelStatus); } /** * Sets or remove the reduced debug screen. * * @param reduced should the player has the reduced debug screen */ public void setReducedDebugScreenInformation(boolean reduced) { this.reducedDebugScreenInformation = reduced; // Magic values: https://wiki.vg/Entity_statuses#Player // TODO remove magic values final byte debugScreenStatus = (byte) (reduced ? 22 : 23); triggerStatus(debugScreenStatus); } /** * Gets if the player has the reduced debug screen. * * @return true if the player has the reduced debug screen, false otherwise */ public boolean hasReducedDebugScreenInformation() { return reducedDebugScreenInformation; } /** * The invulnerable field appear in the {@link PlayerAbilitiesPacket} packet. * * @return true if the player is invulnerable, false otherwise */ public boolean isInvulnerable() { return super.isInvulnerable(); } /** * This do update the {@code invulnerable} field in the packet {@link PlayerAbilitiesPacket} * and prevent the player from receiving damage. * * @param invulnerable should the player be invulnerable */ public void setInvulnerable(boolean invulnerable) { super.setInvulnerable(invulnerable); refreshAbilities(); } /** * Gets if the player is currently flying. * * @return true if the player if flying, false otherwise */ public boolean isFlying() { return flying; } /** * Sets the player flying. * * @param flying should the player fly */ public void setFlying(boolean flying) { this.flying = flying; refreshAbilities(); } /** * Updates the internal flying field. *

* Mostly unsafe since there is nothing to backup the value, used internally for creative players. * * @param flying the new flying field * @see #setFlying(boolean) instead */ public void refreshFlying(boolean flying) { 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 getFieldViewModifier() { return fieldViewModifier; } public void setFieldViewModifier(float fieldViewModifier) { this.fieldViewModifier = fieldViewModifier; refreshAbilities(); } /** * This is the map used to send the statistic packet. * It is possible to add/remove/change statistic value directly into it. * * @return the modifiable statistic map */ @NotNull public Map getStatisticValueMap() { return statisticValueMap; } /** * Gets the player vehicle information. * * @return the player vehicle information */ @NotNull public PlayerVehicleInformation getVehicleInformation() { return vehicleInformation; } /** * Sends to the player a {@link PlayerAbilitiesPacket} with all the updated fields. */ protected void refreshAbilities() { PlayerAbilitiesPacket playerAbilitiesPacket = new PlayerAbilitiesPacket(); playerAbilitiesPacket.invulnerable = invulnerable; playerAbilitiesPacket.flying = flying; playerAbilitiesPacket.allowFlying = allowFlying; playerAbilitiesPacket.instantBreak = instantBreak; playerAbilitiesPacket.flyingSpeed = flyingSpeed; playerAbilitiesPacket.fieldViewModifier = fieldViewModifier; playerConnection.sendPacket(playerAbilitiesPacket); } /** * All packets in the queue are executed in the {@link #update(long)} method * It is used internally to add all received packet from the client. * Could be used to "simulate" a received packet, but to use at your own risk. * * @param packet the packet to add in the queue */ public void addPacketToQueue(@NotNull ClientPlayPacket packet) { this.packets.add(packet); } /** * Changes the storage player latency and update its tab value. * * @param latency the new player latency */ public void refreshLatency(int latency) { this.latency = latency; PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_LATENCY); playerInfoPacket.playerInfos.add(new PlayerInfoPacket.UpdateLatency(getUuid(), latency)); sendPacketToViewersAndSelf(playerInfoPacket); } public void refreshOnGround(boolean onGround) { this.onGround = onGround; } /** * Used to change internally the last sent last keep alive id. *

* Warning: could lead to have the player kicked because of a wrong keep alive packet. * * @param lastKeepAlive the new lastKeepAlive id */ public void refreshKeepAlive(long lastKeepAlive) { this.lastKeepAlive = lastKeepAlive; this.answerKeepAlive = false; } public boolean didAnswerKeepAlive() { return answerKeepAlive; } public void refreshAnswerKeepAlive(boolean answerKeepAlive) { this.answerKeepAlive = answerKeepAlive; } /** * Changes the held item for the player viewers * Also cancel 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 breakers) { this.targetCustomBlock = targetCustomBlock; this.targetBlockPosition = targetBlockPosition; refreshBreakDelay(breakers); } /** * Refreshes the break delay for the next block break stage. * * @param breakers the list of breakers, can be null if {@code this} is the only breaker */ private void refreshBreakDelay(@Nullable Set breakers) { breakers = breakers == null ? targetBreakers : breakers; // Refresh the last tick update this.targetBlockBreakCount = 0; // Get if multi player breaking is enabled final boolean multiPlayerBreaking = targetCustomBlock.enableMultiPlayerBreaking(); // Get the stage from the custom block object if it is, otherwise use the local field final byte stage = multiPlayerBreaking ? targetCustomBlock.getBreakStage(instance, targetBlockPosition) : targetStage; // Retrieve the break delay for the current stage this.targetBreakDelay = targetCustomBlock.getBreakDelay(this, targetBlockPosition, stage, breakers); } /** * Resets data from the current block the player is mining. * If the currently mined block (or if there isn't any) is not a {@link CustomBlock}, nothing happen. */ public void resetTargetBlock() { // Remove effect PlayerDiggingListener.removeEffect(this); if (targetCustomBlock != null) { targetCustomBlock.stopDigging(instance, targetBlockPosition, this); this.targetCustomBlock = null; this.targetBlockPosition = null; this.targetBreakDelay = 0; this.targetBlockBreakCount = 0; this.targetStage = 0; } } public void refreshVehicleSteer(float sideways, float forward, boolean jump, boolean unmount) { this.vehicleInformation.refresh(sideways, forward, jump, unmount); } /** * @return the chunk range of the viewers, * which is {@link MinecraftServer#getChunkViewDistance()} or {@link PlayerSettings#getViewDistance()} * based on which one is the lowest */ public int getChunkRange() { final int playerRange = getSettings().viewDistance; if (playerRange < 1) { // Didn't receive settings packet yet (is the case on login) // In this case we send an arbitrary number of chunks // Will be updated in PlayerSettings#refresh. // Non-compliant clients might also be stuck with this view return 7; } else { final int serverRange = MinecraftServer.getChunkViewDistance(); return Math.min(playerRange, serverRange); } } /** * Gets the last sent keep alive id. * * @return the last keep alive id sent to the player */ public long getLastKeepAlive() { return lastKeepAlive; } @Override public @NotNull HoverEvent asHoverEvent(@NotNull UnaryOperator op) { return HoverEvent.showEntity(ShowEntity.of(EntityType.PLAYER, this.uuid, this.displayName)); } /** * Gets the packet to add the player from the tab-list. * * @return a {@link PlayerInfoPacket} to add the player */ @NotNull protected PlayerInfoPacket getAddPlayerToList() { PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.ADD_PLAYER); PlayerInfoPacket.AddPlayer addPlayer = new PlayerInfoPacket.AddPlayer(getUuid(), getUsername(), getGameMode(), getLatency()); addPlayer.displayName = displayName; // Skin support if (skin != null) { final String textures = skin.getTextures(); final String signature = skin.getSignature(); PlayerInfoPacket.AddPlayer.Property prop = new PlayerInfoPacket.AddPlayer.Property("textures", textures, signature); addPlayer.properties.add(prop); } playerInfoPacket.playerInfos.add(addPlayer); return playerInfoPacket; } /** * Gets the packet to remove the player from the tab-list. * * @return a {@link PlayerInfoPacket} to remove the player */ @NotNull protected PlayerInfoPacket getRemovePlayerToList() { PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER); PlayerInfoPacket.RemovePlayer removePlayer = new PlayerInfoPacket.RemovePlayer(getUuid()); playerInfoPacket.playerInfos.add(removePlayer); return playerInfoPacket; } /** * Sends all the related packet to have the player sent to another with related data * (create player, spawn position, velocity, metadata, equipments, passengers, team). *

* WARNING: this alone does not sync the player, please use {@link #addViewer(Player)}. * * @param connection the connection to show the player to */ protected void showPlayer(@NotNull PlayerConnection connection) { connection.sendPacket(getAddPlayerToList()); connection.sendPacket(getEntityType().getSpawnType().getSpawnPacket(this)); connection.sendPacket(getVelocityPacket()); connection.sendPacket(getMetadataPacket()); connection.sendPacket(getEquipmentsPacket()); 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); } @Override public Locale getLocale() { return settings.locale == null ? null : Locale.forLanguageTag(settings.locale); } /** * Sets the player's locale. This will only set the locale of the player as it * is stored in the server. This will also be reset if the settings are refreshed. * * @param locale the new locale */ @Override public void setLocale(@Nullable Locale locale) { settings.locale = locale == null ? null : locale.toLanguageTag(); } @Override public @NotNull Identity identity() { return this.identity; } @Override public void setUuid(@NotNull UUID uuid) { super.setUuid(uuid); // update identity this.identity = Identity.identity(uuid); } /** * 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 = this.viewDistance != viewDistance; this.locale = locale; this.viewDistance = viewDistance; this.chatMode = chatMode; this.chatColors = chatColors; this.displayedSkinParts = displayedSkinParts; this.mainHand = mainHand; metadata.setIndex((byte) 16, Metadata.Byte(displayedSkinParts)); this.firstRefresh = false; // Client changed his view distance in the settings if (viewDistanceChanged) { refreshVisibleChunks(); } } } }