package net.minestom.server.entity; import net.minestom.server.MinecraftServer; import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.attribute.Attribute; import net.minestom.server.attribute.AttributeInstance; import net.minestom.server.attribute.Attributes; import net.minestom.server.bossbar.BossBar; import net.minestom.server.chat.ChatParser; import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.JsonMessage; 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.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.gamedata.tags.TagManager; 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.listener.PlayerDiggingListener; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.network.packet.client.play.ClientChatMessagePacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.play.*; 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.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.binary.BinaryWriter; 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.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.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; /** * Those are the major actors of the server, * they are not necessary backed by a {@link NettyPlayerConnection} as shown by {@link FakePlayer}. *
* You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}. */ public class Player extends LivingEntity implements CommandSender { /** * @see #getPlayerSynchronizationGroup() */ private static volatile int playerSynchronizationGroup = 50; /** * For the number of viewers that a player has, the position synchronization packet will be sent * every 1 tick + (viewers/{@code playerSynchronizationGroup}). * (eg with a value of 100, having 300 viewers means sending the synchronization packet every 3 ticks) *
* Used to prevent sending exponentially more packets and therefore reduce network load.
*
* @return the viewers count which would result in a 1 tick delay
*/
public static int getPlayerSynchronizationGroup() {
return playerSynchronizationGroup;
}
/**
* Changes the viewers count resulting in an additional delay of 1 tick for the position synchronization.
*
* @param playerSynchronizationGroup the new synchronization group size
* @see #getPlayerSynchronizationGroup()
*/
public static void setPlayerSynchronizationGroup(int playerSynchronizationGroup) {
Player.playerSynchronizationGroup = playerSynchronizationGroup;
}
/**
* Gets the number of tick between each position synchronization.
*
* @param viewersCount the player viewers count
* @return the number of tick between each position synchronization.
*/
public static int getPlayerSynchronizationTickDelay(int viewersCount) {
return viewersCount / playerSynchronizationGroup + 1;
}
private long lastKeepAlive;
private boolean answerKeepAlive;
private String username;
protected final PlayerConnection playerConnection;
// All the entities that this player can see
protected final Set
* WARNING: executed in the main update thread
*/
protected void init() {
JoinGamePacket joinGamePacket = new JoinGamePacket();
joinGamePacket.entityId = getEntityId();
joinGamePacket.gameMode = gameMode;
joinGamePacket.dimensionType = dimensionType;
joinGamePacket.maxPlayers = 0; // Unused
joinGamePacket.viewDistance = MinecraftServer.getChunkViewDistance();
joinGamePacket.reducedDebugInfo = false;
joinGamePacket.isFlat = levelFlat;
playerConnection.sendPacket(joinGamePacket);
// Server brand name
{
playerConnection.sendPacket(PluginMessagePacket.getBrandPacket());
}
ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket();
serverDifficultyPacket.difficulty = MinecraftServer.getDifficulty();
serverDifficultyPacket.locked = true;
playerConnection.sendPacket(serverDifficultyPacket);
SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket();
spawnPositionPacket.x = (int) respawnPoint.getX();
spawnPositionPacket.y = (int) respawnPoint.getY();
spawnPositionPacket.z = (int) respawnPoint.getZ();
playerConnection.sendPacket(spawnPositionPacket);
// Add player to list with spawning skin
PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this, skin);
callEvent(PlayerSkinInitEvent.class, skinInitEvent);
this.skin = skinInitEvent.getSkin();
playerConnection.sendPacket(getAddPlayerToList());
// Commands start
{
CommandManager commandManager = MinecraftServer.getCommandManager();
DeclareCommandsPacket declareCommandsPacket = commandManager.createDeclareCommandsPacket(this);
playerConnection.sendPacket(declareCommandsPacket);
}
// Commands end
// Recipes start
{
RecipeManager recipeManager = MinecraftServer.getRecipeManager();
DeclareRecipesPacket declareRecipesPacket = recipeManager.getDeclareRecipesPacket();
if (declareRecipesPacket.recipes != null) {
playerConnection.sendPacket(declareRecipesPacket);
}
List
* Be aware that because chunk operations are expensive,
* it is possible for this method to be non-blocking when retrieving chunks is required.
*
* When this method is called for the first time (during player login), the player will be teleport at {@link #getRespawnPoint()}.
*
* @param instance the new instance of the player
*/
@Override
public void setInstance(@NotNull Instance instance) {
Check.notNull(instance, "instance cannot be null!");
Check.argCondition(this.instance == instance, "Instance should be different than the current one");
final boolean firstSpawn = this.instance == null; // TODO: Handle player reconnections, must be false in that case too
// true if the chunks need to be sent to the client, can be false if the instances share the same chunks (eg SharedInstance)
final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance);
if (needWorldRefresh && !firstSpawn) {
// Remove all previous viewable chunks (from the previous instance)
for (Chunk viewableChunk : viewableChunks) {
viewableChunk.removeViewer(this);
}
if (this.instance != null) {
final DimensionType instanceDimensionType = instance.getDimensionType();
if (dimensionType != instanceDimensionType)
sendDimension(instanceDimensionType);
}
final long[] visibleChunks = ChunkUtils.getChunksInRange(position, getChunkRange());
final int length = visibleChunks.length;
AtomicInteger counter = new AtomicInteger(0);
for (long visibleChunk : visibleChunks) {
final int chunkX = ChunkUtils.getChunkCoordX(visibleChunk);
final int chunkZ = ChunkUtils.getChunkCoordZ(visibleChunk);
final ChunkCallback callback = (chunk) -> {
if (chunk != null) {
chunk.addViewer(this);
if (chunk.getChunkX() == ChunkUtils.getChunkCoordinate((int) getPosition().getX()) &&
chunk.getChunkZ() == ChunkUtils.getChunkCoordinate((int) getPosition().getZ())) {
updateViewPosition(chunk);
}
}
final boolean isLast = counter.get() == length - 1;
if (isLast) {
// This is the last chunk to be loaded , spawn player
spawnPlayer(instance, false);
} 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);
}
} else if (firstSpawn) {
// The player always believe that his position is 0;0 so this is a pretty hacky fix
instance.loadOptionalChunk(0, 0, chunk -> spawnPlayer(instance, true));
} 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, false);
}
}
/**
* 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)}.
*
* @param firstSpawn true if this is the player first spawn
*/
private void spawnPlayer(Instance instance, boolean firstSpawn) {
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
super.setInstance(instance);
if (firstSpawn) {
teleport(getRespawnPoint());
}
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
callEvent(PlayerSpawnEvent.class, spawnEvent);
}
@NotNull
@Override
public Consumer
* Sets to null to show the player username.
*
* @param displayName the display name, null to display the username
*/
public void setDisplayName(@Nullable ColoredText displayName) {
this.displayName = displayName;
PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_DISPLAY_NAME);
infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateDisplayName(getUuid(), displayName));
sendPacketToViewersAndSelf(infoPacket);
}
/**
* Gets the player skin.
*
* @return the player skin object,
* null means that the player has his {@link #getUuid()} default skin
*/
@Nullable
public PlayerSkin getSkin() {
return skin;
}
/**
* Changes the player skin.
*
* This does remove the player for all viewers to spawn it again with the correct new skin.
*
* @param skin the player skin, null to reset it to his {@link #getUuid()} default skin
* @see PlayerSkinInitEvent if you want to apply the skin at connection
*/
public synchronized void setSkin(@Nullable PlayerSkin skin) {
this.skin = skin;
if (instance == null)
return;
DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket();
destroyEntitiesPacket.entityIds = new int[]{getEntityId()};
final PlayerInfoPacket removePlayerPacket = getRemovePlayerToList();
final PlayerInfoPacket addPlayerPacket = getAddPlayerToList();
RespawnPacket respawnPacket = new RespawnPacket();
respawnPacket.dimensionType = getDimensionType();
respawnPacket.gameMode = getGameMode();
respawnPacket.isFlat = levelFlat;
playerConnection.sendPacket(removePlayerPacket);
playerConnection.sendPacket(destroyEntitiesPacket);
playerConnection.sendPacket(respawnPacket);
playerConnection.sendPacket(addPlayerPacket);
for (Player viewer : getViewers()) {
final PlayerConnection connection = viewer.getPlayerConnection();
connection.sendPacket(removePlayerPacket);
connection.sendPacket(destroyEntitiesPacket);
showPlayer(connection);
}
getInventory().update();
teleport(getPosition());
}
/**
* Gets if the player has the respawn screen enabled or disabled.
*
* @return true if the player has the respawn screen, false if he didn't
*/
public boolean isEnableRespawnScreen() {
return enableRespawnScreen;
}
/**
* Enables or disable the respawn screen.
*
* @param enableRespawnScreen true to enable the respawn screen, false to disable it
*/
public void setEnableRespawnScreen(boolean enableRespawnScreen) {
this.enableRespawnScreen = enableRespawnScreen;
sendChangeGameStatePacket(ChangeGameStatePacket.Reason.ENABLE_RESPAWN_SCREEN, enableRespawnScreen ? 0 : 1);
}
/**
* Gets the player username.
*
* @return the player username
*/
@NotNull
public String getUsername() {
return username;
}
/**
* Changes the internal player name, used for the {@link PlayerPreLoginEvent}
* mostly unsafe outside of it.
*
* @param username the new player name
*/
protected void setUsername(@NotNull String username) {
this.username = username;
}
private void sendChangeGameStatePacket(@NotNull ChangeGameStatePacket.Reason reason, float value) {
ChangeGameStatePacket changeGameStatePacket = new ChangeGameStatePacket();
changeGameStatePacket.reason = reason;
changeGameStatePacket.value = value;
playerConnection.sendPacket(changeGameStatePacket);
}
/**
* Calls an {@link ItemDropEvent} with a specified item.
*
* @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) {
ItemDropEvent itemDropEvent = new ItemDropEvent(item);
callEvent(ItemDropEvent.class, itemDropEvent);
return !itemDropEvent.isCancelled();
}
/**
* Sets the player resource pack.
*
* @param resourcePack the resource pack
*/
public void setResourcePack(@NotNull ResourcePack resourcePack) {
Check.notNull(resourcePack, "The resource pack cannot be null");
final String url = resourcePack.getUrl();
final String hash = resourcePack.getHash();
ResourcePackSendPacket resourcePackSendPacket = new ResourcePackSendPacket();
resourcePackSendPacket.url = url;
resourcePackSendPacket.hash = hash;
playerConnection.sendPacket(resourcePackSendPacket);
}
/**
* Rotates the player to face {@code targetPosition}.
*
* @param facePoint the point from where the player should aim
* @param targetPosition the target position to face
*/
public void facePosition(@NotNull FacePoint facePoint, @NotNull Position targetPosition) {
facePosition(facePoint, targetPosition, null, null);
}
/**
* Rotates the player to face {@code entity}.
*
* @param facePoint the point from where the player should aim
* @param entity the entity to face
* @param targetPoint the point to aim at {@code entity} position
*/
public void facePosition(@NotNull FacePoint facePoint, Entity entity, FacePoint targetPoint) {
facePosition(facePoint, entity.getPosition(), entity, targetPoint);
}
private void facePosition(@NotNull FacePoint facePoint, @NotNull Position targetPosition,
@Nullable Entity entity, @Nullable FacePoint targetPoint) {
FacePlayerPacket facePlayerPacket = new FacePlayerPacket();
facePlayerPacket.entityFacePosition = facePoint == FacePoint.EYE ?
FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET;
facePlayerPacket.targetX = targetPosition.getX();
facePlayerPacket.targetY = targetPosition.getY();
facePlayerPacket.targetZ = targetPosition.getZ();
if (entity != null) {
facePlayerPacket.entityId = entity.getEntityId();
facePlayerPacket.entityFacePosition = targetPoint == FacePoint.EYE ?
FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET;
}
playerConnection.sendPacket(facePlayerPacket);
}
/**
* Sets the camera at {@code entity} eyes.
*
* @param entity the entity to spectate
*/
public void spectate(@NotNull Entity entity) {
CameraPacket cameraPacket = new CameraPacket();
cameraPacket.cameraId = entity.getEntityId();
playerConnection.sendPacket(cameraPacket);
}
/**
* Resets the camera at the player.
*/
public void stopSpectating() {
spectate(this);
}
/**
* Used to retrieve the default spawn point.
*
* Can be altered by the {@link PlayerRespawnEvent#setRespawnPosition(Position)}.
*
* @return a copy of the default respawn point
*/
@NotNull
public Position getRespawnPoint() {
return respawnPoint.copy();
}
/**
* Changes the default spawn point.
*
* @param respawnPoint the player respawn point
*/
public void setRespawnPoint(@NotNull Position respawnPoint) {
this.respawnPoint = respawnPoint;
}
/**
* Called after the player teleportation to refresh his position
* and send data to his new viewers.
*/
protected void refreshAfterTeleport() {
getInventory().update();
SpawnPlayerPacket spawnPlayerPacket = new SpawnPlayerPacket();
spawnPlayerPacket.entityId = getEntityId();
spawnPlayerPacket.playerUuid = getUuid();
spawnPlayerPacket.position = getPosition();
sendPacketToViewers(spawnPlayerPacket);
// Update for viewers
sendPacketToViewersAndSelf(getVelocityPacket());
sendPacketToViewersAndSelf(getMetadataPacket());
playerConnection.sendPacket(getPropertiesPacket());
syncEquipments();
{
// Send new chunks
final BlockPosition pos = position.toBlockPosition();
final Chunk chunk = instance.getChunk(pos.getX() >> 4, pos.getZ() >> 4);
Check.notNull(chunk, "Tried to interact with an unloaded chunk.");
refreshVisibleChunks(chunk);
}
}
/**
* Sets the player food and health values to their maximum.
*/
protected void refreshHealth() {
this.food = 20;
this.foodSaturation = 5;
// refresh health and send health packet
heal();
}
/**
* Sends an {@link UpdateHealthPacket} to refresh client-side information about health and food.
*/
protected void sendUpdateHealthPacket() {
UpdateHealthPacket updateHealthPacket = new UpdateHealthPacket();
updateHealthPacket.health = getHealth();
updateHealthPacket.food = food;
updateHealthPacket.foodSaturation = foodSaturation;
playerConnection.sendPacket(updateHealthPacket);
}
/**
* Gets the percentage displayed in the experience bar.
*
* @return the exp percentage 0-1
*/
public float getExp() {
return exp;
}
/**
* Used to change the percentage experience bar.
* This cannot change the displayed level, see {@link #setLevel(int)}.
*
* @param exp a percentage between 0 and 1
* @throws IllegalArgumentException if {@code exp} is not between 0 and 1
*/
public void setExp(float exp) {
Check.argCondition(!MathUtils.isBetween(exp, 0, 1), "Exp should be between 0 and 1");
this.exp = exp;
sendExperienceUpdatePacket();
}
/**
* Gets the level of the player displayed in the experience bar.
*
* @return the player level
*/
public int getLevel() {
return level;
}
/**
* Used to change the level of the player
* This cannot change the displayed percentage bar see {@link #setExp(float)}
*
* @param level the new level of the player
*/
public void setLevel(int level) {
this.level = level;
sendExperienceUpdatePacket();
}
/**
* Sends a {@link SetExperiencePacket} to refresh client-side information about the experience bar.
*/
protected void sendExperienceUpdatePacket() {
SetExperiencePacket setExperiencePacket = new SetExperiencePacket();
setExperiencePacket.percentage = exp;
setExperiencePacket.level = level;
playerConnection.sendPacket(setExperiencePacket);
}
/**
* Called when the player changes chunk (move from one to another).
* Can also be used to refresh the list of chunks that the client should see based on {@link #getChunkRange()}.
*
* It does remove and add the player from the chunks viewers list when removed or added.
* It also calls the events {@link PlayerChunkUnloadEvent} and {@link PlayerChunkLoadEvent}.
*
* @param newChunk the current/new player chunk (can be the current one)
*/
public void refreshVisibleChunks(@NotNull Chunk newChunk) {
// Previous chunks indexes
final long[] lastVisibleChunks = viewableChunks.stream().mapToLong(viewableChunks ->
ChunkUtils.getChunkIndex(viewableChunks.getChunkX(), viewableChunks.getChunkZ())
).toArray();
// New chunks indexes
final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunk.toPosition(), getChunkRange());
// Find the difference between the two arrays¬
final int[] oldChunks = ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks);
final int[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks);
// Unload old chunks
for (int index : oldChunks) {
final long chunkIndex = lastVisibleChunks[index];
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
unloadChunkPacket.chunkX = chunkX;
unloadChunkPacket.chunkZ = chunkZ;
playerConnection.sendPacket(unloadChunkPacket);
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk != null)
chunk.removeViewer(this);
}
// Update client render distance
updateViewPosition(newChunk);
// Load new chunks
for (int index : newChunks) {
final long chunkIndex = updatedVisibleChunks[index];
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
instance.loadOptionalChunk(chunkX, chunkZ, chunk -> {
if (chunk == null) {
// Cannot load chunk (auto load is not enabled)
return;
}
chunk.addViewer(this);
});
}
}
/**
* Refreshes the list of entities that the player should be able to see based on {@link MinecraftServer#getEntityViewDistance()}
* and {@link Entity#isAutoViewable()}.
*
* @param newChunk the new chunk of the player (can be the current one)
*/
public void refreshVisibleEntities(@NotNull Chunk newChunk) {
final int entityViewDistance = MinecraftServer.getEntityViewDistance();
final float maximalDistance = entityViewDistance * Chunk.CHUNK_SECTION_SIZE;
// Manage already viewable entities
this.viewableEntities.forEach(entity -> {
final float distance = entity.getDistance(this);
if (distance > maximalDistance) {
// Entity shouldn't be viewable anymore
if (isAutoViewable()) {
entity.removeViewer(this);
}
if (entity instanceof Player && entity.isAutoViewable()) {
removeViewer((Player) entity);
}
}
});
// Manage entities in unchecked chunks
final long[] chunksInRange = ChunkUtils.getChunksInRange(newChunk.toPosition(), entityViewDistance);
for (long chunkIndex : chunksInRange) {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null)
continue;
instance.getChunkEntities(chunk).forEach(entity -> {
if (isAutoViewable() && !entity.viewers.contains(this)) {
entity.addViewer(this);
}
if (entity instanceof Player && entity.isAutoViewable() && !viewers.contains(entity)) {
addViewer((Player) entity);
}
});
}
}
@Override
public void teleport(@NotNull Position position, @Nullable Runnable callback) {
super.teleport(position, () -> {
updatePlayerPosition();
OptionalCallback.execute(callback);
});
}
@Override
public void teleport(@NotNull Position position) {
teleport(position, null);
}
/**
* Gets the player connection.
*
* Used to send packets and get stuff related to the connection.
*
* @return the player connection
*/
@NotNull
public PlayerConnection getPlayerConnection() {
return playerConnection;
}
/**
* Gets if the player is online or not.
*
* @return true if the player is online, false otherwise
*/
public boolean isOnline() {
return playerConnection.isOnline();
}
/**
* Gets the player settings.
*
* @return the player settings
*/
@NotNull
public PlayerSettings getSettings() {
return settings;
}
/**
* Gets the player dimension.
*
* @return the player current dimension
*/
public DimensionType getDimensionType() {
return dimensionType;
}
@NotNull
public PlayerInventory getInventory() {
return inventory;
}
/**
* Used to get the player latency,
* computed by seeing how long it takes the client to answer the {@link KeepAlivePacket} packet.
*
* @return the player latency
*/
public int getLatency() {
return latency;
}
/**
* Gets the player {@link GameMode}.
*
* @return the player current gamemode
*/
public GameMode getGameMode() {
return gameMode;
}
/**
* Changes the player {@link GameMode}.
*
* @param gameMode the new player GameMode
*/
public void setGameMode(@NotNull GameMode gameMode) {
Check.notNull(gameMode, "GameMode cannot be null");
this.gameMode = gameMode;
sendChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.getId());
PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_GAMEMODE);
infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateGamemode(getUuid(), gameMode));
sendPacketToViewersAndSelf(infoPacket);
}
/**
* Gets if this player is in creative. Used for code readability.
*
* @return true if the player is in creative mode
*/
public boolean isCreative() {
return gameMode == GameMode.CREATIVE;
}
/**
* Changes the dimension of the player.
* Mostly unsafe since it requires sending chunks after.
*
* @param dimensionType the new player dimension
*/
protected void sendDimension(@NotNull DimensionType dimensionType) {
Check.notNull(dimensionType, "Dimension cannot be null!");
Check.argCondition(dimensionType.equals(getDimensionType()), "The dimension needs to be different than the current one!");
this.dimensionType = dimensionType;
RespawnPacket respawnPacket = new RespawnPacket();
respawnPacket.dimensionType = dimensionType;
respawnPacket.gameMode = gameMode;
respawnPacket.isFlat = levelFlat;
playerConnection.sendPacket(respawnPacket);
}
/**
* Kicks the player with a reason.
*
* @param text the kick reason
*/
public void kick(@NotNull ColoredText text) {
DisconnectPacket disconnectPacket = new DisconnectPacket();
disconnectPacket.message = text;
playerConnection.sendPacket(disconnectPacket);
playerConnection.disconnect();
playerConnection.refreshOnline(false);
}
/**
* Kicks the player with a reason.
*
* @param message the kick reason
*/
public void kick(@NotNull String message) {
kick(ColoredText.of(message));
}
/**
* Changes the current held slot for the player.
*
* @param slot the slot that the player has to held
* @throws IllegalArgumentException if {@code slot} is not between 0 and 8
*/
public void setHeldItemSlot(byte slot) {
Check.argCondition(!MathUtils.isBetween(slot, 0, 8), "Slot has to be between 0 and 8");
HeldItemChangePacket heldItemChangePacket = new HeldItemChangePacket();
heldItemChangePacket.slot = slot;
playerConnection.sendPacket(heldItemChangePacket);
refreshHeldSlot(slot);
}
/**
* Gets the player held slot (0-8).
*
* @return the current held slot for the player
*/
public byte getHeldSlot() {
return heldSlot;
}
public void setTeam(Team team) {
super.setTeam(team);
if (team != null)
getPlayerConnection().sendPacket(team.createTeamsCreationPacket());
}
/**
* Changes the tag below the name.
*
* @param belowNameTag The new below name tag
*/
public void setBelowNameTag(BelowNameTag belowNameTag) {
if (this.belowNameTag == belowNameTag) return;
if (this.belowNameTag != null) {
this.belowNameTag.removeViewer(this);
}
this.belowNameTag = belowNameTag;
}
/**
* Gets if the player is sneaking.
*
* WARNING: this can be bypassed by hacked client, this is only what the client told the server.
*
* @return true if the player is sneaking
*/
public boolean isSneaking() {
return crouched;
}
/**
* Gets if the player is sprinting.
*
* WARNING: this can be bypassed by hacked client, this is only what the client told the server.
*
* @return true if the player is sprinting
*/
public boolean isSprinting() {
return sprinting;
}
/**
* Used to get the {@link CustomBlock} that the player is currently mining.
*
* @return the currently mined {@link CustomBlock} by the player, null if there is not
*/
@Nullable
public CustomBlock getCustomBlockTarget() {
return targetCustomBlock;
}
/**
* Gets the player open inventory.
*
* @return the currently open inventory, null if there is not (player inventory is not detected)
*/
@Nullable
public Inventory getOpenInventory() {
return openInventory;
}
/**
* Opens the specified Inventory, close the previous inventory if existing.
*
* @param inventory the inventory to open
* @return true if the inventory has been opened/sent to the player, false otherwise (cancelled by event)
*/
public boolean openInventory(@NotNull Inventory inventory) {
Check.notNull(inventory, "Inventory cannot be null, use Player#closeInventory() to close current");
InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(this, inventory);
callCancellableEvent(InventoryOpenEvent.class, inventoryOpenEvent, () -> {
if (getOpenInventory() != null) {
closeInventory();
}
Inventory newInventory = inventoryOpenEvent.getInventory();
if (newInventory == null) {
// just close the inventory
return;
}
OpenWindowPacket openWindowPacket = new OpenWindowPacket();
openWindowPacket.windowId = newInventory.getWindowId();
openWindowPacket.windowType = newInventory.getInventoryType().getWindowType();
openWindowPacket.title = newInventory.getTitle();
playerConnection.sendPacket(openWindowPacket);
newInventory.addViewer(this);
this.openInventory = newInventory;
});
return !inventoryOpenEvent.isCancelled();
}
/**
* Closes the current inventory if there is any.
* It closes the player inventory (when opened) if {@link #getOpenInventory()} returns null.
*/
public void closeInventory() {
Inventory openInventory = getOpenInventory();
// Drop cursor item when closing inventory
ItemStack cursorItem;
if (openInventory == null) {
cursorItem = getInventory().getCursorItem();
getInventory().setCursorItem(ItemStack.getAirItem());
} else {
cursorItem = openInventory.getCursorItem(this);
openInventory.setCursorItem(this, ItemStack.getAirItem());
}
if (!cursorItem.isAir()) {
// Add item to inventory if he hasn't been able to drop it
if (!dropItem(cursorItem)) {
getInventory().addItemStack(cursorItem);
}
}
CloseWindowPacket closeWindowPacket = new CloseWindowPacket();
if (openInventory == null) {
closeWindowPacket.windowId = 0;
} else {
closeWindowPacket.windowId = openInventory.getWindowId();
openInventory.removeViewer(this); // Clear cache
this.openInventory = null;
}
playerConnection.sendPacket(closeWindowPacket);
inventory.update();
this.didCloseInventory = true;
}
/**
* Used internally to prevent an inventory click to be processed
* when the inventory listeners closed the inventory.
*
* Should only be used within an inventory listener (event or condition).
*
* @return true if the inventory has been closed, false otherwise
*/
public boolean didCloseInventory() {
return didCloseInventory;
}
/**
* Used internally to reset the didCloseInventory field.
*
* Shouldn't be used externally without proper understanding of its consequence.
*
* @param didCloseInventory the new didCloseInventory field
*/
public void UNSAFE_changeDidCloseInventory(boolean didCloseInventory) {
this.didCloseInventory = didCloseInventory;
}
/**
* Gets the player viewable chunks.
*
* WARNING: adding or removing a chunk there will not load/unload it,
* use {@link Chunk#addViewer(Player)} or {@link Chunk#removeViewer(Player)}.
*
* @return a {@link Set} containing all the chunks that the player sees
*/
public Set
* Mostly unsafe since there is nothing to backup the value, used internally for creative players.
*
* @param flying the new flying field
* @see #setFlying(boolean) instead
*/
public void refreshFlying(boolean flying) {
this.flying = flying;
}
/**
* Gets if the player is allowed to fly.
*
* @return true if the player if allowed to fly, false otherwise
*/
public boolean isAllowFlying() {
return allowFlying;
}
/**
* Allows or forbid the player to fly.
*
* @param allowFlying should the player be allowed to fly
*/
public void setAllowFlying(boolean allowFlying) {
this.allowFlying = allowFlying;
refreshAbilities();
}
public boolean isInstantBreak() {
return instantBreak;
}
/**
* Changes the player ability "Creative Mode".
* see
*
* WARNING: this has nothing to do with {@link CustomBlock#getBreakDelay(Player, BlockPosition, byte, Set)}.
*
* @param instantBreak true to allow instant break
*/
public void setInstantBreak(boolean instantBreak) {
this.instantBreak = instantBreak;
refreshAbilities();
}
/**
* Gets the player flying speed.
*
* @return the flying speed of the player
*/
public float getFlyingSpeed() {
return flyingSpeed;
}
/**
* Updates the internal field and send a {@link PlayerAbilitiesPacket} with the new flying speed.
*
* @param flyingSpeed the new flying speed of the player
*/
public void setFlyingSpeed(float flyingSpeed) {
this.flyingSpeed = flyingSpeed;
refreshAbilities();
}
public float getWalkingSpeed() {
return walkingSpeed;
}
public void setWalkingSpeed(float walkingSpeed) {
this.walkingSpeed = walkingSpeed;
refreshAbilities();
}
/**
* This is the map used to send the statistic packet.
* It is possible to add/remove/change statistic value directly into it.
*
* @return the modifiable statistic map
*/
@NotNull
public Map
* Warning: could lead to have the player kicked because of a wrong keep alive packet.
*
* @param lastKeepAlive the new lastKeepAlive id
*/
public void refreshKeepAlive(long lastKeepAlive) {
this.lastKeepAlive = lastKeepAlive;
this.answerKeepAlive = false;
}
public boolean didAnswerKeepAlive() {
return answerKeepAlive;
}
public void refreshAnswerKeepAlive(boolean answerKeepAlive) {
this.answerKeepAlive = answerKeepAlive;
}
/**
* Changes the held item for the player viewers
* Also cancel eating if {@link #isEating()} was true.
*
* Warning: the player will not be noticed by this chance, only his viewers,
* see instead: {@link #setHeldItemSlot(byte)}.
*
* @param slot the new held slot
*/
public void refreshHeldSlot(byte slot) {
this.heldSlot = slot;
syncEquipment(EntityEquipmentPacket.Slot.MAIN_HAND);
refreshEating(false);
}
public void refreshEating(boolean isEating, long eatingTime) {
this.isEating = isEating;
if (isEating) {
this.startEatingTime = System.currentTimeMillis();
this.eatingTime = eatingTime;
} else {
this.startEatingTime = 0;
}
}
public void refreshEating(boolean isEating) {
refreshEating(isEating, defaultEatingTime);
}
/**
* Used to call {@link ItemUpdateStateEvent} with the proper item
* It does check which hand to get the item to update.
*
* @param allowFood true if food should be updated, false otherwise
* @return the called {@link ItemUpdateStateEvent},
* null if there is no item to update the state
*/
@Nullable
public ItemUpdateStateEvent callItemUpdateStateEvent(boolean allowFood) {
final Material mainHandMat = getItemInMainHand().getMaterial();
final Material offHandMat = getItemInOffHand().getMaterial();
final boolean isOffhand = offHandMat.hasState();
final ItemStack updatedItem = isOffhand ? getItemInOffHand() :
mainHandMat.hasState() ? getItemInMainHand() : null;
if (updatedItem == null) // No item with state, cancel
return null;
final boolean isFood = updatedItem.getMaterial().isFood();
if (isFood && !allowFood)
return null;
final Hand hand = isOffhand ? Hand.OFF : Hand.MAIN;
ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(this, hand, updatedItem);
callEvent(ItemUpdateStateEvent.class, itemUpdateStateEvent);
return itemUpdateStateEvent;
}
/**
* Makes the player digging a custom block, see {@link #resetTargetBlock()} to rewind.
*
* @param targetCustomBlock the custom block to dig
* @param targetBlockPosition the custom block position
* @param breakers the breakers of the block, can be null if {@code this} is the only breaker
*/
public void setTargetBlock(@NotNull CustomBlock targetCustomBlock, @NotNull BlockPosition targetBlockPosition,
@Nullable Set
* WARNING: this alone does not sync the player, please use {@link #addViewer(Player)}.
*
* @param connection the connection to show the player to
*/
protected void showPlayer(@NotNull PlayerConnection connection) {
SpawnPlayerPacket spawnPlayerPacket = new SpawnPlayerPacket();
spawnPlayerPacket.entityId = getEntityId();
spawnPlayerPacket.playerUuid = getUuid();
spawnPlayerPacket.position = getPosition();
connection.sendPacket(getAddPlayerToList());
connection.sendPacket(spawnPlayerPacket);
connection.sendPacket(getVelocityPacket());
connection.sendPacket(getMetadataPacket());
// Equipments synchronization
syncEquipments(connection);
if (hasPassenger()) {
connection.sendPacket(getPassengersPacket());
}
// Team
if (this.getTeam() != null)
connection.sendPacket(this.getTeam().createTeamsCreationPacket());
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
entityHeadLookPacket.yaw = position.getYaw();
connection.sendPacket(entityHeadLookPacket);
}
@NotNull
@Override
public ItemStack getItemInMainHand() {
return inventory.getItemInMainHand();
}
@Override
public void setItemInMainHand(@NotNull ItemStack itemStack) {
inventory.setItemInMainHand(itemStack);
}
@NotNull
@Override
public ItemStack getItemInOffHand() {
return inventory.getItemInOffHand();
}
@Override
public void setItemInOffHand(@NotNull ItemStack itemStack) {
inventory.setItemInOffHand(itemStack);
}
@NotNull
@Override
public ItemStack getHelmet() {
return inventory.getHelmet();
}
@Override
public void setHelmet(@NotNull ItemStack itemStack) {
inventory.setHelmet(itemStack);
}
@NotNull
@Override
public ItemStack getChestplate() {
return inventory.getChestplate();
}
@Override
public void setChestplate(@NotNull ItemStack itemStack) {
inventory.setChestplate(itemStack);
}
@NotNull
@Override
public ItemStack getLeggings() {
return inventory.getLeggings();
}
@Override
public void setLeggings(@NotNull ItemStack itemStack) {
inventory.setLeggings(itemStack);
}
@NotNull
@Override
public ItemStack getBoots() {
return inventory.getBoots();
}
@Override
public void setBoots(@NotNull ItemStack itemStack) {
inventory.setBoots(itemStack);
}
/**
* Represents the main or off hand of the player.
*/
public enum Hand {
MAIN,
OFF
}
public enum FacePoint {
FEET,
EYE
}
// Settings enum
/**
* Represents where is located the main hand of the player (can be changed in Minecraft option).
*/
public enum MainHand {
LEFT,
RIGHT
}
public enum ChatMode {
ENABLED,
COMMANDS_ONLY,
HIDDEN
}
public class PlayerSettings {
private String locale;
private byte viewDistance;
private ChatMode chatMode;
private boolean chatColors;
private byte displayedSkinParts;
private MainHand mainHand;
private boolean firstRefresh = true;
/**
* The player game language.
*
* @return the player locale
*/
public String getLocale() {
return locale;
}
/**
* Gets the player view distance.
*
* @return the player view distance
*/
public byte getViewDistance() {
return viewDistance;
}
/**
* Gets the player chat mode.
*
* @return the player chat mode
*/
public ChatMode getChatMode() {
return chatMode;
}
/**
* Gets if the player has chat colors enabled.
*
* @return true if chat colors are enabled, false otherwise
*/
public boolean hasChatColors() {
return chatColors;
}
public byte getDisplayedSkinParts() {
return displayedSkinParts;
}
/**
* Gets the player main hand.
*
* @return the player main hand
*/
public MainHand getMainHand() {
return mainHand;
}
/**
* Changes the player settings internally.
*
* WARNING: the player will not be noticed by this change, probably unsafe.
*
* @param locale the player locale
* @param viewDistance the player view distance
* @param chatMode the player chat mode
* @param chatColors the player chat colors
* @param displayedSkinParts the player displayed skin parts
* @param mainHand the player main hand
*/
public void refresh(String locale, byte viewDistance, ChatMode chatMode, boolean chatColors,
byte displayedSkinParts, MainHand mainHand) {
final boolean viewDistanceChanged = !firstRefresh && this.viewDistance != viewDistance;
this.locale = locale;
this.viewDistance = viewDistance;
this.chatMode = chatMode;
this.chatColors = chatColors;
this.displayedSkinParts = displayedSkinParts;
this.mainHand = mainHand;
sendMetadataIndex(16);
this.firstRefresh = false;
// Client changed his view distance in the settings
if (viewDistanceChanged) {
final Chunk playerChunk = getChunk();
if (playerChunk != null) {
refreshVisibleChunks(playerChunk);
}
}
}
}
}