blob: 8bf7906c1848c8951457e221bb589625c12493c3 [file] [log] [blame] [raw]
package net.glowstone.entity;
import com.destroystokyo.paper.Title;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.flowpowered.network.Message;
import com.flowpowered.network.util.ByteBufUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import net.glowstone.EventFactory;
import net.glowstone.GlowOfflinePlayer;
import net.glowstone.GlowWorld;
import net.glowstone.GlowWorldBorder;
import net.glowstone.block.GlowBlock;
import net.glowstone.block.ItemTable;
import net.glowstone.block.MaterialUtil;
import net.glowstone.block.blocktype.BlockBed;
import net.glowstone.block.entity.SignEntity;
import net.glowstone.block.flattening.generated.FlatteningUtil;
import net.glowstone.block.itemtype.ItemFood;
import net.glowstone.block.itemtype.ItemType;
import net.glowstone.chunk.ChunkManager.ChunkLock;
import net.glowstone.chunk.GlowChunk;
import net.glowstone.chunk.GlowChunk.Key;
import net.glowstone.command.LocalizedEnumNames;
import net.glowstone.constants.GameRules;
import net.glowstone.constants.GlowBlockEntity;
import net.glowstone.constants.GlowEffect;
import net.glowstone.constants.GlowParticle;
import net.glowstone.constants.GlowSound;
import net.glowstone.entity.meta.ClientSettings;
import net.glowstone.entity.meta.MetadataIndex;
import net.glowstone.entity.meta.MetadataIndex.StatusFlags;
import net.glowstone.entity.meta.MetadataMap;
import net.glowstone.entity.meta.profile.GlowPlayerProfile;
import net.glowstone.entity.monster.GlowBoss;
import net.glowstone.entity.objects.GlowItem;
import net.glowstone.entity.passive.GlowFishingHook;
import net.glowstone.i18n.GlowstoneMessages;
import net.glowstone.inventory.GlowInventory;
import net.glowstone.inventory.GlowInventoryView;
import net.glowstone.inventory.InventoryMonitor;
import net.glowstone.inventory.ToolType;
import net.glowstone.inventory.crafting.PlayerRecipeMonitor;
import net.glowstone.io.PlayerDataService.PlayerReader;
import net.glowstone.map.GlowMapCanvas;
import net.glowstone.net.GlowSession;
import net.glowstone.net.message.play.entity.AnimateEntityMessage;
import net.glowstone.net.message.play.entity.DestroyEntitiesMessage;
import net.glowstone.net.message.play.entity.EntityMetadataMessage;
import net.glowstone.net.message.play.entity.EntityVelocityMessage;
import net.glowstone.net.message.play.entity.SetPassengerMessage;
import net.glowstone.net.message.play.game.BlockBreakAnimationMessage;
import net.glowstone.net.message.play.game.BlockChangeMessage;
import net.glowstone.net.message.play.game.ChatMessage;
import net.glowstone.net.message.play.game.ExperienceMessage;
import net.glowstone.net.message.play.game.HealthMessage;
import net.glowstone.net.message.play.game.JoinGameMessage;
import net.glowstone.net.message.play.game.MapDataMessage;
import net.glowstone.net.message.play.game.MultiBlockChangeMessage;
import net.glowstone.net.message.play.game.NamedSoundEffectMessage;
import net.glowstone.net.message.play.game.PlayEffectMessage;
import net.glowstone.net.message.play.game.PlayParticleMessage;
import net.glowstone.net.message.play.game.PluginMessage;
import net.glowstone.net.message.play.game.PositionRotationMessage;
import net.glowstone.net.message.play.game.RespawnMessage;
import net.glowstone.net.message.play.game.SignEditorMessage;
import net.glowstone.net.message.play.game.SpawnPositionMessage;
import net.glowstone.net.message.play.game.StateChangeMessage;
import net.glowstone.net.message.play.game.StateChangeMessage.Reason;
import net.glowstone.net.message.play.game.TimeMessage;
import net.glowstone.net.message.play.game.TitleMessage;
import net.glowstone.net.message.play.game.TitleMessage.Action;
import net.glowstone.net.message.play.game.UnloadChunkMessage;
import net.glowstone.net.message.play.game.UpdateBlockEntityMessage;
import net.glowstone.net.message.play.game.UpdateSignMessage;
import net.glowstone.net.message.play.game.UserListHeaderFooterMessage;
import net.glowstone.net.message.play.game.UserListItemMessage;
import net.glowstone.net.message.play.game.UserListItemMessage.Entry;
import net.glowstone.net.message.play.inv.CloseWindowMessage;
import net.glowstone.net.message.play.inv.OpenWindowMessage;
import net.glowstone.net.message.play.inv.SetWindowContentsMessage;
import net.glowstone.net.message.play.inv.SetWindowSlotMessage;
import net.glowstone.net.message.play.inv.WindowPropertyMessage;
import net.glowstone.net.message.play.player.ResourcePackSendMessage;
import net.glowstone.net.message.play.player.UseBedMessage;
import net.glowstone.scoreboard.GlowScoreboard;
import net.glowstone.scoreboard.GlowTeam;
import net.glowstone.util.Convert;
import net.glowstone.util.EntityUtils;
import net.glowstone.util.InventoryUtil;
import net.glowstone.util.Position;
import net.glowstone.util.StatisticMap;
import net.glowstone.util.TextMessage;
import net.glowstone.util.TickUtil;
import net.glowstone.util.nbt.CompoundTag;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import org.bukkit.Achievement;
import org.bukkit.BanList;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Difficulty;
import org.bukkit.Effect;
import org.bukkit.Effect.Type;
import org.bukkit.GameMode;
import org.bukkit.Instrument;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Note;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.Statistic;
import org.bukkit.WeatherType;
import org.bukkit.World;
import org.bukkit.World.Environment;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.boss.BossBar;
import org.bukkit.configuration.serialization.DelegateDeserialization;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Villager;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerAchievementAwardedEvent;
import org.bukkit.event.player.PlayerBedEnterEvent;
import org.bukkit.event.player.PlayerBedLeaveEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerExpChangeEvent;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerLevelChangeEvent;
import org.bukkit.event.player.PlayerLocaleChangeEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerRegisterChannelEvent;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerStatisticIncrementEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.event.player.PlayerToggleSprintEvent;
import org.bukkit.event.player.PlayerUnregisterChannelEvent;
import org.bukkit.event.player.PlayerVelocityEvent;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.InventoryView.Property;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MainHand;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.Recipe;
import org.bukkit.map.MapView;
import org.bukkit.material.MaterialData;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.messaging.StandardMessenger;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.util.BlockVector;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.json.simple.JSONObject;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static net.glowstone.GlowServer.logger;
/**
* Represents an in-game player.
*
* @author Graham Edgecombe
*/
@DelegateDeserialization(GlowOfflinePlayer.class)
public class GlowPlayer extends GlowHumanEntity implements Player {
/**
* A static entity id to use when telling the client about itself.
*/
public static final int SELF_ID = 0;
public static final int HOOK_MAX_DISTANCE = 32;
private static final Achievement[] ACHIEVEMENT_VALUES = Achievement.values();
private static final LocalizedEnumNames<Achievement> ACHIEVEMENT_NAMES
= new LocalizedEnumNames<Achievement>(
(Function<String, Achievement>) Achievement::valueOf,
"glowstone.achievement.unknown",
null, "maps/achievement", true);
/**
* The network session attached to this player.
*
* @return The GlowSession of the player.
*/
@Getter
private final GlowSession session;
/**
* The entities that the client knows about. Guarded by {@link #worldLock}.
*/
private final Set<GlowEntity> knownEntities = new HashSet<>();
/**
* The entities that are hidden from the client.
*/
private final Set<UUID> hiddenEntities = new HashSet<>();
/**
* The chunks that the client knows about.
*/
private final Set<Key> knownChunks = new HashSet<>();
/**
* A queue of BlockChangeMessages to be sent.
*/
private final Queue<BlockChangeMessage> blockChanges = new ConcurrentLinkedDeque<>();
/**
* A queue of messages that should be sent after block changes are processed.
*
* <p>Used for sign updates and other situations where the block must be sent first.
*/
private final List<Message> afterBlockChanges = new LinkedList<>();
/**
* The set of plugin channels this player is listening on.
*/
private final Set<String> listeningChannels = new HashSet<>();
/**
* The player's statistics, achievements, and related data.
*/
private final StatisticMap stats = new StatisticMap();
/**
* Whether the player has played before (will be false on first join).
*/
private final boolean hasPlayedBefore;
/**
* The time the player first played, or 0 if unknown.
*/
@Getter
private final long firstPlayed;
/**
* The time the player last played, or 0 if unknown.
*/
@Getter
private final long lastPlayed;
@Getter
private final PlayerRecipeMonitor recipeMonitor;
public Location teleportedTo = null;
@Setter
public boolean affectsSpawning = true;
/**
* The time the player joined, in milliseconds, to be saved as last played time.
*
* @return The player's join time.
*/
@Getter
private long joinTime;
/**
* The settings sent by the client.
*/
private ClientSettings settings = ClientSettings.DEFAULT;
/**
* The lock used to prevent chunks from unloading near the player.
*/
private ChunkLock chunkLock;
/**
* The tracker for changes to the currently open inventory.
*/
private InventoryMonitor invMonitor;
/**
* The display name of this player, for chat purposes.
*/
private String displayName;
/**
* The name a player has in the player list.
*/
private String playerListName;
/**
* Cumulative amount of experience points the player has collected.
*/
@Getter
private int totalExperience;
/**
* The current level (or skill point amount) of the player.
*/
@Getter
private int level;
/**
* The progress made to the next level, from 0 to 1.
*/
@Getter
private float exp;
/**
* The human entity's current food level.
*/
@Getter
private int foodLevel = 20;
/**
* The player's current exhaustion level.
*/
@Getter
@Setter
private float exhaustion;
/**
* The player's current saturation level.
*/
@Getter
private float saturation;
/**
* Whether to perform special scaling of the player's health.
*/
@Getter
private boolean healthScaled;
/**
* The scale at which to display the player's health.
*/
@Getter
private double healthScale = 20;
/**
* If this player has seen the end credits.
*/
@Getter
@Setter
private boolean seenCredits;
/**
* Recipes this player has unlocked.
*/
private Collection<Recipe> recipes = new HashSet<>();
/**
* This player's current time offset.
*/
private long timeOffset;
/**
* Whether the time offset is relative.
*/
@Getter
private boolean playerTimeRelative = true;
/**
* The player-specific weather, or null for normal weather.
*/
private WeatherType playerWeather;
/**
* The player's compass target.
*/
@Getter
private Location compassTarget;
/**
* Whether this player's sleeping state is ignored when changing time.
*/
private boolean sleepingIgnored;
/**
* The bed in which the player currently lies.
*/
private GlowBlock bed;
/**
* The bed spawn location of a player.
*/
private Location bedSpawn;
/**
* Whether to use the bed spawn even if there is no bed block.
*
* @return Whether the player is forced to spawn at their bed.
*/
@Getter
private boolean bedSpawnForced;
private final Player.Spigot spigot = new Player.Spigot() {
@Deprecated
public void playEffect(Location location, Effect effect, int id, int data, float offsetX,
float offsetY, float offsetZ, float speed, int particleCount, int radius) {
if (effect.getType() == Type.VISUAL && particleCount > 0) {
MaterialData material = new MaterialData(FlatteningUtil.getMaterialFromBaseId(id),
(byte) data);
showParticle(location, effect, material, offsetX, offsetY, offsetZ, speed,
particleCount);
} else {
GlowPlayer.this.playEffect(location, effect, data);
}
}
@Override
public InetSocketAddress getRawAddress() {
return session.getAddress();
}
@Override
public void respawn() {
GlowPlayer.this.respawn();
}
@Override
public boolean getCollidesWithEntities() {
return isCollidable();
}
@Override
public void setCollidesWithEntities(boolean collides) {
setCollidable(collides);
}
@Override
public Set<Player> getHiddenPlayers() {
return hiddenEntities.stream().map(Bukkit::getPlayer).filter(Objects::nonNull)
.collect(Collectors.toSet());
}
@Override
public void sendMessage(ChatMessageType position, BaseComponent... components) {
GlowPlayer.this.sendMessage(position, components);
}
@Override
public void sendMessage(ChatMessageType position, BaseComponent component) {
GlowPlayer.this.sendMessage(position, component);
}
@Override
public void sendMessage(BaseComponent... components) {
GlowPlayer.this.sendMessage(components);
}
@Override
public void sendMessage(BaseComponent component) {
GlowPlayer.this.sendMessage(component);
}
@Override
public String getLocale() {
return GlowPlayer.this.getLocale();
}
};
/**
* The location of the sign the player is currently editing, or null.
*/
private Location signLocation;
/**
* Whether the player is permitted to fly.
*/
private boolean canFly;
/**
* Whether the player is currently flying.
*/
@Getter
private boolean flying;
/**
* The player's base flight speed.
*/
@Getter
private float flySpeed = 0.1f;
/**
* The player's base walking speed.
*/
@Getter
private float walkSpeed = 0.2f;
/**
* The scoreboard the player is currently subscribed to.
*/
private GlowScoreboard scoreboard;
/**
* The player's current title, if any.
*/
private Title.Builder currentTitle = new Title.Builder();
/**
* The one block the player is currently digging.
*/
@Getter
private GlowBlock digging;
/**
* The number of ticks elapsed since the player started digging.
*/
private long diggingTicks = 0;
/**
* The total number of ticks needed to dig the current block.
*/
private long totalDiggingTicks = Long.MAX_VALUE;
/**
* The one itemstack the player is currently usage and associated time.
*/
@Getter
@Setter
private ItemStack usageItem;
@Getter
private int usageTime;
@Getter
private int startingUsageTime;
private Entity spectating;
private HashMap<Advancement, AdvancementProgress> advancements;
private String resourcePackHash;
private PlayerResourcePackStatusEvent.Status resourcePackStatus;
private List<Conversation> conversations = new ArrayList<>();
private Set<BossBar> bossBars = ConcurrentHashMap.newKeySet();
/**
* The player's previous chunk x coordinate.
*/
private int prevCentralX;
/**
* The player's previous chunk x coordinate.
*/
private int prevCentralZ;
/**
* If this is the player's first time getting blocks streamed.
*/
private boolean firstStream = true;
/**
* If we should force block streaming regardless of chunk difference.
*/
private boolean forceStream = false;
/**
* Current casted fishing hook.
*/
private final AtomicReference<GlowFishingHook> currentFishingHook = new AtomicReference<>(null);
/**
* The player's ender pearl cooldown game tick counter.
* 1 second, or 20 game ticks by default.
* The player can use ender pearl again if equals 0.
*/
@Getter
@Setter
private int enderPearlCooldown = 0;
@Getter
@Setter
@Nullable
private String playerListHeader;
@Getter
@Setter
@Nullable
private String playerListFooter;
/**
* Returns the current fishing hook.
*
* @return the current fishing hook, or null if not fishing
*/
public GlowFishingHook getCurrentFishingHook() {
return currentFishingHook.get();
}
/**
* Creates a new player and adds it to the world.
*
* @param session The player's session.
* @param profile The player's profile with name and UUID information.
* @param reader The PlayerReader to be used to initialize the player.
*/
public GlowPlayer(GlowSession session, GlowPlayerProfile profile, PlayerReader reader) {
super(initLocation(session, reader), profile);
setBoundingBox(0.6, 1.8);
this.session = session;
chunkLock = world.newChunkLock(getName());
// read data from player reader
hasPlayedBefore = reader.hasPlayedBefore();
if (hasPlayedBefore) {
firstPlayed = reader.getFirstPlayed();
lastPlayed = reader.getLastPlayed();
bedSpawn = reader.getBedSpawnLocation();
} else {
firstPlayed = 0;
lastPlayed = 0;
}
//creates InventoryMonitor to avoid NullPointerException
invMonitor = new InventoryMonitor(getOpenInventory());
server.getPlayerStatisticIoService().readStatistics(this);
recipeMonitor = new PlayerRecipeMonitor(this);
updateBossBars();
}
////////////////////////////////////////////////////////////////////////////
// Internals
/**
* Read the location from a PlayerReader for entity initialization.
*
* <p>Will fall back to a reasonable default rather than returning null.
*
* @param session The player's session.
* @param reader The PlayerReader to get the location from.
* @return The location to spawn the player.
*/
private static Location initLocation(GlowSession session, PlayerReader reader) {
if (reader.hasPlayedBefore()) {
Location loc = reader.getLocation();
if (loc != null) {
return loc;
}
}
return findSafeSpawnLocation(session.getServer().getWorlds().get(0).getSpawnLocation());
}
/**
* Find a a Location obove or below the specified Location, which is on ground.
*
* <p>The returned Location will be at the center of the block, X and Y wise.
*
* @param spawn The Location a safe spawn position should be found at.
* @return The location to spawn the player at.
*/
private static Location findSafeSpawnLocation(Location spawn) {
World world = spawn.getWorld();
int blockX = spawn.getBlockX();
int blockY = spawn.getBlockY();
int blockZ = spawn.getBlockZ();
int highestY = world.getHighestBlockYAt(blockX, blockZ);
int y = blockY;
boolean wasPreviousSafe = false;
for (; y <= highestY; y++) {
Material type = world.getBlockAt(blockX, y, blockZ).getType();
boolean safe = Material.AIR.equals(type);
if (wasPreviousSafe && safe) {
y--;
break;
}
wasPreviousSafe = safe;
}
return new Location(world, blockX + 0.5, y, blockZ + 0.5);
}
/**
* Loads the player's state and sends the messages that are necessary on login.
*
* @param session the player's session
* @param reader the source of the player's saved state
*/
public void join(GlowSession session, PlayerReader reader) {
// send join game
// in future, handle hardcore, difficulty, and level type
String type = world.getWorldType().getName().toLowerCase();
int gameMode = getGameMode().getValue();
if (server.isHardcore()) {
gameMode |= 0x8;
}
session.send(new JoinGameMessage(SELF_ID, gameMode, world.getEnvironment().getId(), world
.getDifficulty().getValue(), session.getServer().getMaxPlayers(), type, world
.getGameRuleMap().getBoolean(GameRules.REDUCED_DEBUG_INFO)));
setGameModeDefaults();
// send server brand and supported plugin channels
Message pluginMessage = PluginMessage.fromString("minecraft:brand", server.getName());
if (pluginMessage != null) {
session.send(pluginMessage);
}
sendSupportedChannels();
joinTime = System.currentTimeMillis();
reader.readData(this);
reader.close();
// Add player to list of online players
getServer().setPlayerOnline(this, true);
// save data back out
saveData();
streamBlocks(); // stream the initial set of blocks
sendWeather();
sendRainDensity();
sendSkyDarkness();
getServer().sendPlayerAbilities(this);
// send initial location
session.send(new PositionRotationMessage(location));
session.send(((GlowWorldBorder) world.getWorldBorder()).createMessage());
sendTime();
setCompassTarget(world.getSpawnLocation()); // set our compass target
scoreboard = server.getScoreboardManager().getMainScoreboard();
scoreboard.subscribe(this);
invMonitor = new InventoryMonitor(getOpenInventory());
updateInventory(); // send inventory contents
session.send(recipeMonitor.createInitMessage());
if (!server.getResourcePackUrl().isEmpty()) {
setResourcePack(server.getResourcePackUrl(), server.getResourcePackHash());
}
}
@Override
public String toString() {
return "GlowPlayer{name=" + getName() + "}";
}
@Override
public void damage(double amount) {
damage(amount, DamageCause.CUSTOM);
}
@Override
public void damage(double amount, Entity cause) {
super.damage(amount, cause);
sendHealth();
}
@Override
public void damage(double amount, Entity source, @NotNull DamageCause cause) {
boolean pvpAllowed = server.isPvpEnabled() && world.getPVP();
if (!pvpAllowed) {
if (source instanceof Player) {
return;
}
if (cause == DamageCause.PROJECTILE && source instanceof Projectile) {
Projectile projectile = (Projectile) source;
if (projectile.getShooter() instanceof Player) {
return;
}
}
}
super.damage(amount, source, cause);
}
@Override
public void damage(double amount, DamageCause cause) {
// todo: better idea
double old = getHealth();
super.damage(amount, cause);
if (old != getHealth()) {
addExhaustion(0.1f);
sendHealth();
incrementStatistic(Statistic.DAMAGE_TAKEN, (int) Math.round(amount));
}
}
@Override
public boolean canTakeDamage(DamageCause damageCause) {
return damageCause == DamageCause.FALL ? !getAllowFlight() && super
.canTakeDamage(damageCause) : super.canTakeDamage(damageCause);
}
/**
* Kicks this player.
*/
@Override
public void remove() {
knownChunks.clear();
chunkLock.clear();
saveData();
getInventory().removeViewer(this);
getInventory().getCraftingInventory().removeViewer(this);
permissions.clearPermissions();
getServer().setPlayerOnline(this, false);
getWorld().getRawPlayers().remove(this);
if (scoreboard != null) {
scoreboard.unsubscribe(this);
scoreboard = null;
}
clearBossBars();
super.remove();
}
/**
* Handle player disconnection.
*
* @param async if true, the player's data is saved asynchronously
*/
public void remove(boolean async) {
knownChunks.clear();
chunkLock.clear();
saveData(async);
getInventory().removeViewer(this);
getInventory().getCraftingInventory().removeViewer(this);
permissions.clearPermissions();
getServer().setPlayerOnline(this, false);
if (scoreboard != null) {
scoreboard.unsubscribe(this);
scoreboard = null;
}
clearBossBars();
super.remove();
}
@Override
public boolean shouldSave() {
return false;
}
@Override
public void pulse() {
super.pulse();
incrementStatistic(Statistic.TIME_SINCE_DEATH);
if (usageItem != null) {
if (usageItem.equals(getItemInHand())) { //todo: implement offhand
if (--usageTime == 0) {
ItemType item = ItemTable.instance().getItem(usageItem.getType());
if (item instanceof ItemFood) {
((ItemFood) item).eat(this, usageItem);
}
}
} else {
usageItem = null;
usageTime = 0;
}
}
if (digging != null) {
pulseDigging();
}
if (exhaustion > 4.0f) {
exhaustion -= 4.0f;
if (saturation > 0f) {
saturation = Math.max(saturation - 1f, 0f);
sendHealth();
} else if (world.getDifficulty() != Difficulty.PEACEFUL) {
FoodLevelChangeEvent event = EventFactory.getInstance()
.callEvent(new FoodLevelChangeEvent(this, Math.max(foodLevel - 1, 0)));
if (!event.isCancelled()) {
foodLevel = event.getFoodLevel();
}
sendHealth();
}
}
if (getHealth() < getMaxHealth() && !isDead()) {
if (foodLevel >= 18 && ticksLived % 80 == 0
|| world.getDifficulty() == Difficulty.PEACEFUL) {
EntityUtils.heal(this, 1, EntityRegainHealthEvent.RegainReason.SATIATED);
exhaustion = Math.min(exhaustion + 3.0f, 40.0f);
saturation -= 3;
}
}
// Process food level and starvation based on difficulty.
switch (world.getDifficulty()) {
case PEACEFUL: {
if (foodLevel < 20 && ticksLived % 20 == 0) {
foodLevel++;
}
break;
}
case EASY: {
if (foodLevel == 0 && getHealth() > 10 && ticksLived % 80 == 0) {
damage(1, DamageCause.STARVATION);
}
break;
}
case NORMAL: {
if (foodLevel == 0 && getHealth() > 1 && ticksLived % 80 == 0) {
damage(1, DamageCause.STARVATION);
}
break;
}
case HARD: {
if (foodLevel == 0 && ticksLived % 80 == 0) {
damage(1, DamageCause.STARVATION);
}
break;
}
default: {
// Do nothing when there are other game difficulties.
}
}
// process ender pearl cooldown, decrease by 1 every game tick.
if (enderPearlCooldown > 0) {
enderPearlCooldown--;
}
// stream world
streamBlocks();
processBlockChanges();
// add to playtime (despite inaccurate name, this counts ticks rather than minutes)
incrementStatistic(Statistic.PLAY_ONE_MINUTE);
if (isSneaking()) {
incrementStatistic(Statistic.SNEAK_TIME);
}
// update inventory
for (InventoryMonitor.Entry entry : invMonitor.getChanges()) {
sendItemChange(entry.slot, entry.item);
}
// send changed metadata
List<MetadataMap.Entry> changes = metadata.getChanges();
if (!changes.isEmpty()) {
session.send(new EntityMetadataMessage(SELF_ID, changes));
}
// Entity IDs are only unique per world, so we can't spawn or teleport between worlds while
// updating them.
worldLock.writeLock().lock();
try {
// update or remove entities
List<GlowEntity> destroyEntities = new LinkedList<>();
for (Iterator<GlowEntity> it = knownEntities.iterator(); it.hasNext(); ) {
GlowEntity entity = it.next();
if (!isWithinDistance(entity) || entity.isRemoved()) {
destroyEntities.add(entity);
} else {
entity.createUpdateMessage(session).forEach(session::send);
}
}
if (!destroyEntities.isEmpty()) {
List<Integer> destroyIds = new ArrayList(destroyEntities.size());
for (GlowEntity entity : destroyEntities) {
knownEntities.remove(entity);
destroyIds.add(entity.getEntityId());
}
session.send(new DestroyEntitiesMessage(destroyIds));
}
// add entities
knownChunks.forEach(key ->
world.getChunkAt(key.getX(), key.getZ()).getRawEntities().stream()
.filter(entity -> this != entity
&& isWithinDistance(entity)
&& !entity.isDead()
&& !knownEntities.contains(entity)
&& !hiddenEntities.contains(entity.getUniqueId()))
.forEach((entity) -> Bukkit.getScheduler()
.runTaskAsynchronously(null, () -> {
worldLock.readLock().lock();
try {
knownEntities.add(entity);
} finally {
worldLock.readLock().unlock();
}
entity.createSpawnMessage().forEach(session::send);
entity.createAfterSpawnMessage(session)
.forEach(session::send);
})));
} finally {
worldLock.writeLock().unlock();
}
if (passengerChanged) {
session.send(new SetPassengerMessage(SELF_ID, getPassengers().stream()
.mapToInt(Entity::getEntityId).toArray()));
}
getAttributeManager().sendMessages(session);
GlowFishingHook hook = currentFishingHook.get();
if (hook != null) {
// The line will disappear if the player wanders more than 32 blocks away from the
// bobber, or if the player stops holding a fishing rod.
if (getInventory().getItemInMainHand().getType() != Material.FISHING_ROD
&& getInventory().getItemInOffHand().getType() != Material.FISHING_ROD) {
setCurrentFishingHook(null);
}
if (hook.location.distanceSquared(location) > HOOK_MAX_DISTANCE * HOOK_MAX_DISTANCE) {
setCurrentFishingHook(null);
}
}
}
@Override
protected void pulsePhysics() {
// trust the client with physics
// just update the bounding box
updateBoundingBox();
}
@Override
protected void jump() {
// don't make the client jump, please
}
/**
* Process and send pending BlockChangeMessages.
*/
private void processBlockChanges() {
// separate messages by chunk
// inner map is used to only send one entry for same coordinates
Map<Key, Map<BlockVector, BlockChangeMessage>> chunks = new HashMap<>();
while (true) {
BlockChangeMessage message = blockChanges.poll();
if (message == null) {
break;
}
Key key = GlowChunk.Key.of(message.getX() >> 4, message.getZ() >> 4);
if (canSeeChunk(key)) {
Map<BlockVector, BlockChangeMessage> map = chunks
.computeIfAbsent(key, k -> new HashMap<>());
map.put(new BlockVector(message.getX(), message.getY(), message
.getZ()), message);
}
}
// send away
for (Map.Entry<Key, Map<BlockVector, BlockChangeMessage>> entry : chunks.entrySet()) {
Key key = entry.getKey();
List<BlockChangeMessage> value = new ArrayList<>(entry.getValue().values());
if (value.size() == 1) {
session.send(value.get(0));
} else if (value.size() > 1) {
session.send(new MultiBlockChangeMessage(key.getX(), key.getZ(), value));
}
}
// now send post-block-change messages
List<Message> postMessages = new ArrayList<>(afterBlockChanges);
afterBlockChanges.clear();
postMessages.forEach(session::send);
}
/**
* Streams chunks to the player's client.
*/
private void streamBlocks() {
Set<Key> previousChunks = null;
ArrayList<Key> newChunks = new ArrayList<>();
int centralX = location.getBlockX() >> 4;
int centralZ = location.getBlockZ() >> 4;
int radius = Math.min(server.getViewDistance(), 1 + settings.getViewDistance());
if (firstStream) {
firstStream = false;
for (int x = centralX - radius; x <= centralX + radius; x++) {
for (int z = centralZ - radius; z <= centralZ + radius; z++) {
newChunks.add(GlowChunk.Key.of(x, z));
}
}
} else if (Math.abs(centralX - prevCentralX) > radius
|| Math.abs(centralZ - prevCentralZ) > radius) {
knownChunks.clear();
for (int x = centralX - radius; x <= centralX + radius; x++) {
for (int z = centralZ - radius; z <= centralZ + radius; z++) {
newChunks.add(GlowChunk.Key.of(x, z));
}
}
} else if (forceStream || prevCentralX != centralX || prevCentralZ != centralZ) {
previousChunks = new HashSet<>(knownChunks);
for (int x = centralX - radius; x <= centralX + radius; x++) {
for (int z = centralZ - radius; z <= centralZ + radius; z++) {
Key key = GlowChunk.Key.of(x, z);
if (knownChunks.contains(key)) {
previousChunks.remove(key);
} else {
newChunks.add(key);
}
}
}
} else {
return; // early end if there's no changes
}
prevCentralX = centralX;
prevCentralZ = centralZ;
// sort chunks by distance from player - closer chunks sent first
newChunks.sort((a, b) -> {
double dx = 16 * a.getX() + 8 - location.getX();
double dz = 16 * a.getZ() + 8 - location.getZ();
double da = dx * dx + dz * dz;
dx = 16 * b.getX() + 8 - location.getX();
dz = 16 * b.getZ() + 8 - location.getZ();
double db = dx * dx + dz * dz;
return Double.compare(da, db);
});
// populate then send chunks to the player
// done in two steps so that all the new chunks are finalized before any of them are sent
// this prevents sending a chunk then immediately sending block changes in it because
// one of its neighbors has populated
// first step: force population then acquire lock on each chunk
newChunks.forEach(newChunk -> {
world.getChunkManager().forcePopulation(newChunk.getX(), newChunk.getZ());
knownChunks.add(newChunk);
chunkLock.acquire(newChunk);
});
boolean skylight = world.getEnvironment() == Environment.NORMAL;
newChunks.stream().map(key -> world.getChunkAt(key.getX(), key.getZ()).toMessage(skylight))
.forEach(session::send);
// send visible block entity data
newChunks.stream().flatMap(key -> world.getChunkAt(key.getX(),
key.getZ()).getRawBlockEntities().stream())
.forEach(entity -> entity.update(this));
// and remove old chunks
if (previousChunks != null) {
previousChunks.forEach(key -> {
session.send(new UnloadChunkMessage(key.getX(), key.getZ()));
knownChunks.remove(key);
chunkLock.release(key);
});
previousChunks.clear();
}
}
/**
* Spawn the player at the given location after they have already joined.
*
* <p>Used for changing worlds and respawning after death.
*
* @param location The location to place the player.
*/
private void spawnAt(Location location) {
GlowWorld oldWorld;
// switch worlds
worldLock.writeLock().lock();
try {
oldWorld = world;
world.getEntityManager().unregister(this);
world = (GlowWorld) location.getWorld();
world.getEntityManager().register(this);
updateBossBars();
} finally {
worldLock.writeLock().unlock();
}
// switch chunk set
// no need to send chunk unload messages - respawn unloads all chunks
knownChunks.clear();
chunkLock.clear();
chunkLock = world.newChunkLock(getName());
// spawn into world
String type = world.getWorldType().getName().toLowerCase();
session.send(new RespawnMessage(world.getEnvironment().getId(), world.getDifficulty()
.getValue(), getGameMode().getValue(), type));
setRawLocation(location, false); // take us to spawn position
session.send(new PositionRotationMessage(location));
teleportedTo = location.clone();
setCompassTarget(world.getSpawnLocation()); // set our compass target
streamBlocks(); // stream blocks
sendWeather();
sendRainDensity();
sendSkyDarkness();
sendTime();
updateInventory();
// fire world change if needed
if (oldWorld != world) {
session.send(((GlowWorldBorder) world.getWorldBorder()).createMessage());
EventFactory.getInstance().callEvent(new PlayerChangedWorldEvent(this, oldWorld));
}
}
/**
* Remove all boss bars, then add back the ones whose world we're in.
*/
private void updateBossBars() {
clearBossBars();
worldLock.readLock().lock();
try {
for (GlowBoss boss : world.getEntitiesByClass(GlowBoss.class)) {
boss.addBarToPlayer(this);
}
} finally {
worldLock.readLock().unlock();
}
}
private void clearBossBars() {
for (BossBar bar : bossBars) {
removeBossBar(bar);
}
}
/**
* Respawn the player after they have died.
*/
public void respawn() {
if (!isDead()) {
return;
}
// restore health
setHealth(getMaxHealth());
setFoodLevel(20);
// reset fire ticks
setFireTicks(0);
worldLock.writeLock().lock();
try {
// determine spawn destination
boolean spawnAtBed = true;
Location dest = getBedSpawnLocation();
if (dest == null) {
dest = world.getSpawnLocation();
spawnAtBed = false;
if (bedSpawn != null) {
setBedSpawnLocation(null);
sendMessage("Your home bed was missing or obstructed");
}
}
if (!spawnAtBed) {
dest = findSafeSpawnLocation(dest);
}
// fire event and perform spawn
PlayerRespawnEvent event = new PlayerRespawnEvent(this, dest, spawnAtBed);
EventFactory.getInstance().callEvent(event);
if (event.getRespawnLocation().getWorld().equals(getWorld()) && !knownEntities
.isEmpty()) {
// we need to manually reset all known entities if the player respawns in the
// same world
List<Integer> entityIds = new ArrayList<>(knownEntities.size());
entityIds.addAll(knownEntities.stream().map(GlowEntity::getEntityId)
.collect(Collectors.toList()));
session.send(new DestroyEntitiesMessage(entityIds));
knownEntities.clear();
}
active = true;
deathTicks = 0;
setStatistic(Statistic.TIME_SINCE_DEATH, 0);
spawnAt(event.getRespawnLocation());
} finally {
worldLock.writeLock().unlock();
}
// just in case any items are left in their inventory after they respawn
updateInventory();
}
/**
* Checks whether the player can see the given chunk.
*
* @param chunk The chunk to check.
* @return If the chunk is known to the player's client.
*/
public boolean canSeeChunk(Key chunk) {
return knownChunks.contains(chunk);
}
/**
* Checks whether the player can see the given entity.
*
* @param entity The entity to check.
* @return If the entity is known to the player's client.
*/
public boolean canSeeEntity(GlowEntity entity) {
worldLock.readLock().lock();
try {
return knownEntities.contains(entity);
} finally {
worldLock.readLock().unlock();
}
}
////////////////////////////////////////////////////////////////////////////
// Basic stuff
/**
* Open the sign editor interface at the specified location.
*
* @param loc The location to open the editor at
*/
public void openSignEditor(Location loc) {
signLocation = loc.clone();
signLocation.setX(loc.getBlockX());
signLocation.setY(loc.getBlockY());
signLocation.setZ(loc.getBlockZ());
signLocation.setYaw(0);
signLocation.setPitch(0);
// Client closes inventory when sign editor is opened
if (!GlowInventoryView.isDefault(getOpenInventory())) {
closeInventory();
}
session.send(new SignEditorMessage(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
}
/**
* Check that the specified location matches that of the last opened sign editor, and if so,
* clears the last opened sign editor.
*
* @param loc The location to check
* @return Whether the location matched.
*/
public boolean checkSignLocation(Location loc) {
if (loc.equals(signLocation)) {
signLocation = null;
return true;
} else {
return false;
}
}
/**
* Get a UserListItemMessage entry representing adding this player.
*
* @return The entry (action ADD_PLAYER) with this player's information.
*/
public Entry getUserListEntry() {
TextMessage displayName = null;
if (playerListName != null && !playerListName.isEmpty()) {
displayName = new TextMessage(playerListName);
}
return UserListItemMessage.add(getProfile(), getGameMode().getValue(), 0, displayName);
}
/**
* Send a UserListItemMessage to every player that can see this player.
*
* @param updateMessage The message to send.
*/
private void updateUserListEntries(UserListItemMessage updateMessage) {
server.getRawOnlinePlayers().stream().filter(player -> player.canSee(this))
.forEach(player -> player.getSession().send(updateMessage));
}
@Override
public void setVelocity(Vector velocity) {
PlayerVelocityEvent event = EventFactory.getInstance()
.callEvent(new PlayerVelocityEvent(this, velocity));
if (!event.isCancelled()) {
velocity = event.getVelocity();
super.setVelocity(velocity);
session.send(new EntityVelocityMessage(SELF_ID, velocity));
}
}
@Override
public @NotNull BoundingBox getBoundingBox() {
return null;
}
@Override
public void setRotation(float yaw, float pitch) {
}
/**
* Set this player's client settings.
*
* @param settings The settings to set.
*/
public void setSettings(ClientSettings settings) {
String newLocale = settings.getLocale();
if (!newLocale.equalsIgnoreCase(this.settings.getLocale())) {
EventFactory.getInstance().callEvent(new PlayerLocaleChangeEvent(this, newLocale));
}
forceStream = settings.getViewDistance() != this.settings.getViewDistance()
&& settings.getViewDistance() + 1 <= server.getViewDistance();
this.settings = settings;
metadata.set(MetadataIndex.PLAYER_SKIN_PARTS, settings.getSkinFlags());
metadata.set(MetadataIndex.PLAYER_MAIN_HAND, settings.getMainHand());
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> ret = new HashMap<>();
ret.put("name", getName()); // NON-NLS
return ret;
}
@Override
public EntityType getType() {
return EntityType.PLAYER;
}
@Override
public InetSocketAddress getAddress() {
return session.getAddress();
}
@Override
public int getProtocolVersion() {
return session.getVersion();
}
@Nullable
@Override
public InetSocketAddress getVirtualHost() {
return session.getVirtualHost();
}
@Override
public boolean isOnline() {
return session.isActive() && session.isOnline();
}
////////////////////////////////////////////////////////////////////////////
// HumanEntity overrides
@Override
public boolean isBanned() {
return server.getBanList(BanList.Type.NAME).isBanned(getName());
}
@Override
public boolean isWhitelisted() {
return server.getWhitelist().containsProfile(
new GlowPlayerProfile(getName(), getUniqueId(), true));
}
@Override
protected boolean hasDefaultLandingBehavior() {
return false;
}
@Override
public void setWhitelisted(boolean value) {
if (value) {
server.getWhitelist().add(this);
} else {
server.getWhitelist().remove(new GlowPlayerProfile(getName(), getUniqueId(), true));
}
}
////////////////////////////////////////////////////////////////////////////
// Editable properties
@Override
public Player getPlayer() {
return this;
}
@Override
public boolean hasPlayedBefore() {
return hasPlayedBefore;
}
@Override
public boolean isOp() {
return getServer().getOpsList().containsUuid(getUniqueId());
}
@Override
public void setOp(boolean value) {
if (value) {
getServer().getOpsList().add(this);
} else {
getServer().getOpsList().remove(new GlowPlayerProfile(getName(), getUniqueId(), true));
}
permissions.recalculatePermissions();
}
@Override
public List<Message> createSpawnMessage() {
List<Message> result = super.createSpawnMessage();
if (bed != null) {
result.add(new UseBedMessage(getEntityId(), bed.getX(), bed.getY(), bed.getZ()));
}
return result;
}
@Override
public String getDisplayName() {
if (displayName != null) {
return displayName;
}
if (scoreboard != null) {
GlowTeam team = (GlowTeam) scoreboard.getEntryTeam(getName());
if (team != null) {
return team.getPlayerDisplayName(getName());
}
}
return getName();
}
@Override
public void setDisplayName(String name) {
displayName = name;
}
@Override
public String getPlayerListName() {
return playerListName == null || playerListName.isEmpty() ? getName() : playerListName;
}
@Override
public void setPlayerListName(String name) {
// update state
playerListName = name;
// send update message
TextMessage displayName = null;
if (playerListName != null && !playerListName.isEmpty()) {
displayName = new TextMessage(playerListName);
}
updateUserListEntries(UserListItemMessage.displayNameOne(getUniqueId(), displayName));
}
@Override
public void setPlayerListHeaderFooter(@Nullable String header,
@Nullable String footer) {
setPlayerListHeader(header);
setPlayerListFooter(footer);
}
@Override
public void setCompassTarget(Location loc) {
compassTarget = loc;
session.send(new SpawnPositionMessage(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
}
@Override
public Location getBedSpawnLocation() {
if (bedSpawn == null) {
return null;
}
// Find head of bed
GlowBlock block = (GlowBlock) bedSpawn.getBlock();
GlowBlock head = BlockBed.getHead(block);
GlowBlock foot = BlockBed.getFoot(block);
if (head != null) {
// If there is a bed, try to find an empty spot next to the bed
if (MaterialUtil.BEDS.contains(head.getType())) {
Block spawn = BlockBed.getExitLocation(head, foot);
return spawn == null ? null : spawn.getLocation().add(0.5, 0.1, 0.5);
}
if (bedSpawnForced) {
Material bottom = head.getType();
Material top = head.getRelative(BlockFace.UP).getType();
// Do not check floor when forcing spawn
if (BlockBed.isValidSpawn(bottom) && BlockBed.isValidSpawn(top)) {
return bedSpawn.clone().add(0.5, 0.1, 0.5); // No blocks are blocking the spawn
}
}
}
return null;
}
@Override
public long getLastLogin() {
return 0;
}
@Override
public long getLastSeen() {
return 0;
}
////////////////////////////////////////////////////////////////////////////
// Entity status
@Override
public void setBedSpawnLocation(Location bedSpawn) {
setBedSpawnLocation(bedSpawn, false);
}
@Override
public void setBedSpawnLocation(Location location, boolean force) {
bedSpawn = location;
bedSpawnForced = force;
}
@Override
public boolean sleep(@NotNull Location location, boolean force) {
return false;
}
@Override
public void wakeup(boolean setSpawnLocation) {
}
@Override
public @NotNull Location getBedLocation() {
return null;
}
@Override
public boolean isSleepingIgnored() {
return sleepingIgnored;
}
@Override
public void setSleepingIgnored(boolean isSleeping) {
sleepingIgnored = isSleeping;
}
@Override
public void setGameMode(GameMode mode) {
if (getGameMode() != mode) {
PlayerGameModeChangeEvent event = new PlayerGameModeChangeEvent(this, mode);
if (EventFactory.getInstance().callEvent(event).isCancelled()) {
return;
}
super.setGameMode(mode);
super.setFallDistance(0);
updateUserListEntries(UserListItemMessage.gameModeOne(getUniqueId(), mode.getValue()));
session.send(new StateChangeMessage(Reason.GAMEMODE, mode.getValue()));
}
setGameModeDefaults();
}
@Override
public ItemStack getActiveItem() {
return usageItem;
}
public void setUsageTime(int usageTime) {
startingUsageTime = usageTime;
this.usageTime = usageTime;
}
@Override
public int getItemUseRemainingTime() {
return usageTime;
}
@Override
public int getHandRaisedTime() {
return startingUsageTime - usageTime;
}
@Override
public boolean isHandRaised() {
return usageTime != 0;
}
////////////////////////////////////////////////////////////////////////////
// Player capabilities
private void setGameModeDefaults() {
GameMode mode = getGameMode();
setAllowFlight(mode == GameMode.CREATIVE || mode == GameMode.SPECTATOR);
metadata.setBit(MetadataIndex.STATUS, StatusFlags.INVISIBLE, mode == GameMode.SPECTATOR);
}
@Override
public boolean isSneaking() {
return metadata.getBit(MetadataIndex.STATUS, StatusFlags.SNEAKING);
}
@Override
public void setSneaking(boolean sneak) {
if (EventFactory.getInstance()
.callEvent(new PlayerToggleSneakEvent(this, sneak)).isCancelled()) {
return;
}
metadata.setBit(MetadataIndex.STATUS, StatusFlags.SNEAKING, sneak);
}
@Override
public boolean isSprinting() {
return metadata.getBit(MetadataIndex.STATUS, StatusFlags.SPRINTING);
}
@Override
public void setSprinting(boolean sprinting) {
if (EventFactory.getInstance()
.callEvent(new PlayerToggleSprintEvent(this, sprinting)).isCancelled()) {
return;
}
metadata.setBit(MetadataIndex.STATUS, StatusFlags.SPRINTING, sprinting);
}
@Override
public double getEyeHeight() {
return getEyeHeight(false);
}
@Override
public double getEyeHeight(boolean ignoreSneaking) {
// Height of player's eyes above feet. Matches CraftBukkit.
if (ignoreSneaking || !isSneaking()) {
return 1.62;
} else {
return 1.54;
}
}
@Override
public boolean getAllowFlight() {
return canFly;
}
@Override
public void setAllowFlight(boolean flight) {
canFly = flight;
if (!canFly) {
flying = false;
}
getServer().sendPlayerAbilities(this);
}
////////////////////////////////////////////////////////////////////////////
// Experience and levelling
@Override
public void setFlying(boolean value) {
flying = value && canFly;
getServer().sendPlayerAbilities(this);
}
@Override
public void setFlySpeed(float value) throws IllegalArgumentException {
flySpeed = value;
getServer().sendPlayerAbilities(this);
}
@Override
public void setWalkSpeed(float value) throws IllegalArgumentException {
walkSpeed = value;
getServer().sendPlayerAbilities(this);
}
@Override
public void setLevel(int level) {
int newLevel = Math.max(level, 0);
if (newLevel != this.level) {
EventFactory.getInstance().callEvent(
new PlayerLevelChangeEvent(this, this.level, newLevel));
this.level = newLevel;
sendExperience();
}
}
@Override
public void setTotalExperience(int exp) {
totalExperience = Math.max(exp, 0);
sendExperience();
}
@Override
public void giveExp(int xp) {
PlayerExpChangeEvent event = EventFactory.getInstance()
.callEvent(new PlayerExpChangeEvent(this, xp));
xp = event.getAmount();
totalExperience += xp;
// gradually award levels based on xp points
float value = 1.0f / getExpToLevel();
for (int i = 0; i < xp; ++i) {
exp += value;
if (exp >= 1) {
exp -= 1;
setLevel(level + 1);
value = 1.0f / getExpToLevel(level);
}
}
sendExperience();
}
@Override
public void giveExp(int xp, boolean applyMending) {
// todo: implement applyMending
giveExp(xp);
}
@Override
public int applyMending(int amount) {
throw new UnsupportedOperationException("Not implemented yet.");
}
@Override
public void giveExpLevels(int amount) {
setLevel(getLevel() + amount);
}
@Override
public void setExp(float percentToLevel) {
exp = Math.min(Math.max(percentToLevel, 0), 1);
sendExperience();
}
@Override
public int getExpToLevel() {
return getExpToLevel(level);
}
@Override
public boolean discoverRecipe(@NotNull NamespacedKey recipe) {
return false;
}
@Override
public int discoverRecipes(@NotNull Collection<NamespacedKey> recipes) {
return 0;
}
@Override
public boolean undiscoverRecipe(@NotNull NamespacedKey recipe) {
return false;
}
@Override
public int undiscoverRecipes(@NotNull Collection<NamespacedKey> recipes) {
return 0;
}
private int getExpToLevel(int level) {
if (level >= 30) {
return 62 + (level - 30) * 7;
} else if (level >= 15) {
return 17 + (level - 15) * 3;
} else {
return 17;
}
}
private void sendExperience() {
session.send(new ExperienceMessage(getExp(), getLevel(), getTotalExperience()));
}
////////////////////////////////////////////////////////////////////////////
// Health and food handling
/**
* Recipes this player has unlocked.
*
* @return An immutable list of unlocked recipes.
*/
public Collection<Recipe> getUnlockedRecipes() {
return ImmutableList.copyOf(recipes);
}
/**
* Teach the player a new recipe.
*
* @param recipe The recipe to be added to learnt recipes
* @param notify If the player should be notified of the recipes learnt
* @return If this recipe was not learned already.
*/
public boolean learnRecipe(Recipe recipe, boolean notify) {
return recipe != null && recipes.add(recipe);
}
/**
* Remove a recipe from the player's known recipes.
*
* @param recipe The recipe to be removed from learnt recipes
* @return If this recipe was learned before it was removed.
*/
public boolean unlearnRecipe(Recipe recipe) {
return recipes.remove(recipe);
}
/**
* Checks to see if the player knows this recipe.
*
* @param recipe The recipe to check
* @return If the player knows the recipe
*/
public boolean knowsRecipe(Recipe recipe) {
return recipes.contains(recipe);
}
@Override
public void setHealth(double health) {
super.setHealth(health);
sendHealth();
}
@Override
public void setMaxHealth(double health) {
super.setMaxHealth(health);
sendHealth();
}
@Override
public void setHealthScaled(boolean scale) {
healthScaled = scale;
sendHealth();
}
@Override
public void setHealthScale(double scale) throws IllegalArgumentException {
healthScaled = true;
healthScale = scale;
sendHealth();
}
@Override
public Entity getSpectatorTarget() {
return spectating;
}
@Override
public void setSpectatorTarget(Entity entity) {
teleport(entity.getLocation(), PlayerTeleportEvent.TeleportCause.SPECTATE);
spectating = entity;
}
/**
* Updates the hunger bar and hunger saturation.
*
* @param food the amount of food (in half-icons on the hunger bar)
* @param saturation the amount of food saturation (in half-icons of food it will save)
*/
public void setFoodLevelAndSaturation(int food, float saturation) {
this.foodLevel = Math.max(Math.min(food, 20), 0);
this.saturation = Math.min(this.saturation + food * saturation * 2.0F, this.foodLevel);
sendHealth();
}
@Override
public void setFoodLevel(int food) {
this.foodLevel = Math.min(food, 20);
sendHealth();
}
private boolean shouldCalculateExhaustion() {
return getGameMode() == GameMode.SURVIVAL | getGameMode() == GameMode.ADVENTURE;
}
/**
* Increases the exhaustion counter, but applies the maximum.
*
* @param exhaustion the amount of exhaustion to add
*/
// todo: effects
// todo: swim
// todo: jump
// todo: food poisoning
// todo: jump and sprint
public void addExhaustion(float exhaustion) {
if (shouldCalculateExhaustion()) {
this.exhaustion = Math.min(this.exhaustion + exhaustion, 40f);
}
}
////////////////////////////////////////////////////////////////////////////
// Actions
/**
* Add the exhaustion for sprinting from the given location to the current location, if this
* player <em>is</em> sprinting.
*
* @param move the previous location
*/
public void addMoveExhaustion(Location move) {
if (shouldCalculateExhaustion() && !teleported && isSprinting()) {
double distanceSquared = location.distanceSquared(move);
if (distanceSquared > 0) { // update packet and rotation
double distance = Math.sqrt(distanceSquared);
addExhaustion((float) (0.1f * distance));
}
}
}
@Override
public void setSaturation(float value) {
saturation = Math.min(value, foodLevel);
sendHealth();
}
private void sendHealth() {
float finalHealth = (float) (getHealth() / getMaxHealth() * getHealthScale());
session.send(new HealthMessage(finalHealth, getFoodLevel(), getSaturation()));
}
/**
* Teleport the player.
*
* @param location The destination to teleport to.
* @return Whether the teleport was a success.
*/
@Override
public boolean teleport(Location location) {
return teleport(location, TeleportCause.UNKNOWN);
}
@Override
public boolean teleport(Location location, TeleportCause cause) {
checkNotNull(location, "location cannot be null"); // NON-NLS
checkNotNull(location.getWorld(), "location's world cannot be null"); // NON-NLS
checkNotNull(cause, "cause cannot be null"); // NON-NLS
if (this.location != null && this.location.getWorld() != null) {
PlayerTeleportEvent event
= new PlayerTeleportEvent(this, this.location, location, cause);
if (EventFactory.getInstance().callEvent(event).isCancelled()) {
return false;
}
location = event.getTo();
closeInventory();
}
worldLock.writeLock().lock();
try {
if (location.getWorld() != world) {
spawnAt(location);
} else {
world.getEntityManager().move(this, location);
//Position.copyLocation(location, this.previousLocation);
//Position.copyLocation(location, this.location);
session.send(new PositionRotationMessage(location));
teleportedTo = location.clone();
}
} finally {
worldLock.writeLock().unlock();
}
teleportedTo = location.clone();
return true;
}
/**
* Finishes the teleport process.
*/
public void endTeleport() {
Position.copyLocation(teleportedTo, location);
teleportedTo = null;
teleported = true;
}
@Override
protected boolean teleportToSpawn() {
Location target = getBedSpawnLocation();
if (target == null) {
target = server.getWorlds().get(0).getSpawnLocation();
}
PlayerPortalEvent event = EventFactory.getInstance()
.callEvent(new PlayerPortalEvent(this, location.clone(), target, null));
if (event.isCancelled()) {
return false;
}
target = event.getTo();
spawnAt(target);
teleported = true;
return true;
}
@Override
protected boolean teleportToEnd() {
if (!server.getAllowEnd()) {
return false;
}
Location target = null;
for (World world : server.getWorlds()) {
if (world.getEnvironment() == Environment.THE_END) {
target = world.getSpawnLocation();
break;
}
}
if (target == null) {
return false;
}
PlayerPortalEvent event = EventFactory.getInstance()
.callEvent(new PlayerPortalEvent(this, location.clone(), target, null));
if (event.isCancelled()) {
return false;
}
target = event.getTo();
spawnAt(target);
teleported = true;
return true;
}
/**
* This player enters the specified bed and is marked as sleeping.
*
* @param block the bed
*/
public void enterBed(GlowBlock block) {
checkNotNull(block, "Bed block cannot be null");
Preconditions.checkState(bed == null, "Player already in bed");
GlowBlock head = BlockBed.getHead(block);
GlowBlock foot = BlockBed.getFoot(block);
if (EventFactory.getInstance()
.callEvent(new PlayerBedEnterEvent(this, head)).isCancelled()) {
return;
}
// Occupy the bed
BlockBed.setOccupied(head, foot, true);
bed = head;
sleeping = true;
setRawLocation(head.getLocation(), false);
getSession().send(new UseBedMessage(SELF_ID, head.getX(), head.getY(), head.getZ()));
UseBedMessage msg = new UseBedMessage(getEntityId(), head.getX(), head.getY(), head.getZ());
world.getRawPlayers().stream().filter(p -> p != this && p.canSeeEntity(this))
.forEach(p -> p.getSession().send(msg));
}
/**
* This player leaves their bed causing them to quit sleeping.
*
* @param setSpawn Whether to set the bed spawn of the player
*/
public void leaveBed(boolean setSpawn) {
Preconditions.checkState(bed != null, "Player is not in bed");
GlowBlock head = BlockBed.getHead(bed);
GlowBlock foot = BlockBed.getFoot(bed);
// Determine exit location
Block exitBlock = BlockBed.getExitLocation(head, foot);
if (exitBlock == null) { // If no empty blocks were found fallback to block above bed
exitBlock = head.getRelative(BlockFace.UP);
}
// Set their spawn (normally omitted if their bed gets destroyed instead of them leaving it)
if (setSpawn) {
setBedSpawnLocation(head.getLocation());
}
// Empty the bed
BlockBed.setOccupied(head, foot, false);
bed = null;
sleeping = false;
// And eject the player
Location exitLocation = exitBlock.getLocation().add(0.5, 0.1, 0.5); // Use center of block
setRawLocation(exitLocation, false);
teleported = true;
// Call event
EventFactory.getInstance().callEvent(new PlayerBedLeaveEvent(this, head, setSpawn));
playAnimationToSelf(EntityAnimation.LEAVE_BED);
playAnimation(EntityAnimation.LEAVE_BED);
}
@Override
public void sendMessage(String message) {
sendRawMessage(message);
}
@Override
public void sendMessage(String[] messages) {
for (String line : messages) {
sendMessage(line);
}
}
@Override
public void sendMessage(BaseComponent component) {
sendMessage(ChatMessageType.CHAT, component);
}
@Override
public void sendMessage(BaseComponent... components) {
sendMessage(ChatMessageType.CHAT, components);
}
@Override
public void sendMessage(ChatMessageType chatMessageType, BaseComponent... baseComponents) {
session.send(new ChatMessage(TextMessage
.decode(ComponentSerializer.toString(baseComponents)), chatMessageType.ordinal()));
}
@Override
public void sendRawMessage(String message) {
// old-style formatting to json conversion is in TextMessage
session.send(new ChatMessage(message));
}
@Override
public void sendActionBar(String message) {
// "old" formatting workaround because apparently "new" styling doesn't work as of
// 01/18/2015
JSONObject json = new JSONObject();
json.put("text", message);
session.send(new ChatMessage(new TextMessage(json), 2));
}
@Override
public void sendActionBar(char alternateChar, String message) {
sendActionBar(message); // TODO: don't ignore formatting codes
}
@Override
public void spawnParticle(Particle particle, Location location, int count) {
spawnParticle(particle, location, count, null);
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count) {
spawnParticle(particle, x, y, z, count, null);
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, T data) {
spawnParticle(particle, location, count, 0, 0, 0, 1, data);
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count,
T data) {
spawnParticle(particle, x, y, z, count, 0, 0, 0, 1, data);
}
@Override
public void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ) {
spawnParticle(particle, location, count, offsetX, offsetY, offsetZ, 1, null);
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, 1, null);
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ, T data) {
spawnParticle(particle, location, count, offsetX, offsetY, offsetZ, 1, data);
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ, T data) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetY, 1, data);
}
@Override
public void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ, double extra) {
spawnParticle(particle, location, count, offsetX, offsetY, offsetZ, extra, null);
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ, double extra) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, null);
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ, double extra, T data) {
double distance = getLocation().distanceSquared(location);
boolean isLongDistance = GlowParticle.isLongDistance(particle);
int particleId = GlowParticle.getId(particle);
Object[] particleData = GlowParticle.getExtData(particle, data);
if (distance <= 1024.0D || isLongDistance && distance <= 262144.0D) {
getSession().send(new PlayParticleMessage(particleId, isLongDistance,
(float) location.getX(), (float) location.getY(), (float) location.getZ(),
(float) offsetX, (float) offsetY, (float) offsetZ,
(float) extra, count, particleData));
}
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ, double extra, T data) {
spawnParticle(particle, new Location(world, x, y, z), count, offsetX, offsetY, offsetZ,
extra, data);
}
@Override
public AdvancementProgress getAdvancementProgress(Advancement advancement) {
return advancements.get(advancement);
}
@Override
public int getClientViewDistance() {
return 0;
}
@Override
public String getLocale() {
return settings.getLocale();
}
@Override
public boolean getAffectsSpawning() {
return affectsSpawning;
}
@Override
public void updateCommands() {
}
@Override
public int getViewDistance() {
return settings.getViewDistance();
}
@Override
public void setViewDistance(int viewDistance) {
settings.setViewDistance(viewDistance);
}
@Override
public void kickPlayer(String message) {
remove();
session.disconnect(message == null ? "" : message);
}
public void kickPlayer(String message, boolean async) {
remove(async);
session.disconnect(message == null ? "" : message);
}
@Override
public boolean performCommand(String command) {
return getServer().dispatchCommand(this, command);
}
@Override
public void chat(String text) {
chat(text, false);
}
/**
* Says a message (or runs a command).
*
* @param text message sent by the player.
* @param async whether the message was received asynchronously.
*/
public void chat(String text, boolean async) {
if (text.charAt(0) == '/') {
Runnable task = () -> {
server.getLogger().info(getName() + " issued command: " + text);
try {
PlayerCommandPreprocessEvent event
= new PlayerCommandPreprocessEvent(this, text);
if (!EventFactory.getInstance().callEvent(event).isCancelled()) {
server.dispatchCommand(this, event.getMessage().substring(1));
}
} catch (Exception ex) {
sendMessage(ChatColor.RED
+ "An internal error occurred while executing your command.");
server.getLogger()
.log(Level.SEVERE, "Exception while executing command: " + text, ex);
}
};
// if async is true, this task should happen synchronously
// otherwise, we're sync already, it can happen here
if (async) {
server.getScheduler().runTask(null, task);
} else {
task.run();
}
} else {
AsyncPlayerChatEvent event = EventFactory.getInstance().onPlayerChat(async, this, text);
if (event.isCancelled()) {
return;
}
String message = String.format(event.getFormat(), getDisplayName(), event.getMessage());
getServer().getLogger().info(message);
for (Player recipient : event.getRecipients()) {
recipient.sendMessage(message);
}
}
}
@Override
public void saveData() {
saveData(true);
}
/**
* Saves the players current location, health, inventory, motion, and other information into the
* username.dat file, in the world/player folder.
*
* @param async if true, save asynchronously; if false, block until saved
*/
public void saveData(boolean async) {
if (async) {
Bukkit.getScheduler().runTaskAsynchronously(null, () -> {
server.getPlayerDataService().writeData(GlowPlayer.this);
server.getPlayerStatisticIoService().writeStatistics(GlowPlayer.this);
});
} else {
server.getPlayerDataService().writeData(this);
server.getPlayerStatisticIoService().writeStatistics(this);
}
}
@Override
public void loadData() {
server.getPlayerDataService().readData(this);
server.getPlayerStatisticIoService().readStatistics(this);
}
@Override
@Deprecated
public void setTexturePack(String url) {
setResourcePack(url);
}
@Override
public void setResourcePack(String url) {
setResourcePack(url, "");
}
@Override
public void setResourcePack(String url, byte[] hash) {
checkNotNull(url);
checkNotNull(hash);
checkArgument(hash.length == 20, "Resource pack hash is of an invalid length.");
setResourcePack(url, Convert.fromBytes(hash));
}
@Override
public void setResourcePack(String url, String hash) {
checkNotNull(url);
checkNotNull(hash);
checkArgument(hash.length() == 40, "Resource pack hash is of an invalid length.");
session.send(new ResourcePackSendMessage(url, hash));
resourcePackHash = hash;
}
@Override
public PlayerResourcePackStatusEvent.Status getResourcePackStatus() {
return resourcePackStatus;
}
public void setResourcePackStatus(PlayerResourcePackStatusEvent.Status status) {
resourcePackStatus = status;
}
@Override
public String getResourcePackHash() {
return resourcePackHash;
}
////////////////////////////////////////////////////////////////////////////
// Effect and data transmission
@Override
public boolean hasResourcePack() {
return resourcePackStatus == PlayerResourcePackStatusEvent.Status.SUCCESSFULLY_LOADED;
}
@Override
public PlayerProfile getPlayerProfile() {
return getProfile();
}
@Override
public void setPlayerProfile(PlayerProfile playerProfile) {
throw new UnsupportedOperationException("Not implemented yet.");
}
@Override
public float getCooldownPeriod() {
return 0;
}
@Override
public float getCooledAttackStrength(float adjustTicks) {
return 0;
}
@Override
public void resetCooldown() {
}
@Override
public void playNote(Location loc, Instrument instrument, Note note) {
Sound sound;
switch (instrument) {
case PIANO:
sound = Sound.BLOCK_NOTE_BLOCK_HARP;
break;
case BASS_DRUM:
sound = Sound.BLOCK_NOTE_BLOCK_BASEDRUM;
break;
case SNARE_DRUM:
sound = Sound.BLOCK_NOTE_BLOCK_SNARE;
break;
case STICKS:
sound = Sound.BLOCK_NOTE_BLOCK_HAT;
break;
case BASS_GUITAR:
sound = Sound.BLOCK_NOTE_BLOCK_BASS;
break;
case BELL:
sound = Sound.BLOCK_NOTE_BLOCK_BELL;
break;
default:
sound = null;
}
byte step = note.getId();
int octave = note.getOctave();
float pitch = (float) Math.pow(2, octave) / 2f;
for (int i = 1; i <= step; i++) {
if (i < 7) {
pitch += 1f / 3f;
} else if (step < 18) {
pitch += 0.05f;
} else {
pitch += 0.1f;
}
}
playSound(loc, sound, SoundCategory.MUSIC, 3.0f, pitch);
}
@Override
public void playNote(Location loc, byte instrument, byte note) {
playNote(loc, Instrument.getByType(instrument), new Note(note));
}
@Override
public void playEffect(Location loc, Effect effect, int data) {
int id = effect.getId();
session.send(new PlayEffectMessage(id, loc.getBlockX(), loc.getBlockY(), loc
.getBlockZ(), data, false));
}
@Override
public <T> void playEffect(Location loc, Effect effect, T data) {
playEffect(loc, effect, GlowEffect.getDataValue(effect, data));
}
@Override
public void playSound(Location location, Sound sound, float volume, float pitch) {
playSound(location, sound, GlowSound
.getSoundCategory(GlowSound.getVanillaId(sound)), volume, pitch);
}
@Override
public void playSound(Location location, String sound, float volume, float pitch) {
playSound(location, GlowSound.getVanillaSound(sound), volume, pitch);
}
@Override
public void playSound(Location location, String sound, SoundCategory category, float volume,
float pitch) {
if (location == null || sound == null) {
return;
}
double x = location.getX();
double y = location.getY();
double z = location.getZ();
session.send(new NamedSoundEffectMessage(sound, category, x, y, z, volume, pitch));
}
@Override
public void playSound(Location location, Sound sound, SoundCategory category, float volume,
float pitch) {
playSound(location, GlowSound.getVanillaId(sound), category, volume, pitch);
}
@Override
public void stopSound(Sound sound) {
stopSound(null, sound);
}
public void stopSound(SoundCategory category) {
stopSound("", category);
}
@Override
public void stopSound(Sound sound, SoundCategory soundCategory) {
stopSound(GlowSound.getVanillaId(sound), soundCategory);
}
@Override
public void stopSound(String sound, SoundCategory category) {
String source = "";
if (category != null) {
source = category.name().toLowerCase();
}
if (sound == null || sound.equalsIgnoreCase("all")) {
sound = "";
}
ByteBuf buffer = Unpooled.buffer();
try {
ByteBufUtils.writeUTF8(buffer, source); //Source
ByteBufUtils.writeUTF8(buffer, sound); //Sound
session.sendAndRelease(new PluginMessage("MC|StopSound", buffer.array()), // NON-NLS
buffer);
} catch (IOException e) {
logger.info("Failed to send stop-sound event.");
e.printStackTrace();
}
}
public void stopSound(SoundCategory category, Sound sound) {
stopSound(sound == null ? "" : GlowSound.getVanillaId(sound), category);
}
@Override
public void stopSound(String sound) {
if (sound == null || sound.equalsIgnoreCase("all")) {
sound = "";
}
stopSound(sound, null);
}
public void stopAllSounds() {
stopSound("");
}
@Override
public Player.Spigot spigot() {
return spigot;
}
@Override
public CreatureSpawnEvent.@NotNull SpawnReason getEntitySpawnReason() {
return null;
}
/**
* Sends a {@link PlayParticleMessage} to display the given particle.
*
* @param loc the location
* @param particle the particle type
* @param material the item or block data
* @param offsetX TODO: document this parameter
* @param offsetY TODO: document this parameter
* @param offsetZ TODO: document this parameter
* @param speed TODO: document this parameter
* @param amount the number of particles
*/
//@Override
public void showParticle(Location loc, Effect particle, MaterialData material, float offsetX,
float offsetY, float offsetZ, float speed, int amount) {
if (location == null || particle == null || particle.getType() != Type.VISUAL) {
return;
}
int id = GlowParticle.getId(particle);
boolean longDistance = GlowParticle.isLongDistance(particle);
float x = (float) loc.getX();
float y = (float) loc.getY();
float z = (float) loc.getZ();
Object[] extData = GlowParticle.getExtData(particle, material);
session.send(new PlayParticleMessage(id, longDistance, x, y, z, offsetX, offsetY,
offsetZ, speed, amount, extData));
}
@Override
public void sendBlockChange(Location loc, Material material, byte data) {
sendBlockChange(loc, material.getId(), data);
}
@Override
public void sendBlockChange(@NotNull Location loc, @NotNull BlockData block) {
}
@Deprecated
public void sendBlockChange(Location loc, int material, byte data) {
sendBlockChange(new BlockChangeMessage(loc.getBlockX(), loc.getBlockY(), loc
.getBlockZ(), material, data));
}
/**
* Sends the given {@link BlockChangeMessage} if it's in a chunk this player can see.
*
* @param message the message to send
*/
public void sendBlockChange(BlockChangeMessage message) {
// only send message if the chunk is within visible range
Key key = GlowChunk.Key.of(message.getX() >> 4, message.getZ() >> 4);
if (canSeeChunk(key)) {
blockChanges.add(message);
}
}
@Deprecated
public void sendBlockChangeForce(BlockChangeMessage message) {
blockChanges.add(message);
}
@Override
public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void sendSignChange(Location location, String[] lines) throws IllegalArgumentException {
checkNotNull(location, "location cannot be null");
checkNotNull(lines, "lines cannot be null");
checkArgument(lines.length == 4, "lines.length must equal 4");
afterBlockChanges.add(UpdateSignMessage
.fromPlainText(location.getBlockX(), location.getBlockY(), location
.getBlockZ(), lines));
}
/**
* Send a sign change, similar to {@link #sendSignChange(Location, String[])}, but using
* complete TextMessages instead of strings.
*
* @param sign the sign
* @param location the location of the sign
* @param lines the new text on the sign or null to clear it
* @throws IllegalArgumentException if location is null
* @throws IllegalArgumentException if lines is non-null and has a length less than 4
*/
public void sendSignChange(SignEntity sign, Location location,
TextMessage[] lines) throws IllegalArgumentException {
checkNotNull(location, "location cannot be null");
checkNotNull(lines, "lines cannot be null");
checkArgument(lines.length == 4, "lines.length must equal 4");
CompoundTag tag = new CompoundTag();
sign.saveNbt(tag);
afterBlockChanges.add(new UpdateBlockEntityMessage(location.getBlockX(), location
.getBlockY(), location.getBlockZ(), GlowBlockEntity.SIGN.getValue(), tag));
}
/**
* Send a block entity change to the given location.
*
* @param location The location of the block entity.
* @param type The type of block entity being sent.
* @param nbt The NBT structure to send to the client.
*/
public void sendBlockEntityChange(Location location, GlowBlockEntity type, CompoundTag nbt) {
checkNotNull(location, "Location cannot be null");
checkNotNull(type, "Type cannot be null");
checkNotNull(nbt, "NBT cannot be null");
afterBlockChanges.add(new UpdateBlockEntityMessage(location.getBlockX(), location
.getBlockY(), location.getBlockZ(), type.getValue(), nbt));
}
@Override
public void sendMap(MapView map) {
GlowMapCanvas mapCanvas = GlowMapCanvas.createAndRender(map, this);
session.send(new MapDataMessage(map.getId(), map.getScale().ordinal(), Collections
.emptyList(),
mapCanvas.toSection()));
}
@Override
public void setPlayerListHeaderFooter(BaseComponent[] header, BaseComponent[] footer) {
TextMessage h = TextMessage.decode(ComponentSerializer.toString(header));
TextMessage f = TextMessage.decode(ComponentSerializer.toString(footer));
session.send(new UserListHeaderFooterMessage(h, f));
}
@Override
public void setPlayerListHeaderFooter(BaseComponent header, BaseComponent footer) {
setPlayerListHeaderFooter(new BaseComponent[]{header}, new BaseComponent[]{footer});
}
@Override
public void setTitleTimes(int fadeInTicks, int stayTicks, int fadeOutTicks) {
currentTitle.fadeIn(fadeInTicks);
currentTitle.stay(stayTicks);
currentTitle.fadeOut(fadeOutTicks);
}
@Override
public void setSubtitle(BaseComponent[] subtitle) {
currentTitle.subtitle(subtitle);
}
@Override
public void setSubtitle(BaseComponent subtitle) {
currentTitle.subtitle(subtitle);
}
@Override
public void showTitle(BaseComponent[] title) {
sendTitle(new Title(title));
}
@Override
public void showTitle(BaseComponent title) {
sendTitle(new Title(title));
}
@Override
public void showTitle(BaseComponent[] title, BaseComponent[] subtitle, int fadeInTicks,
int stayTicks, int fadeOutTicks) {
sendTitle(new Title(title, subtitle, fadeInTicks, stayTicks, fadeOutTicks));
}
@Override
public void showTitle(BaseComponent title, BaseComponent subtitle, int fadeInTicks,
int stayTicks, int fadeOutTicks) {
sendTitle(new Title(title, subtitle, fadeInTicks, stayTicks, fadeOutTicks));
}
@Override
public void sendTitle(Title title) {
session.sendAll(TitleMessage.fromTitle(title));
}
@Override
public void sendTitle(String title, String subtitle) {
sendTitle(new Title(title, subtitle));
}
@Override
public void sendTitle(String title, String subtitle, int fadeIn, int stay, int fadeOut) {
sendTitle(new Title(title, subtitle, fadeIn, stay, fadeOut));
}
/**
* Send the player a title base on a {@link Title.Builder}.
*
* @param title the {@link Title.Builder} to send the player
*/
public void sendTitle(Title.Builder title) {
sendTitle(title.build());
}
/**
* Send the player their current title.
*/
public void sendTitle() {
sendTitle(currentTitle);
currentTitle = new Title.Builder();
}
@Override
public void updateTitle(Title title) {
Title builtTitle = currentTitle.build();
if (title.getTitle().length != 0) {
currentTitle.title(title.getTitle());
}
if (title.getSubtitle() != null) {
currentTitle.subtitle(title.getSubtitle());
}
if (builtTitle.getFadeIn() != title.getFadeIn()
&& title.getFadeIn() != Title.DEFAULT_FADE_IN) {
currentTitle.fadeIn(title.getFadeIn());
}
if (builtTitle.getStay() != title.getStay() && title.getStay() != Title.DEFAULT_STAY) {
currentTitle.stay(title.getStay());
}
if (builtTitle.getFadeOut() != title.getFadeOut()
&& title.getFadeOut() != Title.DEFAULT_FADE_OUT) {
currentTitle.fadeOut(title.getFadeOut());
}
}
/**
* Update a specific attribute of the player's title.
*
* @param action the attribute to update
* @param value the value of the attribute
*/
public void updateTitle(TitleMessage.Action action, Object... value) {
Preconditions.checkArgument(
value.length > 0, "Expected at least one argument. Got " + value.length);
switch (action) {
case TITLE:
Preconditions.checkArgument(!(value instanceof String[]
|| value instanceof BaseComponent[]), "Value is not of the correct type");
if (value[0] instanceof String) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < value.length; i++) {
if (i > 0) {
builder.append(" ");
}
builder.append(value[i]);
}
currentTitle.title(builder.toString());
} else {
BaseComponent[] formattedValue = (BaseComponent[]) value;
currentTitle.title(formattedValue);
}
break;
case SUBTITLE:
Preconditions.checkArgument(!(value instanceof String[]
|| value instanceof BaseComponent[]), "Value is not of the correct type");
if (value[0] instanceof String) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < value.length; i++) {
if (i > 0) {
builder.append(" ");
}
builder.append(value[i]);
}
currentTitle.subtitle(builder.toString());
} else {
BaseComponent[] formattedValue = (BaseComponent[]) value;
currentTitle.subtitle(formattedValue);
}
break;
case TIMES:
Preconditions
.checkArgument(!(value instanceof Integer[]), "Value is not of the "
+ "correct type");
Preconditions
.checkArgument(value.length == 3, "Expected 3 values. Got " + value.length);
currentTitle.fadeIn((int) value[0]);
currentTitle.stay((int) value[1]);
currentTitle.fadeOut((int) value[2]);
break;
default:
Preconditions
.checkArgument(true, "Action is something other than a title, subtitle, "
+ "or times");
}
}
////////////////////////////////////////////////////////////////////////////
// Achievements and statistics
@Override
public void hideTitle() {
currentTitle = new Title.Builder();
session.send(new TitleMessage(Action.CLEAR));
}
@Override
public boolean hasAchievement(Achievement achievement) {
throw new UnsupportedOperationException("Achievements are no longer implemented.");
}
@Override
public void awardAchievement(Achievement achievement) {
awardAchievement(achievement, true);
}
/**
* Awards the given achievement if the player already has the parent achievement, otherwise does
* nothing.
*
* <p>If {@code awardParents} is true, award the player all parent achievements and the given
* achievement, making this method equivalent to {@link #awardAchievement(Achievement)}.
*
* @param achievement the achievement to award.
* @param awardParents whether parent achievements should be awarded.
* @return {@code true} if the achievement was awarded, {@code false} otherwise
*/
public boolean awardAchievement(Achievement achievement, boolean awardParents) {
if (hasAchievement(achievement)) {
return false;
}
Achievement parent = achievement.getParent();
if (parent != null && !hasAchievement(parent)) {
if (!awardParents || !awardAchievement(parent, true)) {
// does not have or failed to award required parent achievement
return false;
}
}
PlayerAchievementAwardedEvent event = new PlayerAchievementAwardedEvent(this, achievement);
if (EventFactory.getInstance().callEvent(event).isCancelled()) {
return false; // event was cancelled
}
stats.setAchievement(achievement, true);
if (server.getAnnounceAchievements()) {
// todo: make message fancier (hover)
server.broadcastMessage(GlowstoneMessages.Achievement.EARNED.get(
getName(), ACHIEVEMENT_NAMES.valueToName(Locale.getDefault(), achievement)));
}
return true;
}
@Override
public void removeAchievement(Achievement achievement) {
if (!hasAchievement(achievement)) {
return;
}
stats.setAchievement(achievement, false);
}
@Override
public int getStatistic(Statistic statistic) throws IllegalArgumentException {
return stats.get(statistic);
}
@Override
public int getStatistic(Statistic statistic,
Material material) throws IllegalArgumentException {
return stats.get(statistic, material);
}
@Override
public int getStatistic(Statistic statistic,
EntityType entityType) throws IllegalArgumentException {
return stats.get(statistic, entityType);
}
@Override
public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException {
stats.set(statistic, newValue);
}
@Override
public void setStatistic(Statistic statistic, Material material,
int newValue) throws IllegalArgumentException {
stats.set(statistic, material, newValue);
}
@Override
public void setStatistic(Statistic statistic, EntityType entityType, int newValue) {
stats.set(statistic, entityType, newValue);
}
@Override
public void incrementStatistic(Statistic statistic) {
incrementStatistic(statistic, 1);
}
@Override
public void incrementStatistic(Statistic statistic, int amount) {
int initialAmount = stats.get(statistic);
PlayerStatisticIncrementEvent event = EventFactory.getInstance().callEvent(
new PlayerStatisticIncrementEvent(this, statistic, initialAmount,
initialAmount + amount));
if (!event.isCancelled()) {
stats.add(statistic, amount);
}
}
@Override
public void incrementStatistic(Statistic statistic, Material material) {
incrementStatistic(statistic, material, 1);
}
@Override
public void incrementStatistic(Statistic statistic, Material material, int amount) {
int initialAmount = stats.get(statistic);
PlayerStatisticIncrementEvent event = EventFactory.getInstance().callEvent(
new PlayerStatisticIncrementEvent(this, statistic, initialAmount,
initialAmount + amount, material));
if (!event.isCancelled()) {
stats.add(statistic, material, amount);
}
}
@Override
public void incrementStatistic(Statistic statistic,
EntityType entityType) throws IllegalArgumentException {
incrementStatistic(statistic, entityType, 1);
}
@Override
public void incrementStatistic(Statistic statistic, EntityType entityType,
int amount) throws IllegalArgumentException {
int initialAmount = stats.get(statistic);
PlayerStatisticIncrementEvent event = EventFactory.getInstance().callEvent(
new PlayerStatisticIncrementEvent(this, statistic, initialAmount,
initialAmount + amount, entityType));
if (!event.isCancelled()) {
stats.add(statistic, entityType, amount);
}
}
@Override
public void decrementStatistic(Statistic statistic) throws IllegalArgumentException {
stats.add(statistic, -1);
}
@Override
public void decrementStatistic(Statistic statistic,
int amount) throws IllegalArgumentException {
stats.add(statistic, -amount);
}
@Override
public void decrementStatistic(Statistic statistic,
Material material) throws IllegalArgumentException {
stats.add(statistic, material, -1);
}
@Override
public void decrementStatistic(Statistic statistic, Material material,
int amount) throws IllegalArgumentException {
stats.add(statistic, material, -amount);
}
@Override
public void decrementStatistic(Statistic statistic,
EntityType entityType) throws IllegalArgumentException {
stats.add(statistic, entityType, -1);
}
@Override
public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) {
stats.add(statistic, entityType, -amount);
}
public StatisticMap getStatisticMap() {
return stats;
}
////////////////////////////////////////////////////////////////////////////
// Inventory
public void sendStats() {
session.send(stats.toMessage());
}
@Override
public void updateInventory() {
session.send(new SetWindowContentsMessage(invMonitor.getId(), invMonitor.getContents()));
}
/**
* Sends a {@link SetWindowSlotMessage} to update the contents of an inventory slot.
*
* @param slot the slot ID
* @param item the new contents
*/
public void sendItemChange(int slot, ItemStack item) {
if (invMonitor != null) {
session.send(new SetWindowSlotMessage(invMonitor.getId(), slot, item));
}
}
@Override
public void setItemOnCursor(ItemStack item) {
super.setItemOnCursor(item);
session.send(new SetWindowSlotMessage(-1, -1, item));
}
@Override
public boolean hasCooldown(Material material) {
return false;
}
@Override
public int getCooldown(Material material) {
return 0;
}
@Override
public void setCooldown(Material material, int ticks) {
}
@Override
public MainHand getMainHand() {
return metadata.getByte(MetadataIndex.PLAYER_MAIN_HAND) == 0 ? MainHand.LEFT
: MainHand.RIGHT;
}
@Override
public boolean setWindowProperty(Property prop, int value) {
if (!super.setWindowProperty(prop, value)) {
return false;
}
session.send(new WindowPropertyMessage(invMonitor.getId(), prop.getId(), value));
return true;
}
@Override
public void openInventory(InventoryView view) {
session.send(new CloseWindowMessage(invMonitor.getId()));
super.openInventory(view);
invMonitor = new InventoryMonitor(getOpenInventory());
int viewId = invMonitor.getId();
if (viewId != 0) {
InventoryOpenEvent event = EventFactory.getInstance().callEvent(
new InventoryOpenEvent(view));
if (event.isCancelled()) {
// close the inventory but don't fire the InventoryCloseEvent
resetInventoryView();
return;
}
String title = view.getTitle();
boolean defaultTitle = Objects.equals(view.getType().getDefaultTitle(), title);
if (view.getTopInventory() instanceof PlayerInventory && defaultTitle) {
title = ((PlayerInventory) view.getTopInventory()).getHolder().getName();
}
Message open = new OpenWindowMessage(viewId, invMonitor
.getType(), title, ((GlowInventory) view.getTopInventory()).getRawSlots());
session.send(open);
}
updateInventory();
}
@Override
public InventoryView openMerchant(Villager villager, boolean b) {
return null;
}
@Override
public InventoryView openMerchant(Merchant merchant, boolean b) {
return null;
}
////////////////////////////////////////////////////////////////////////////
// Player-specific time and weather
@Override
public GlowItem drop(ItemStack stack) {
GlowItem dropping = super.drop(stack);
if (dropping != null) {
PlayerDropItemEvent event = new PlayerDropItemEvent(this, dropping);
EventFactory.getInstance().callEvent(event);
if (event.isCancelled()) {
dropping.remove();
dropping = null;
} else {
incrementStatistic(Statistic.DROP, stack.getAmount());
}
}
return dropping;
}
@Override
public void setPlayerTime(long time, boolean relative) {
timeOffset = (time % TickUtil.TICKS_PER_DAY + TickUtil.TICKS_PER_DAY)
% TickUtil.TICKS_PER_DAY;
playerTimeRelative = relative;
sendTime();
}
@Override
public long getPlayerTime() {
if (playerTimeRelative) {
// add timeOffset ticks to current time
return (world.getTime() + timeOffset) % TickUtil.TICKS_PER_DAY;
} else {
// return time offset
return timeOffset;
}
}
@Override
public long getPlayerTimeOffset() {
return timeOffset;
}
@Override
public void resetPlayerTime() {
setPlayerTime(0, true);
}
/**
* Sends a {@link TimeMessage} with the time of day.
*/
public void sendTime() {
long time = getPlayerTime();
if (!playerTimeRelative || !world.getGameRuleMap().getBoolean(GameRules.DO_DAYLIGHT_CYCLE)) {
time *= -1; // negative value indicates fixed time
}
session.send(new TimeMessage(world.getFullTime(), time));
}
@Override
public WeatherType getPlayerWeather() {
return playerWeather;
}
@Override
public void setPlayerWeather(WeatherType type) {
playerWeather = type;
sendWeather();
}
@Override
public void resetPlayerWeather() {
playerWeather = null;
sendWeather();
sendRainDensity();
sendSkyDarkness();
}
/**
* Sends a {@link StateChangeMessage} with the current weather.
*/
public void sendWeather() {
boolean stormy = playerWeather == null ? getWorld().hasStorm()
: playerWeather == WeatherType.DOWNFALL;
session.send(new StateChangeMessage(stormy ? Reason.START_RAIN : Reason.STOP_RAIN, 0));
}
public void sendRainDensity() {
session.send(new StateChangeMessage(Reason.RAIN_DENSITY, getWorld().getRainDensity()));
}
////////////////////////////////////////////////////////////////////////////
// Player visibility
public void sendSkyDarkness() {
session.send(new StateChangeMessage(Reason.SKY_DARKNESS, getWorld().getSkyDarkness()));
}
@Override
public void hidePlayer(Player player) {
checkNotNull(player, "player cannot be null");
if (equals(player) || !player.isOnline() || !session.isActive()) {
return;
}
if (hiddenEntities.contains(player.getUniqueId())) {
return;
}
hiddenEntities.add(player.getUniqueId());
worldLock.writeLock().lock();
try {
if (knownEntities.remove(player)) {
session.send(new DestroyEntitiesMessage(Collections
.singletonList(player.getEntityId())));
}
} finally {
worldLock.writeLock().unlock();
}
session.send(UserListItemMessage.removeOne(player.getUniqueId()));
}
@Override
public void hidePlayer(Plugin plugin, Player player) {
hidePlayer(player); // call old
}
@Override
public void showPlayer(Player player) {
checkNotNull(player, "player cannot be null");
if (equals(player) || !player.isOnline() || !session.isActive()) {
return;
}
if (!hiddenEntities.contains(player.getUniqueId())) {
return;
}
hiddenEntities.remove(player.getUniqueId());
session.send(new UserListItemMessage(UserListItemMessage.Action.ADD_PLAYER, ((GlowPlayer)
player)
.getUserListEntry()));
}
@Override
public void showPlayer(Plugin plugin, Player player) {
showPlayer(player); // call old
}
@Override
public boolean canSee(Player player) {
return !hiddenEntities.contains(player.getUniqueId());
}
////////////////////////////////////////////////////////////////////////////
// Scoreboard
/**
* Called when a player hidden to this player disconnects. This is necessary so the player is
* visible again after they reconnected.
*
* @param player The disconnected player
*/
public void stopHidingDisconnectedPlayer(Player player) {
hiddenEntities.remove(player.getUniqueId());
}
@Override
public Scoreboard getScoreboard() {
return scoreboard;
}
////////////////////////////////////////////////////////////////////////////
// Conversable
@Override
public void setScoreboard(
Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException {
checkNotNull(scoreboard, "Scoreboard must not be null");
if (!(scoreboard instanceof GlowScoreboard)) {
throw new IllegalArgumentException("Scoreboard must be GlowScoreboard");
}
if (this.scoreboard == null) {
throw new IllegalStateException("Player has not loaded or is already offline");
}
this.scoreboard.unsubscribe(this);
this.scoreboard = (GlowScoreboard) scoreboard;
this.scoreboard.subscribe(this);
}
@Override
public boolean isConversing() {
return !conversations.isEmpty();
}
@Override
public void acceptConversationInput(String input) {
conversations.get(0).acceptInput(input);
}
@Override
public boolean beginConversation(Conversation conversation) {
boolean noQueue = conversations.isEmpty();
conversations.add(conversation);
if (noQueue) {
conversation.begin();
}
return noQueue;
}
@Override
public void abandonConversation(Conversation conversation) {
abandonConversation(conversation, null);
}
@Override
public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) {
conversations.remove(conversation);
if (details == null) {
conversation.abandon();
} else {
conversation.abandon(details);
}
}
////////////////////////////////////////////////////////////////////////////
// Plugin messages
@Override
public void sendPluginMessage(Plugin source, String channel, byte[] message) {
StandardMessenger
.validatePluginMessage(getServer().getMessenger(), source, channel, message);
if (listeningChannels.contains(channel)) {
// only send if player is listening for it
session.send(new PluginMessage(channel, message));
}
}
@Override
public Set<String> getListeningPluginChannels() {
return Collections.unmodifiableSet(listeningChannels);
}
/**
* Add a listening channel to this player.
*
* @param channel The channel to add.
*/
public void addChannel(String channel) {
checkArgument(listeningChannels.size() < 128, "Cannot add more than 127 channels!");
if (listeningChannels.add(channel)) {
EventFactory.getInstance().callEvent(new PlayerRegisterChannelEvent(this, channel));
}
}
/**
* Remove a listening channel from this player.
*
* @param channel The channel to remove.
*/
public void removeChannel(String channel) {
if (listeningChannels.remove(channel)) {
EventFactory.getInstance().callEvent(new PlayerUnregisterChannelEvent(this, channel));
}
}
/**
* Send the supported plugin channels to the client.
*/
private void sendSupportedChannels() {
Set<String> listening = server.getMessenger().getIncomingChannels();
if (!listening.isEmpty()) {
// send NUL-separated list of channels we support
ByteBuf buf = Unpooled.buffer(16 * listening.size());
for (String channel : listening) {
buf.writeBytes(channel.getBytes(StandardCharsets.UTF_8));
buf.writeByte(0);
}
session.sendAndRelease(new PluginMessage("REGISTER", buf.array()), buf); // NON-NLS
}
}
/**
* Updates level after enchanting.
*
* @param clicked the enchanting-table slot used: 0 for top, 1 for middle, 2 for bottom
*/
public void enchanted(int clicked) {
int newLevel = level - clicked - 1;
if (newLevel < 0) {
setExp(0);
setTotalExperience(0);
}
setLevel(newLevel);
setXpSeed(ThreadLocalRandom.current().nextInt()); //TODO use entity's random instance?
}
////////////////////////////////////////////////////////////////////////////
// Titles
public Title getTitle() {
return currentTitle.build();
}
public void clearTitle() {
session.send(new TitleMessage(Action.CLEAR));
}
@Override
public void setOnGround(boolean onGround) {
super.setOnGround(onGround);
int fallDistance = Math.round(getFallDistance());
this.incrementStatistic(Statistic.FALL_ONE_CM, fallDistance);
}
@Override
public void resetTitle() {
currentTitle = new Title.Builder();
session.send(new TitleMessage(Action.RESET));
}
/**
* Starts breaking a block.
*
* @param block the block to start breaking
*/
public void setDigging(GlowBlock block) {
if (Objects.equals(block, digging)) {
return;
}
if (block == null) {
totalDiggingTicks = Long.MAX_VALUE;
// remove the animation
broadcastBlockBreakAnimation(digging, 10);
} else {
double hardness = block.getMaterialValues().getHardness();
if (hardness >= Float.MAX_VALUE) {
// This block can't be broken by digging.
setDigging(null);
return;
}
double breakingTimeMultiplier = 5; // default of 5 when using bare hands
ItemStack tool = getItemInHand();
if (tool != null) {
Material toolType = tool.getType();
if (block.getType() == Material.COBWEB && ToolType.SWORD.matches(toolType)) {
breakingTimeMultiplier = 0.1;
} else if (MaterialUtil.WOOLS.contains(block.getType())
&& toolType == Material.SHEARS) {
breakingTimeMultiplier = 0.3;
} else {
ToolType effectiveTool = block.getMaterialValues().getTool();
if (effectiveTool != null && effectiveTool.matches(toolType)) {
double miningMultiplier = ToolType.getMiningMultiplier(toolType);
int efficiencyLevel = tool.getEnchantmentLevel(Enchantment.DIG_SPEED);
if (efficiencyLevel > 0) {
miningMultiplier += efficiencyLevel * efficiencyLevel + 1;
}
breakingTimeMultiplier = 1.5 / miningMultiplier;
} else if (effectiveTool == null
|| !effectiveTool.matches(Material.DIAMOND_PICKAXE)) {
// If the current tool isn't optimal but can still mine the block, the
// multiplier is 1.5. Here, we assume for simplicity that this is true of
// all non-pickaxe blocks.
// FIXME: Does this always match vanilla?
breakingTimeMultiplier = 1.5;
}
}
}
// TODO: status effects (e.g. Mining Fatigue, Slowness); effect of underwater digging
totalDiggingTicks = (long)
(breakingTimeMultiplier * hardness * 20.0 + 0.5); // seconds to ticks, round half-up
// show other clients the block is beginning to crack
broadcastBlockBreakAnimation(block, 0);
}
diggingTicks = 0;
digging = block;
}
private void sendBlockBreakAnimation(Location loc, int destroyStage) {
afterBlockChanges
.add(new BlockBreakAnimationMessage(this.getEntityId(), loc.getBlockX(), loc
.getBlockY(), loc.getBlockZ(), destroyStage));
}
private void broadcastBlockBreakAnimation(GlowBlock block, int destroyStage) {
GlowChunk.Key key = GlowChunk.Key.of(block.getX() >> 4, block.getZ() >> 4);
block.getWorld().getRawPlayers().stream()
.filter(player -> player != this && player.canSeeChunk(key))
.forEach(player -> player
.sendBlockBreakAnimation(block.getLocation(), destroyStage));
}
private void pulseDigging() {
++diggingTicks;
if (diggingTicks <= totalDiggingTicks) {
// diggingTicks starts at 1 and progresses to totalDiggingTicks, but animation stages
// are 0 through 9, so subtract 1 from the current tick
int stage = (int) (10.0 * ((double) (diggingTicks - 1)) / totalDiggingTicks);
broadcastBlockBreakAnimation(digging, stage);
return;
}
ItemStack tool = getItemInHand();
short durability = tool.getDurability();
short maxDurability = tool.getType().getMaxDurability();
if (!InventoryUtil.isEmpty(tool) && maxDurability != 0 && durability != maxDurability) {
int baseDamage; // Before applying unbreaking enchantment
switch (digging.getType()) {
case GRASS_BLOCK:
case DIRT:
case SAND:
case GRAVEL:
case MYCELIUM:
case SOUL_SAND:
baseDamage = ToolType.SHOVEL.matches(tool.getType()) ? 1 : 2;
break;
case OAK_LOG:
case DARK_OAK_LOG:
case ACACIA_LOG:
case BIRCH_LOG:
case JUNGLE_LOG:
case SPRUCE_LOG:
case OAK_WOOD:
case DARK_OAK_WOOD:
case ACACIA_WOOD:
case BIRCH_WOOD:
case JUNGLE_WOOD:
case SPRUCE_WOOD:
case CHEST:
baseDamage = ToolType.AXE.matches(tool.getType()) ? 1 : 2;
break;
case STONE:
case COBBLESTONE:
baseDamage = ToolType.PICKAXE.matches(tool.getType()) ? 1 : 2;
break;
default:
baseDamage = 2;
break;
}
for (int i = 0; i < baseDamage; i++) {
tool = InventoryUtil.damageItem(this, tool);
}
}
// Force-update item
setItemInHand(tool);
// Break the block
digging.breakNaturally(tool);
// Send block status to clients
Location dugLocation = digging.getLocation();
// OK to use sequential stream here, because sendBlockChange is async
world.getRawPlayers().stream()
.filter(player -> player.canSeeChunk(GlowChunk.Key.to(dugLocation.getChunk())))
.forEach(player -> player.sendBlockChange(dugLocation, Material.AIR, (byte) 0));
setDigging(null);
}
/**
* Returns true if the player is inside a water block.
*
* @return True if entity is in water.
*/
public boolean isInWater() {
Material mat = getLocation().getBlock().getType();
return mat == Material.WATER;
}
public void playAnimationToSelf(EntityAnimation animation) {
AnimateEntityMessage message = new AnimateEntityMessage(SELF_ID, animation.ordinal());
getSession().send(message);
}
/**
* Add a boss bar.
*
* @param bar the boss bar to add
*/
public void addBossBar(BossBar bar) {
bossBars.add(bar);
}
/**
* Remove a boss bar.
*
* @param bar the boss bar to remove
*/
public void removeBossBar(BossBar bar) {
bossBars.remove(bar);
}
/**
* Returns a collection of the boss bars this player sees.
*
* @return the boss bars this player sees
*/
public Collection<BossBar> getBossBars() {
return new ArrayList<>(bossBars);
}
/**
* Gets the currently open window ID.
*
* @return the currently open window ID, -1 if there is no open window
*/
public int getOpenWindowId() {
if (invMonitor == null) {
return -1;
}
return invMonitor.getId();
}
/**
* Removes the current fishing hook, if any, and sets a new one.
*
* @param fishingHook the new fishing hook, or null to stop fishing
*/
public void setCurrentFishingHook(GlowFishingHook fishingHook) {
GlowFishingHook oldHook = currentFishingHook.getAndSet(fishingHook);
if (oldHook != null && !(oldHook.equals(fishingHook)) && !oldHook.isDead()) {
oldHook.remove();
}
}
}