package net.minestom.server.entity; import net.minestom.server.MinecraftServer; import net.minestom.server.attribute.Attribute; import net.minestom.server.bossbar.BossBar; import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.RichMessage; 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.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.network.packet.PacketWriter; import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.login.JoinGamePacket; import net.minestom.server.network.packet.server.play.*; 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.BelowNameScoreboard; import net.minestom.server.scoreboard.Team; import net.minestom.server.sound.Sound; import net.minestom.server.sound.SoundCategory; 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.Position; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.Dimension; import net.minestom.server.world.LevelType; import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; public class Player extends LivingEntity implements CommandSender { private long lastKeepAlive; private boolean answerKeepAlive; private String username; protected PlayerConnection playerConnection; private ConcurrentLinkedQueue packets = new ConcurrentLinkedQueue<>(); private int latency; private ColoredText displayName; private PlayerSkin skin; private Dimension dimension; private GameMode gameMode; private LevelType levelType; private int teleportId = 0; protected boolean onGround; protected Set viewableEntities = new CopyOnWriteArraySet<>(); protected Set viewableChunks = new CopyOnWriteArraySet<>(); private PlayerSettings settings; private float exp; private int level; private 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 float additionalHearts; 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; private long targetBlockTime; private byte targetLastStage; private int blockBreakTime; private Set bossBars = new CopyOnWriteArraySet<>(); private Team team; private BelowNameScoreboard belowNameScoreboard; /** * Last damage source to hit this player, used to display the death message. */ private DamageType lastDamageSource; private int permissionLevel; private boolean reducedDebugScreenInformation; // Abilities private boolean invulnerable; private boolean flying; private boolean allowFlying; private boolean instantBreak; private float flyingSpeed = 0.05f; private float fieldViewModifier = 0.1f; // Statistics private Map statisticValueMap = new Hashtable<>(); // Vehicle private PlayerVehicleInformation vehicleInformation = new PlayerVehicleInformation(); // Tick related private final PlayerTickEvent playerTickEvent = new PlayerTickEvent(this); public Player(UUID uuid, String username, PlayerConnection playerConnection) { super(EntityType.PLAYER); this.uuid = uuid; // Override Entity#uuid defined in the constructor this.username = username; this.playerConnection = playerConnection; setBoundingBox(0.69f, 1.8f, 0.69f); 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.dimension = Dimension.OVERWORLD; this.levelType = LevelType.DEFAULT; refreshPosition(0, 0, 0); // FakePlayer init its connection there playerConnectionInit(); MinecraftServer.getEntityManager().addWaitingPlayer(this); } /** * Used when the player is created (EntityManager#waitingPlayersTick()) * Init the player and spawn him */ protected void init() { // TODO complete login sequence with optionals packets JoinGamePacket joinGamePacket = new JoinGamePacket(); joinGamePacket.entityId = getEntityId(); joinGamePacket.gameMode = gameMode; joinGamePacket.dimension = dimension; joinGamePacket.maxPlayers = 0; // Unused joinGamePacket.levelType = levelType; joinGamePacket.viewDistance = MinecraftServer.CHUNK_VIEW_DISTANCE; joinGamePacket.reducedDebugInfo = false; playerConnection.sendPacket(joinGamePacket); // TODO minecraft:brand plugin message ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket(); serverDifficultyPacket.difficulty = MinecraftServer.getDifficulty(); serverDifficultyPacket.locked = true; playerConnection.sendPacket(serverDifficultyPacket); SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket(); spawnPositionPacket.x = 0; spawnPositionPacket.y = 0; spawnPositionPacket.z = 0; playerConnection.sendPacket(spawnPositionPacket); // Add player to list with spawning skin PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this); 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 recipesIdentifier = new ArrayList<>(); for (Recipe recipe : recipeManager.getRecipes()) { if (!recipe.shouldShow(this)) continue; recipesIdentifier.add(recipe.getRecipeId()); } if (!recipesIdentifier.isEmpty()) { 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 // Some client update 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 boolean damage(DamageType type, float value) { if (isInvulnerable()) return false; // Compute final heart based on health and additional hearts boolean result = super.damage(type, value); if (result) { lastDamageSource = type; } return result; } @Override public void update(long time) { // Flush all pending packets playerConnection.flush(); // 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) { final byte animationCount = 10; long since = time - targetBlockTime; byte stage = (byte) (since / (blockBreakTime / animationCount)); stage = MathUtils.setBetween(stage, (byte) -1, animationCount); if (stage != targetLastStage) { sendBlockBreakAnimation(targetBlockPosition, stage); } this.targetLastStage = stage; if (stage > 9) { instance.breakBlock(this, targetBlockPosition); resetTargetBlock(); } } // Experience orb pickup Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks Set entities = instance.getChunkEntities(chunk); for (Entity entity : entities) { if (entity instanceof ExperienceOrb) { ExperienceOrb experienceOrb = (ExperienceOrb) entity; BoundingBox itemBoundingBox = experienceOrb.getBoundingBox(); if (expandedBoundingBox.intersect(itemBoundingBox)) { synchronized (experienceOrb) { 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); // Refresh hand boolean isOffHand = itemUpdateStateEvent.getHand() == Player.Hand.OFF; refreshActiveHand(false, isOffHand, false); ItemStack foodItem = itemUpdateStateEvent.getItemStack(); 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 positionChanged = position.getX() != lastX || position.getY() != lastY || position.getZ() != lastZ; final boolean viewChanged = position.getYaw() != lastYaw || position.getPitch() != lastPitch; if (!getViewers().isEmpty() && (positionChanged || viewChanged)) { ServerPacket updatePacket = null; ServerPacket optionalUpdatePacket = null; if (positionChanged && viewChanged) { EntityPositionAndRotationPacket entityPositionAndRotationPacket = new EntityPositionAndRotationPacket(); entityPositionAndRotationPacket.entityId = getEntityId(); entityPositionAndRotationPacket.deltaX = (short) ((position.getX() * 32 - lastX * 32) * 128); entityPositionAndRotationPacket.deltaY = (short) ((position.getY() * 32 - lastY * 32) * 128); entityPositionAndRotationPacket.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128); entityPositionAndRotationPacket.yaw = position.getYaw(); entityPositionAndRotationPacket.pitch = position.getPitch(); entityPositionAndRotationPacket.onGround = onGround; lastX = position.getX(); lastY = position.getY(); lastZ = position.getZ(); lastYaw = position.getYaw(); lastPitch = position.getPitch(); updatePacket = entityPositionAndRotationPacket; } else if (positionChanged) { EntityPositionPacket entityPositionPacket = new EntityPositionPacket(); entityPositionPacket.entityId = getEntityId(); entityPositionPacket.deltaX = (short) ((position.getX() * 32 - lastX * 32) * 128); entityPositionPacket.deltaY = (short) ((position.getY() * 32 - lastY * 32) * 128); entityPositionPacket.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128); entityPositionPacket.onGround = onGround; lastX = position.getX(); lastY = position.getY(); lastZ = position.getZ(); updatePacket = entityPositionPacket; } else if (viewChanged) { EntityRotationPacket entityRotationPacket = new EntityRotationPacket(); entityRotationPacket.entityId = getEntityId(); entityRotationPacket.yaw = position.getYaw(); entityRotationPacket.pitch = position.getPitch(); entityRotationPacket.onGround = onGround; lastYaw = position.getYaw(); lastPitch = position.getPitch(); updatePacket = entityRotationPacket; } if (viewChanged) { EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket(); entityHeadLookPacket.entityId = getEntityId(); entityHeadLookPacket.yaw = position.getYaw(); optionalUpdatePacket = entityHeadLookPacket; } if (updatePacket != null) { if (optionalUpdatePacket != null) { sendPacketsToViewers(updatePacket, optionalUpdatePacket); } else { sendPacketToViewers(updatePacket); } } } } @Override public void kill() { if (!isDead()) { // send death message to player ColoredText deathMessage; if (lastDamageSource != null) { deathMessage = lastDamageSource.buildDeathScreenMessage(this); } else { // may happen if killed by the server without applying damage deathMessage = ColoredText.of("Killed by poor programming."); } CombatEventPacket deathPacket = CombatEventPacket.death(this, Optional.empty(), deathMessage); playerConnection.sendPacket(deathPacket); // send death message to chat RichMessage chatMessage; if (lastDamageSource != null) { chatMessage = lastDamageSource.buildChatMessage(this); } else { // may happen if killed by the server without applying damage ColoredText coloredChatMessage = ColoredText.of(getUsername() + " was killed by poor programming."); chatMessage = RichMessage.of(coloredChatMessage); } MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(player -> { player.sendMessage(chatMessage); }); } super.kill(); } /** * Respawn the player by sending a {@link RespawnPacket} to the player and teleporting him * to {@link #getRespawnPoint()}. It also reset fire and his health */ public void respawn() { if (!isDead()) return; setFireForDuration(0); setOnFire(false); refreshHealth(); RespawnPacket respawnPacket = new RespawnPacket(); respawnPacket.dimension = getDimension(); respawnPacket.gameMode = getGameMode(); respawnPacket.levelType = getLevelType(); getPlayerConnection().sendPacket(respawnPacket); PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this, getRespawnPoint()); 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() { super.remove(); this.packets.clear(); clearBossBars(); if (getOpenInventory() != null) getOpenInventory().removeViewer(this); this.viewableEntities.forEach(entity -> entity.removeViewer(this)); this.viewableChunks.forEach(chunk -> { if (chunk.isLoaded()) chunk.removeViewer(this); }); resetTargetBlock(); callEvent(PlayerDisconnectEvent.class, new PlayerDisconnectEvent(this)); playerConnection.disconnect(); } @Override public boolean addViewer(Player player) { if (player == this) return false; boolean result = super.addViewer(player); if (!result) return false; PlayerConnection viewerConnection = player.getPlayerConnection(); showPlayer(viewerConnection); return result; } @Override public boolean removeViewer(Player player) { if (player == this) return false; boolean result = super.removeViewer(player); PlayerConnection viewerConnection = player.getPlayerConnection(); viewerConnection.sendPacket(getRemovePlayerToList()); // Team if (team != null && team.getPlayers().size() == 1) // If team only contains "this" player viewerConnection.sendPacket(team.createTeamDestructionPacket()); return result; } @Override public void setInstance(Instance instance) { Check.notNull(instance, "instance cannot be null!"); Check.argCondition(this.instance == instance, "Instance should be different than the current one"); boolean firstSpawn = this.instance == null; // TODO: Handle player reconnections, must be false in that case too for (Chunk viewableChunk : viewableChunks) { viewableChunk.removeViewer(this); } viewableChunks.clear(); if (this.instance != null) { Dimension instanceDimension = instance.getDimension(); if (dimension != instanceDimension) sendDimension(instanceDimension); } long[] visibleChunks = ChunkUtils.getChunksInRange(position, getChunkRange()); int length = visibleChunks.length; AtomicInteger counter = new AtomicInteger(0); for (int i = 0; i < length; i++) { int[] chunkPos = ChunkUtils.getChunkCoord(visibleChunks[i]); int chunkX = chunkPos[0]; int chunkZ = chunkPos[1]; Consumer callback = (chunk) -> { if (chunk != null) { viewableChunks.add(chunk); chunk.addViewer(this); instance.sendChunk(this, chunk); updateViewPosition(chunk); } boolean isLast = counter.get() == length - 1; if (isLast) { // This is the last chunk to be loaded , spawn player super.setInstance(instance); PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(instance, firstSpawn); callEvent(PlayerSpawnEvent.class, spawnEvent); } else { // Increment the counter of current loaded chunks counter.incrementAndGet(); } }; // WARNING: if auto load is disabled and no chunks are loaded beforehand, player will be stuck. instance.loadOptionalChunk(chunkX, chunkZ, callback); } } @Override public Consumer getMetadataConsumer() { return packet -> { super.getMetadataConsumer().accept(packet); fillMetadataIndex(packet, 14); }; } @Override protected void fillMetadataIndex(PacketWriter packet, int index) { super.fillMetadataIndex(packet, index); if (index == 14) { packet.writeByte((byte) 14); packet.writeByte(METADATA_FLOAT); packet.writeFloat(additionalHearts); } } /** * Send a {@link BlockBreakAnimationPacket} packet to the player and his viewers * Setting {@code destroyStage} to -1 reset the break animation * * @param blockPosition the position of the block * @param destroyStage the destroy stage * @throws IllegalArgumentException if {@code destroyStage} is not between -1 and 10 */ public void sendBlockBreakAnimation(BlockPosition blockPosition, byte destroyStage) { Check.argCondition(!MathUtils.isBetween(destroyStage, -1, 10), "The destroy stage has to be between -1 and 10"); BlockBreakAnimationPacket breakAnimationPacket = new BlockBreakAnimationPacket(); breakAnimationPacket.entityId = getEntityId() + 1; breakAnimationPacket.blockPosition = blockPosition; breakAnimationPacket.destroyStage = destroyStage; sendPacketToViewersAndSelf(breakAnimationPacket); } // Use legacy color formatting @Override public void sendMessage(String message) { sendMessage(ColoredText.of(message)); } /** * Send a message to the player * * @param coloredText the text to send */ public void sendMessage(ColoredText coloredText) { playerConnection.sendPacket(new ChatMessagePacket(coloredText.toString(), ChatMessagePacket.Position.CHAT)); } /** * Send a message to the player * * @param richMessage the rich text to send */ public void sendMessage(RichMessage richMessage) { playerConnection.sendPacket(new ChatMessagePacket(richMessage.toString(), ChatMessagePacket.Position.CHAT)); } public void playSound(Sound sound, SoundCategory soundCategory, int x, int y, int z, float volume, float pitch) { SoundEffectPacket soundEffectPacket = new SoundEffectPacket(); soundEffectPacket.soundId = sound.getId(); soundEffectPacket.soundCategory = soundCategory; soundEffectPacket.x = x * 8; soundEffectPacket.y = y * 8; soundEffectPacket.z = z * 8; soundEffectPacket.volume = volume; soundEffectPacket.pitch = pitch; playerConnection.sendPacket(soundEffectPacket); } /** * 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(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); } /** * Send a {@link StopSoundPacket} packet */ public void stopSound() { StopSoundPacket stopSoundPacket = new StopSoundPacket(); stopSoundPacket.flags = 0x00; playerConnection.sendPacket(stopSoundPacket); } public void sendHeaderFooter(ColoredText header, ColoredText footer) { PlayerListHeaderAndFooterPacket playerListHeaderAndFooterPacket = new PlayerListHeaderAndFooterPacket(); playerListHeaderAndFooterPacket.emptyHeader = header == null; playerListHeaderAndFooterPacket.emptyFooter = footer == null; playerListHeaderAndFooterPacket.header = header.toString(); playerListHeaderAndFooterPacket.footer = footer.toString(); playerConnection.sendPacket(playerListHeaderAndFooterPacket); } private void sendTitle(ColoredText text, TitlePacket.Action action) { TitlePacket titlePacket = new TitlePacket(); titlePacket.action = action; switch (action) { case SET_TITLE: titlePacket.titleText = text.toString(); break; case SET_SUBTITLE: titlePacket.subtitleText = text.toString(); break; case SET_ACTION_BAR: titlePacket.actionBarText = text.toString(); default: throw new UnsupportedOperationException("Invalid TitlePacket.Action type!"); } playerConnection.sendPacket(titlePacket); } public void sendTitleSubtitleMessage(ColoredText title, ColoredText subtitle) { sendTitle(title, TitlePacket.Action.SET_TITLE); sendTitle(subtitle, TitlePacket.Action.SET_SUBTITLE); } public void sendTitleMessage(ColoredText title) { sendTitle(title, TitlePacket.Action.SET_TITLE); } public void sendSubtitleMessage(ColoredText subtitle) { sendTitle(subtitle, TitlePacket.Action.SET_SUBTITLE); } public void sendActionBarMessage(ColoredText actionBar) { sendTitle(actionBar, TitlePacket.Action.SET_ACTION_BAR); } @Override public boolean isImmune(DamageType type) { if (!getGameMode().canTakeDamage()) { return type != DamageType.VOID; } return super.isImmune(type); } @Override public void setAttribute(Attribute attribute, float value) { super.setAttribute(attribute, value); if (playerConnection != null) playerConnection.sendPacket(getPropertiesPacket()); } @Override public void setHealth(float health) { super.setHealth(health); sendUpdateHealthPacket(); } /** * Get the player additional hearts * * @return the player additional hearts */ public float getAdditionalHearts() { return additionalHearts; } /** * Update the internal field and send the appropriate {@link EntityMetaDataPacket} * * @param additionalHearts the count of additional heartss */ public void setAdditionalHearts(float additionalHearts) { this.additionalHearts = additionalHearts; sendMetadataIndex(14); } /** * Get the player food * * @return the player food */ public int getFood() { return food; } /** * Set and refresh client food bar * * @param food the new food value */ 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; } /** * Set and refresh client food saturation * * @param foodSaturation the food saturation */ public void setFoodSaturation(float foodSaturation) { Check.argCondition(!MathUtils.isBetween(foodSaturation, 0, 5), "Food saturation has to be between 0 and 5"); this.foodSaturation = foodSaturation; sendUpdateHealthPacket(); } /** * Get if the player is eating * * @return true if the player is eating, false otherwise */ public boolean isEating() { return isEating; } /** * Get 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; } /** * Get the player display name in the tab-list * * @return the player display name, * null means that {@link #getUsername()} is displayed */ public ColoredText getDisplayName() { return displayName; } /** * Change the player display name in the tab-list *

* Set to null to show the player username * * @param displayName the display name */ public void setDisplayName(ColoredText displayName) { this.displayName = displayName; String jsonDisplayName = displayName != null ? displayName.toString() : null; PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_DISPLAY_NAME); infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateDisplayName(getUuid(), jsonDisplayName)); sendPacketToViewersAndSelf(infoPacket); } /** * Get the player skin * * @return the player skin object, * null means that the player has his {@link #getUuid()} default skin */ public PlayerSkin getSkin() { return skin; } /** * Change the player skin * * @param skin the player skin, null to reset it to his {@link #getUuid()} default skin */ public synchronized void setSkin(PlayerSkin skin) { this.skin = skin; DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket(); destroyEntitiesPacket.entityIds = new int[]{getEntityId()}; PlayerInfoPacket removePlayerPacket = getRemovePlayerToList(); PlayerInfoPacket addPlayerPacket = getAddPlayerToList(); RespawnPacket respawnPacket = new RespawnPacket(); respawnPacket.dimension = getDimension(); respawnPacket.gameMode = getGameMode(); respawnPacket.levelType = getLevelType(); playerConnection.sendPacket(removePlayerPacket); playerConnection.sendPacket(destroyEntitiesPacket); playerConnection.sendPacket(respawnPacket); playerConnection.sendPacket(addPlayerPacket); for (Player viewer : getViewers()) { PlayerConnection connection = viewer.getPlayerConnection(); connection.sendPacket(removePlayerPacket); connection.sendPacket(destroyEntitiesPacket); showPlayer(connection); } getInventory().update(); teleport(getPosition()); } /** * Used to update the internal skin field * * @param skin the player skin * @see #setSkin(PlayerSkin) instead */ protected void refreshSkin(PlayerSkin skin) { this.skin = skin; } /** * Get 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; } /** * Enable 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); } /** * Get the player username * * @return the player username */ public String getUsername() { return username; } /** * Change the internal player name, used for the {@link PlayerPreLoginEvent} * mostly unsafe outside of it * * @param username the new player name */ protected void setUsername(String username) { this.username = username; } private void sendChangeGameStatePacket(ChangeGameStatePacket.Reason reason, float value) { ChangeGameStatePacket changeGameStatePacket = new ChangeGameStatePacket(); changeGameStatePacket.reason = reason; changeGameStatePacket.value = value; playerConnection.sendPacket(changeGameStatePacket); } /** * @param item the item to drop * @return true if player can drop the item (event not cancelled), false otherwise */ public boolean dropItem(ItemStack item) { ItemDropEvent itemDropEvent = new ItemDropEvent(item); callEvent(ItemDropEvent.class, itemDropEvent); return !itemDropEvent.isCancelled(); } /** * Set the player resource pack * * @param resourcePack the resource pack */ public void setResourcePack(ResourcePack resourcePack) { Check.notNull(resourcePack, "The resource pack cannot be null"); final String url = resourcePack.getUrl(); Check.notNull(url, "The resource pack url cannot be null"); final String hash = resourcePack.getHash(); ResourcePackSendPacket resourcePackSendPacket = new ResourcePackSendPacket(); resourcePackSendPacket.url = url; resourcePackSendPacket.hash = hash; playerConnection.sendPacket(resourcePackSendPacket); } /** * Rotate 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(FacePoint facePoint, Position targetPosition) { facePosition(facePoint, targetPosition, null, null); } /** * Rotate 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(FacePoint facePoint, Entity entity, FacePoint targetPoint) { facePosition(facePoint, entity.getPosition(), entity, targetPoint); } private void facePosition(FacePoint facePoint, Position targetPosition, Entity entity, 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); } /** * Set the camera at {@code entity} eyes * * @param entity the entity to spectate */ public void spectate(Entity entity) { CameraPacket cameraPacket = new CameraPacket(); cameraPacket.cameraId = entity.getEntityId(); playerConnection.sendPacket(cameraPacket); } /** * Reset 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 the default respawn point */ public Position getRespawnPoint() { return respawnPoint; } /** * Change the default spawn point * * @param respawnPoint the player respawn point */ public void setRespawnPoint(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(); sendSynchronization(); } protected void refreshHealth() { this.food = 20; this.foodSaturation = 5; // refresh health and send health packet heal(); } protected void sendUpdateHealthPacket() { UpdateHealthPacket updateHealthPacket = new UpdateHealthPacket(); updateHealthPacket.health = getHealth(); updateHealthPacket.food = food; updateHealthPacket.foodSaturation = foodSaturation; playerConnection.sendPacket(updateHealthPacket); } /** * 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 */ public void setExp(float exp) { Check.argCondition(!MathUtils.isBetween(exp, 0, 1), "Exp should be between 0 and 1"); this.exp = exp; sendExperienceUpdatePacket(); } /** * 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(); } protected void sendExperienceUpdatePacket() { SetExperiencePacket setExperiencePacket = new SetExperiencePacket(); setExperiencePacket.percentage = exp; setExperiencePacket.level = level; playerConnection.sendPacket(setExperiencePacket); } protected void onChunkChange(Chunk lastChunk, Chunk newChunk) { long[] lastVisibleChunks = ChunkUtils.getChunksInRange(new Position(16 * lastChunk.getChunkX(), 0, 16 * lastChunk.getChunkZ()), MinecraftServer.CHUNK_VIEW_DISTANCE); long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(new Position(16 * newChunk.getChunkX(), 0, 16 * newChunk.getChunkZ()), MinecraftServer.CHUNK_VIEW_DISTANCE); int[] oldChunks = ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks); int[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks); // Unload old chunks for (int index : oldChunks) { int[] chunkPos = ChunkUtils.getChunkCoord(lastVisibleChunks[index]); UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket(); unloadChunkPacket.chunkX = chunkPos[0]; unloadChunkPacket.chunkZ = chunkPos[1]; playerConnection.sendPacket(unloadChunkPacket); Chunk chunk = instance.getChunk(chunkPos[0], chunkPos[1]); if (chunk != null) chunk.removeViewer(this); } updateViewPosition(newChunk); // Load new chunks for (int i = 0; i < newChunks.length; i++) { int index = newChunks[i]; int[] chunkPos = ChunkUtils.getChunkCoord(updatedVisibleChunks[index]); instance.loadOptionalChunk(chunkPos[0], chunkPos[1], chunk -> { if (chunk == null) { // Cannot load chunk (auto load is not enabled) return; } this.viewableChunks.add(chunk); chunk.addViewer(this); instance.sendChunk(this, chunk); }); } } @Override public void teleport(Position position, Runnable callback) { super.teleport(position, () -> { updatePlayerPosition(); if (callback != null) callback.run(); }); } @Override public void teleport(Position position) { teleport(position, null); } /** * Get the player connection *

* Used to send packets and get relatives stuff to the connection * * @return the player connection */ public PlayerConnection getPlayerConnection() { return playerConnection; } /** * Get if the player is online or not * * @return true if the player is online, false otherwise */ public boolean isOnline() { return playerConnection.isOnline(); } /** * Get the player settings * * @return the player settings */ public PlayerSettings getSettings() { return settings; } /** * Get the player dimension * * @return the player current dimension */ public Dimension getDimension() { return dimension; } 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; } /** * Get the player GameMode * * @return the player current gamemode */ public GameMode getGameMode() { return gameMode; } /** * Change the player GameMode * * @param gameMode the new player GameMode */ public void setGameMode(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); } /** * Returns true iff this player is in creative. Used for code readability * * @return true if the player is in creative mode, false otherwise */ public boolean isCreative() { return gameMode == GameMode.CREATIVE; } /** * Change the dimension of the player * Mostly unsafe since it requires sending chunks after * * @param dimension the new player dimension */ public void sendDimension(Dimension dimension) { Check.notNull(dimension, "Dimension cannot be null!"); Check.argCondition(dimension.equals(getDimension()), "The dimension need to be different than the current one!"); this.dimension = dimension; RespawnPacket respawnPacket = new RespawnPacket(); respawnPacket.dimension = dimension; respawnPacket.gameMode = gameMode; respawnPacket.levelType = levelType; playerConnection.sendPacket(respawnPacket); } /** * Kick the player with a reason * * @param text the kick reason */ public void kick(ColoredText text) { DisconnectPacket disconnectPacket = new DisconnectPacket(); disconnectPacket.message = text.toString(); playerConnection.sendPacket(disconnectPacket); playerConnection.disconnect(); playerConnection.refreshOnline(false); } /** * Kick the player with a reason * * @param message the kick reason */ public void kick(String message) { kick(ColoredText.of(message)); } public LevelType getLevelType() { return levelType; } /** * Change 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); } /** * Get the player held slot (0-8) * * @return the current held slot for the player */ public byte getHeldSlot() { return heldSlot; } public void setTeam(Team team) { if (this.team == team) return; if (this.team != null) { this.team.removePlayer(this); } this.team = team; if (team != null) { team.addPlayer(this); sendPacketToViewers(team.getTeamsCreationPacket()); // FIXME: only if viewer hasn't already register this team } } public void setBelowNameScoreboard(BelowNameScoreboard belowNameScoreboard) { if (this.belowNameScoreboard == belowNameScoreboard) return; if (this.belowNameScoreboard != null) { this.belowNameScoreboard.removeViewer(this); } this.belowNameScoreboard = belowNameScoreboard; if (belowNameScoreboard != null) { belowNameScoreboard.addViewer(this); belowNameScoreboard.displayScoreboard(this); getViewers().forEach(player -> belowNameScoreboard.addViewer(player)); } } /** * 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 */ public CustomBlock getCustomBlockTarget() { return targetCustomBlock; } /** * @return an unmodifiable {@link Set} containing all the current player viewable boss bars */ public Set getBossBars() { return Collections.unmodifiableSet(bossBars); } /** * Get the player open inventory * * @return the currently open inventory, null if there is not (player inventory is not detected) */ public Inventory getOpenInventory() { return openInventory; } /** * Open 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(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(); 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(); } /** * Close 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; } /** * Get the player viewable chunks * * @return an unmodifiable {@link Set} containing all the chunks that the player sees */ public Set getViewableChunks() { return Collections.unmodifiableSet(viewableChunks); } /** * Remove all the boss bars that the player has */ public void clearBossBars() { this.bossBars.forEach(bossBar -> bossBar.removeViewer(this)); } /** * Send a {@link UpdateViewPositionPacket} to the player * * @param chunk the chunk to update the view */ public void updateViewPosition(Chunk chunk) { UpdateViewPositionPacket updateViewPositionPacket = new UpdateViewPositionPacket(); updateViewPositionPacket.chunkX = chunk.getChunkX(); updateViewPositionPacket.chunkZ = chunk.getChunkZ(); playerConnection.sendPacket(updateViewPositionPacket); } /** * Used for synchronization purpose, mainly for teleportation */ protected void updatePlayerPosition() { PlayerPositionAndLookPacket positionAndLookPacket = new PlayerPositionAndLookPacket(); positionAndLookPacket.position = position; positionAndLookPacket.flags = 0x00; positionAndLookPacket.teleportId = teleportId++; playerConnection.sendPacket(positionAndLookPacket); } /** * Get the player permission level * * @return the player permission level */ public int getPermissionLevel() { return permissionLevel; } /** * Change the player permission level * * @param permissionLevel the new player permission level */ 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 byte permissionLevelStatus = (byte) (24 + permissionLevel); triggerStatus(permissionLevelStatus); } /** * Set 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 byte debugScreenStatus = (byte) (reduced ? 22 : 23); triggerStatus(debugScreenStatus); } /** * Get 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 invulnerable; } /** * 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) { this.invulnerable = invulnerable; refreshAbilities(); } /** * Get if the player is currently flying * * @return true if the player if flying, false otherwise */ public boolean isFlying() { return flying; } /** * Set the player flying * * @param flying should the player fly */ public void setFlying(boolean flying) { this.flying = flying; refreshAbilities(); } /** * Update 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; } /** * Get if the player is allowed to fly * * @return true if the player if allowed to fly, false otherwise */ public boolean isAllowFlying() { return allowFlying; } /** * Allow 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; } /** * Change the player ability "Creative Mode" * see *

* WARNING: this has nothing to do with {@link CustomBlock#getBreakDelay(Player, BlockPosition)} * * @param instantBreak */ public void setInstantBreak(boolean instantBreak) { this.instantBreak = instantBreak; refreshAbilities(); } /** * Get the player flying speed * * @return the flying speed of the player */ public float getFlyingSpeed() { return flyingSpeed; } /** * Update the internal field and send a {@link PlayerAbilitiesPacket} with the new flying speed * * @param flyingSpeed the new flying speed of the player */ public void setFlyingSpeed(float flyingSpeed) { this.flyingSpeed = flyingSpeed; refreshAbilities(); } public float getFieldViewModifier() { return fieldViewModifier; } public void setFieldViewModifier(float fieldViewModifier) { this.fieldViewModifier = fieldViewModifier; refreshAbilities(); } /** * This is the map used to send the statistic packet * It is possible to add/remove/change statistic value directly into it * * @return the modifiable statistic map */ public Map getStatisticValueMap() { return statisticValueMap; } /** * Get the player vehicle information * * @return the player vehicle information */ public PlayerVehicleInformation getVehicleInformation() { return vehicleInformation; } 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(ClientPlayPacket packet) { this.packets.add(packet); } /** * Change 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; } /** * Change 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 */ public ItemUpdateStateEvent callItemUpdateStateEvent(boolean allowFood) { Material mainHandMat = Material.fromId(getItemInMainHand().getMaterialId()); Material offHandMat = Material.fromId(getItemInOffHand().getMaterialId()); boolean isOffhand = offHandMat.hasState(); ItemStack updatedItem = isOffhand ? getItemInOffHand() : mainHandMat.hasState() ? getItemInMainHand() : null; if (updatedItem == null) // No item with state, cancel return null; boolean isFood = updatedItem.getMaterial().isFood(); if (isFood && !allowFood) return null; ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(updatedItem, isOffhand ? Hand.OFF : Hand.MAIN); callEvent(ItemUpdateStateEvent.class, itemUpdateStateEvent); return itemUpdateStateEvent; } /** * Make 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 breakTime the time it will take to break the block in milliseconds */ public void setTargetBlock(CustomBlock targetCustomBlock, BlockPosition targetBlockPosition, int breakTime) { this.targetCustomBlock = targetCustomBlock; this.targetBlockPosition = targetBlockPosition; this.targetBlockTime = targetBlockPosition == null ? 0 : System.currentTimeMillis(); this.blockBreakTime = breakTime; } /** * Reset data from the current block the player is mining. * If the currently mined block (or if there isn't any) is not a CustomBlock, nothing append */ public void resetTargetBlock() { if (targetBlockPosition != null) { sendBlockBreakAnimation(targetBlockPosition, (byte) -1); // Clear the break animation this.targetCustomBlock = null; this.targetBlockPosition = null; this.targetBlockTime = 0; // Remove effect RemoveEntityEffectPacket removeEntityEffectPacket = new RemoveEntityEffectPacket(); removeEntityEffectPacket.entityId = getEntityId(); removeEntityEffectPacket.effectId = 4; getPlayerConnection().sendPacket(removeEntityEffectPacket); } } /** * Used internally to add Bossbar in the {@link #getBossBars()} set * You probably want to use {@link BossBar#addViewer(Player)} * * @param bossBar the bossbar to add internally */ public void refreshAddBossbar(BossBar bossBar) { this.bossBars.add(bossBar); } /** * Used internally to remove Bossbar from the {@link #getBossBars()} set * You probably want to use {@link BossBar#removeViewer(Player)} * * @param bossBar the bossbar to remove internally */ public void refreshRemoveBossbar(BossBar bossBar) { this.bossBars.remove(bossBar); } 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#CHUNK_VIEW_DISTANCE} or {@link PlayerSettings#getViewDistance()} * based on which one is the lowest */ public int getChunkRange() { int serverRange = MinecraftServer.CHUNK_VIEW_DISTANCE; int playerRange = getSettings().viewDistance; if (playerRange == 0) { return serverRange; // Didn't receive settings packet yet (is the case on login) } else { return Math.min(playerRange, serverRange); } } /** * Get the last sent keep alive id * * @return the last keep alive id sent to the player */ public long getLastKeepAlive() { return lastKeepAlive; } /** * Get the packet to add the player from tab-list * * @return a {@link PlayerInfoPacket} to add the player */ protected PlayerInfoPacket getAddPlayerToList() { final String textures = skin == null ? "" : skin.getTextures(); final String signature = skin == null ? null : skin.getSignature(); String jsonDisplayName = displayName != null ? displayName.toString() : null; PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.ADD_PLAYER); PlayerInfoPacket.AddPlayer addPlayer = new PlayerInfoPacket.AddPlayer(getUuid(), getUsername(), getGameMode(), getLatency()); addPlayer.displayName = jsonDisplayName; PlayerInfoPacket.AddPlayer.Property prop = new PlayerInfoPacket.AddPlayer.Property("textures", textures, signature); addPlayer.properties.add(prop); playerInfoPacket.playerInfos.add(addPlayer); return playerInfoPacket; } /** * Get the packet to remove the player from tab-list * * @return a {@link PlayerInfoPacket} to add the player */ protected PlayerInfoPacket getRemovePlayerToList() { PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER); PlayerInfoPacket.RemovePlayer removePlayer = new PlayerInfoPacket.RemovePlayer(getUuid()); playerInfoPacket.playerInfos.add(removePlayer); return playerInfoPacket; } /** * Send 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 does not sync the player, please use {@link #addViewer(Player)} * * @param connection the connection to show the player to */ protected void showPlayer(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 (team != null) connection.sendPacket(team.getTeamsCreationPacket()); } @Override public ItemStack getItemInMainHand() { return inventory.getItemInMainHand(); } @Override public void setItemInMainHand(ItemStack itemStack) { inventory.setItemInMainHand(itemStack); } @Override public ItemStack getItemInOffHand() { return inventory.getItemInOffHand(); } @Override public void setItemInOffHand(ItemStack itemStack) { inventory.setItemInOffHand(itemStack); } @Override public ItemStack getHelmet() { return inventory.getHelmet(); } @Override public void setHelmet(ItemStack itemStack) { inventory.setHelmet(itemStack); } @Override public ItemStack getChestplate() { return inventory.getChestplate(); } @Override public void setChestplate(ItemStack itemStack) { inventory.setChestplate(itemStack); } @Override public ItemStack getLeggings() { return inventory.getLeggings(); } @Override public void setLeggings(ItemStack itemStack) { inventory.setLeggings(itemStack); } @Override public ItemStack getBoots() { return inventory.getBoots(); } @Override public void setBoots(ItemStack itemStack) { inventory.setBoots(itemStack); } /** * Represent the main or off hand of the player */ public enum Hand { MAIN, OFF } public enum FacePoint { FEET, EYE } // Settings enum /** * Represent 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; /** * The player game language * * @return the player locale */ public String getLocale() { return locale; } /** * Get the player view distance * * @return the player view distance */ public byte getViewDistance() { return viewDistance; } /** * Get the player chat mode * * @return the player chat mode */ public ChatMode getChatMode() { return chatMode; } /** * Get 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; } /** * Get the player main hand * * @return the player main hand */ public MainHand getMainHand() { return mainHand; } /** * Change 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) { this.locale = locale; this.viewDistance = viewDistance; this.chatMode = chatMode; this.chatColors = chatColors; this.displayedSkinParts = displayedSkinParts; this.mainHand = mainHand; } } }