/FileExtraction/Java_unseen/Wind-Development_WindSpigot/WindSpigot-Server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
package org.bukkit.craftbukkit.entity;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.Validate;
import org.bukkit.Achievement;
import org.bukkit.BanList;
import org.bukkit.Effect;
import org.bukkit.GameMode;
import org.bukkit.Instrument;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Note;
import org.bukkit.OfflinePlayer;
import org.bukkit.Sound;
import org.bukkit.Statistic;
import org.bukkit.Statistic.Type;
import org.bukkit.WeatherType;
import org.bukkit.World;
import org.bukkit.configuration.serialization.DelegateDeserialization;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.conversations.ManuallyAbandonedConversationCanceller;
import org.bukkit.craftbukkit.CraftEffect;
import org.bukkit.craftbukkit.CraftOfflinePlayer;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.CraftSound;
import org.bukkit.craftbukkit.CraftStatistic;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.block.CraftSign;
import org.bukkit.craftbukkit.conversations.ConversationTracker;
import org.bukkit.craftbukkit.map.CraftMapView;
import org.bukkit.craftbukkit.map.RenderData;
import org.bukkit.craftbukkit.scoreboard.CraftScoreboard;
import org.bukkit.craftbukkit.util.CraftChatMessage;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerRegisterChannelEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerUnregisterChannelEvent;
import org.bukkit.inventory.InventoryView.Property;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.messaging.StandardMessenger;
import org.bukkit.scoreboard.Scoreboard;
// PaperSpigot start
import org.github.paperspigot.Title;
// PaperSpigot end

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.mojang.authlib.GameProfile;

import ga.windpvp.windspigot.WindSpigot;
import ga.windpvp.windspigot.cache.Constants;
import ga.windpvp.windspigot.config.WindSpigotConfig;
import io.netty.buffer.Unpooled;
import net.md_5.bungee.api.chat.BaseComponent;
import net.minecraft.server.AttributeInstance;
import net.minecraft.server.AttributeMapServer;
import net.minecraft.server.AttributeModifiable;
import net.minecraft.server.AttributeRanged;
import net.minecraft.server.BlockPosition;
import net.minecraft.server.ChatComponentText;
import net.minecraft.server.Container;
import net.minecraft.server.Entity;
import net.minecraft.server.EntityArrow;
import net.minecraft.server.EntityHuman;
import net.minecraft.server.EntityPlayer;
import net.minecraft.server.EntityProjectile;
import net.minecraft.server.EntityTracker;
import net.minecraft.server.EntityTrackerEntry;
import net.minecraft.server.IAttribute;
import net.minecraft.server.IChatBaseComponent;
import net.minecraft.server.MapIcon;
import net.minecraft.server.MathHelper;
import net.minecraft.server.MobEffectList;
import net.minecraft.server.NBTTagCompound;
import net.minecraft.server.Packet;
import net.minecraft.server.PacketDataSerializer;
import net.minecraft.server.PacketPlayOutBlockChange;
import net.minecraft.server.PacketPlayOutChat;
import net.minecraft.server.PacketPlayOutCustomPayload;
import net.minecraft.server.PacketPlayOutGameStateChange;
import net.minecraft.server.PacketPlayOutMap;
import net.minecraft.server.PacketPlayOutNamedSoundEffect;
import net.minecraft.server.PacketPlayOutPlayerInfo;
import net.minecraft.server.PacketPlayOutPlayerListHeaderFooter;
import net.minecraft.server.PacketPlayOutSpawnPosition;
import net.minecraft.server.PacketPlayOutTitle;
import net.minecraft.server.PacketPlayOutTitle.EnumTitleAction;
import net.minecraft.server.PacketPlayOutUpdateAttributes;
import net.minecraft.server.PacketPlayOutUpdateHealth;
import net.minecraft.server.PacketPlayOutUpdateSign;
import net.minecraft.server.PacketPlayOutWorldEvent;
import net.minecraft.server.PacketPlayOutWorldParticles;
import net.minecraft.server.PlayerConnection;
import net.minecraft.server.WorldServer;
import net.minecraft.server.WorldSettings;

@DelegateDeserialization(CraftOfflinePlayer.class)
public class CraftPlayer extends CraftHumanEntity implements Player {
	private long firstPlayed = 0;
	private long lastPlayed = 0;
	private boolean hasPlayedBefore = false;
	private final ConversationTracker conversationTracker = new ConversationTracker();
	private final Set<String> channels = new HashSet<String>();
	private final Set<UUID> hiddenPlayers = Sets.newConcurrentHashSet(); // WindSpigot - concurrent collection
	private int hash = 0;
	private double health = 20;
	private boolean scaledHealth = false;
	private double healthScale = 20;

	public CraftPlayer(CraftServer server, EntityPlayer entity) {
		super(server, entity);

		firstPlayed = System.currentTimeMillis();
	}

	public GameProfile getProfile() {
		return getHandle().getProfile();
	}

	@Override
	public boolean isOp() {
		return server.getHandle().isOp(getProfile());
	}

	@Override
	public void setOp(boolean value) {
		if (value == isOp()) {
			return;
		}

		if (value) {
			server.getHandle().addOp(getProfile());
		} else {
			server.getHandle().removeOp(getProfile());
		}

		perm.recalculatePermissions();
	}

	@Override
	public boolean isOnline() {
		return server.getHandle().uuidMap.get(getUniqueId()) != null; // PaperSpigot - replace whole method
	}

	@Override
	public InetSocketAddress getAddress() {
		if (getHandle().playerConnection == null) {
			return null;
		}

		SocketAddress addr = getHandle().playerConnection.networkManager.getSocketAddress();
		if (addr instanceof InetSocketAddress) {
			return (InetSocketAddress) addr;
		} else {
			return null;
		}
	}

	@Override
	public double getEyeHeight() {
		return getEyeHeight(false);
	}

	@Override
	public double getEyeHeight(boolean ignoreSneaking) {
		if (ignoreSneaking) {
			return 1.62D;
		} else if (isSneaking()) {
			return 1.54D;
		} else {
			return 1.62D;
		}
	}

	@Override
	public void sendRawMessage(String message) {
		if (getHandle().playerConnection == null) {
			return;
		}

		for (IChatBaseComponent component : CraftChatMessage.fromString(message)) {
			getHandle().playerConnection.sendPacket(new PacketPlayOutChat(component));
		}
	}

	@Override
	public void sendMessage(String message) {
		if (!conversationTracker.isConversingModaly()) {
			this.sendRawMessage(message);
		}
	}

	@Override
	public void sendMessage(String[] messages) {
		for (String message : messages) {
			sendMessage(message);
		}
	}

	// Paper start
	@Override
	public void sendMessage(BaseComponent component) {
		sendMessage(new BaseComponent[] { component });
	}

	@Override
	public void sendMessage(BaseComponent... components) {
		if (getHandle().playerConnection == null) {
			return;
		}

		PacketPlayOutChat packet = new PacketPlayOutChat();
		packet.components = components;
		getHandle().playerConnection.sendPacket(packet);
	}

	@Override
	public void setPlayerListHeaderFooter(BaseComponent[] header, BaseComponent[] footer) {
		PacketPlayOutPlayerListHeaderFooter packet = new PacketPlayOutPlayerListHeaderFooter();
		packet.header = header;
		packet.footer = footer;
		getHandle().playerConnection.sendPacket(packet);
	}

	@Override
	public void setPlayerListHeaderFooter(BaseComponent header, BaseComponent footer) {
		this.setPlayerListHeaderFooter(header == null ? null : new BaseComponent[] { header },
				footer == null ? null : new BaseComponent[] { footer });
	}

	@Override
	public void setTitleTimes(int fadeInTicks, int stayTicks, int fadeOutTicks) {
		getHandle().playerConnection.sendPacket(new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.TIMES,
				(BaseComponent[]) null, fadeInTicks, stayTicks, fadeOutTicks));
	}

	@Override
	public void setSubtitle(BaseComponent[] subtitle) {
		getHandle().playerConnection
				.sendPacket(new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.SUBTITLE, subtitle, 0, 0, 0));
	}

	@Override
	public void setSubtitle(BaseComponent subtitle) {
		setSubtitle(new BaseComponent[] { subtitle });
	}

	@Override
	public void showTitle(BaseComponent[] title) {
		getHandle().playerConnection
				.sendPacket(new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.TITLE, title, 0, 0, 0));
	}

	@Override
	public void showTitle(BaseComponent title) {
		showTitle(new BaseComponent[] { title });
	}

	@Override
	public void showTitle(BaseComponent[] title, BaseComponent[] subtitle, int fadeInTicks, int stayTicks,
			int fadeOutTicks) {
		setTitleTimes(fadeInTicks, stayTicks, fadeOutTicks);
		setSubtitle(subtitle);
		showTitle(title);
	}

	@Override
	public void showTitle(BaseComponent title, BaseComponent subtitle, int fadeInTicks, int stayTicks,
			int fadeOutTicks) {
		setTitleTimes(fadeInTicks, stayTicks, fadeOutTicks);
		setSubtitle(subtitle);
		showTitle(title);
	}

	@Override
	public void sendTitle(Title title) {
		Preconditions.checkNotNull(title, "Title is null");
		setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut());
		setSubtitle(title.getSubtitle() == null ? new BaseComponent[0] : title.getSubtitle());
		showTitle(title.getTitle());
	}

	@Override
	public void updateTitle(Title title) {
		Preconditions.checkNotNull(title, "Title is null");
		setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut());
		if (title.getSubtitle() != null) {
			setSubtitle(title.getSubtitle());
		}
		showTitle(title.getTitle());
	}

	@Override
	public void hideTitle() {
		getHandle().playerConnection.sendPacket(
				new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.CLEAR, (BaseComponent[]) null, 0, 0, 0));
	}
	// Paper end

	@Override
	public String getDisplayName() {
		return getHandle().displayName;
	}

	@Override
	public void setDisplayName(final String name) {
		getHandle().displayName = name == null ? getName() : name;
	}

	@Override
	public String getPlayerListName() {
		return getHandle().listName == null ? getName() : CraftChatMessage.fromComponent(getHandle().listName);
	}

	@Override
	public void setPlayerListName(String name) {
		if (name == null) {
			name = getName();
		}
		getHandle().listName = name.equals(getName()) ? null : CraftChatMessage.fromString(name)[0];
		for (EntityPlayer player : server.getHandle().players) {
			if (player.getBukkitEntity().canSee(this)) {
				player.playerConnection.sendPacket(new PacketPlayOutPlayerInfo(
						PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_DISPLAY_NAME, getHandle()));
			}
		}
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof OfflinePlayer)) {
			return false;
		}
		OfflinePlayer other = (OfflinePlayer) obj;
		if ((this.getUniqueId() == null) || (other.getUniqueId() == null)) {
			return false;
		}

		boolean uuidEquals = this.getUniqueId().equals(other.getUniqueId());
		boolean idEquals = true;

		if (other instanceof CraftPlayer) {
			idEquals = this.getEntityId() == ((CraftPlayer) other).getEntityId();
		}

		return uuidEquals && idEquals;
	}

	@Override
	public void kickPlayer(String message) {
		org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot
		if (getHandle().playerConnection == null) {
			return;
		}

		getHandle().playerConnection.disconnect(message == null ? "" : message);
	}

	@Override
	public void setCompassTarget(Location loc) {
		if (getHandle().playerConnection == null) {
			return;
		}

		// Do not directly assign here, from the packethandler we'll assign it.
		getHandle().playerConnection.sendPacket(
				new PacketPlayOutSpawnPosition(new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ())));
	}

	@Override
	public Location getCompassTarget() {
		return getHandle().compassTarget;
	}

	@Override
	public void chat(String msg) {
		if (getHandle().playerConnection == null) {
			return;
		}

		getHandle().playerConnection.chat(msg, false);
	}

	@Override
	public boolean performCommand(String command) {
		return server.dispatchCommand(this, command);
	}

	@Override
	public void playNote(Location loc, byte instrument, byte note) {
		if (getHandle().playerConnection == null) {
			return;
		}

		String instrumentName = null;
		switch (instrument) {
		case 0:
			instrumentName = "harp";
			break;
		case 1:
			instrumentName = "bd";
			break;
		case 2:
			instrumentName = "snare";
			break;
		case 3:
			instrumentName = "hat";
			break;
		case 4:
			instrumentName = "bassattack";
			break;
		}

		float f = (float) Math.pow(2.0D, (note - 12.0D) / 12.0D);
		getHandle().playerConnection.sendPacket(new PacketPlayOutNamedSoundEffect("note." + instrumentName,
				loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), 3.0f, f));
	}

	@Override
	public void playNote(Location loc, Instrument instrument, Note note) {
		if (getHandle().playerConnection == null) {
			return;
		}

		String instrumentName = null;
		switch (instrument.ordinal()) {
		case 0:
			instrumentName = "harp";
			break;
		case 1:
			instrumentName = "bd";
			break;
		case 2:
			instrumentName = "snare";
			break;
		case 3:
			instrumentName = "hat";
			break;
		case 4:
			instrumentName = "bassattack";
			break;
		}
		float f = (float) Math.pow(2.0D, (note.getId() - 12.0D) / 12.0D);
		getHandle().playerConnection.sendPacket(new PacketPlayOutNamedSoundEffect("note." + instrumentName,
				loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), 3.0f, f));
	}

	@Override
	public void playSound(Location loc, Sound sound, float volume, float pitch) {
		if (sound == null) {
			return;
		}
		playSound(loc, CraftSound.getSound(sound), volume, pitch);
	}

	@Override
	public void playSound(Location loc, String sound, float volume, float pitch) {
		if (loc == null || sound == null || getHandle().playerConnection == null) {
			return;
		}

		double x = loc.getBlockX() + 0.5;
		double y = loc.getBlockY() + 0.5;
		double z = loc.getBlockZ() + 0.5;

		PacketPlayOutNamedSoundEffect packet = new PacketPlayOutNamedSoundEffect(sound, x, y, z, volume, pitch);
		getHandle().playerConnection.sendPacket(packet);
	}

	@Override
	public void playEffect(Location loc, Effect effect, int data) {
		if (getHandle().playerConnection == null) {
			return;
		}

		spigot().playEffect(loc, effect, data, 0, 0, 0, 0, 1, 1, 64); // Spigot
	}

	@Override
	public <T> void playEffect(Location loc, Effect effect, T data) {
		if (data != null) {
			Validate.isTrue(data.getClass().isAssignableFrom(effect.getData()), "Wrong kind of data for this effect!");
		} else {
			Validate.isTrue(effect.getData() == null, "Wrong kind of data for this effect!");
		}

		int datavalue = data == null ? 0 : CraftEffect.getDataValue(effect, data);
		playEffect(loc, effect, datavalue);
	}

	@Override
	public void sendBlockChange(Location loc, Material material, byte data) {
		sendBlockChange(loc, material.getId(), data);
	}

	@Override
	public void sendBlockChange(Location loc, int material, byte data) {
		if (getHandle().playerConnection == null) {
			return;
		}

		PacketPlayOutBlockChange packet = new PacketPlayOutBlockChange(((CraftWorld) loc.getWorld()).getHandle(),
				new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));

		packet.block = CraftMagicNumbers.getBlock(material).fromLegacyData(data);
		getHandle().playerConnection.sendPacket(packet);
	}

	@Override
	public void sendSignChange(Location loc, String[] lines) {
		if (getHandle().playerConnection == null) {
			return;
		}

		if (lines == null) {
			lines = new String[4];
		}

		Validate.notNull(loc, "Location can not be null");
		if (lines.length < 4) {
			throw new IllegalArgumentException("Must have at least 4 lines");
		}

		IChatBaseComponent[] components = CraftSign.sanitizeLines(lines);

		getHandle().playerConnection.sendPacket(new PacketPlayOutUpdateSign(getHandle().world,
				new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()), components));
	}

	@Override
	public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) {
		if (getHandle().playerConnection == null) {
			return false;
		}

		/*
		 * int x = loc.getBlockX(); int y = loc.getBlockY(); int z = loc.getBlockZ();
		 * 
		 * int cx = x >> 4; int cz = z >> 4;
		 * 
		 * if (sx <= 0 || sy <= 0 || sz <= 0) { return false; }
		 * 
		 * if ((x + sx - 1) >> 4 != cx || (z + sz - 1) >> 4 != cz || y < 0 || y + sy >
		 * 128) { return false; }
		 * 
		 * if (data.length != (sx * sy * sz * 5) / 2) { return false; }
		 * 
		 * Packet51MapChunk packet = new Packet51MapChunk(x, y, z, sx, sy, sz, data);
		 * 
		 * getHandle().playerConnection.sendPacket(packet);
		 * 
		 * return true;
		 */

		throw new NotImplementedException("Chunk changes do not yet work"); // TODO: Chunk changes.
	}

	@Override
	public void sendMap(MapView map) {
		if (getHandle().playerConnection == null) {
			return;
		}

		RenderData data = ((CraftMapView) map).render(this);
		Collection<MapIcon> icons = new ArrayList<MapIcon>();
		for (MapCursor cursor : data.cursors) {
			if (cursor.isVisible()) {
				icons.add(new MapIcon(cursor.getRawType(), cursor.getX(), cursor.getY(), cursor.getDirection()));
			}
		}

		PacketPlayOutMap packet = new PacketPlayOutMap(map.getId(), map.getScale().getValue(), icons, data.buffer, 0, 0,
				0, 0);
		getHandle().playerConnection.sendPacket(packet);
	}

	@Override
	public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) {
		EntityPlayer entity = getHandle();

		if (getHealth() == 0 || entity.dead) {
			return false;
		}

		if (entity.playerConnection == null || entity.playerConnection.isDisconnected()) {
			return false;
		}

		if (entity.passenger != null) {
			return false;
		}

		// From = Players current Location
		Location from = this.getLocation();
		// To = Players new Location if Teleport is Successful
		Location to = location;
		// Create & Call the Teleport Event.
		PlayerTeleportEvent event = new PlayerTeleportEvent(this, from, to, cause);
		server.getPluginManager().callEvent(event);

		// Return False to inform the Plugin that the Teleport was
		// unsuccessful/cancelled.
		if (event.isCancelled()) {
			return false;
		}
		
        WindSpigot.getInstance().getLagCompensator().registerMovement(this, to); // Nacho

		// If this player is riding another entity, we must dismount before teleporting.
		entity.mount(null);

		// Update the From Location
		from = event.getFrom();
		// Grab the new To Location dependent on whether the event was cancelled.
		to = event.getTo();
		// Grab the To and From World Handles.
		WorldServer fromWorld = ((CraftWorld) from.getWorld()).getHandle();
		WorldServer toWorld = ((CraftWorld) to.getWorld()).getHandle();

		// Close any foreign inventory
		if (getHandle().activeContainer != getHandle().defaultContainer) {
			getHandle().closeInventory();
		}

		// Check if the fromWorld and toWorld are the same.
		if (fromWorld == toWorld) {
			entity.playerConnection.teleport(to);
		} else {
			server.getHandle().moveToWorld(entity, toWorld.dimension, true, to, true);
		}
		return true;
	}

	@Override
	public void setSneaking(boolean sneak) {
		getHandle().setSneaking(sneak);
	}

	@Override
	public boolean isSneaking() {
		return getHandle().isSneaking();
	}

	@Override
	public boolean isSprinting() {
		return getHandle().isSprinting();
	}

	@Override
	public void setSprinting(boolean sprinting) {
		getHandle().setSprinting(sprinting);
	}

	@Override
	public void loadData() {
		server.getHandle().playerFileData.load(getHandle());
	}

	@Override
	public void saveData() {
		server.getHandle().playerFileData.save(getHandle());
	}

	@Deprecated
	@Override
	public void updateInventory() {
		getHandle().updateInventory(getHandle().activeContainer);
	}

	@Override
	public void setSleepingIgnored(boolean isSleeping) {
		getHandle().fauxSleeping = isSleeping;
		((CraftWorld) getWorld()).getHandle().checkSleepStatus();
	}

	@Override
	public boolean isSleepingIgnored() {
		return getHandle().fauxSleeping;
	}

	@Override
	public void awardAchievement(Achievement achievement) {
		Validate.notNull(achievement, "Achievement cannot be null");
		if (achievement.hasParent() && !hasAchievement(achievement.getParent())) {
			awardAchievement(achievement.getParent());
		}
		getHandle().getStatisticManager().setStatistic(getHandle(), CraftStatistic.getNMSAchievement(achievement), 1);
		getHandle().getStatisticManager().updateStatistics(getHandle());
	}

	@Override
	public void removeAchievement(Achievement achievement) {
		Validate.notNull(achievement, "Achievement cannot be null");
		for (Achievement achieve : Achievement.values()) {
			if (achieve.getParent() == achievement && hasAchievement(achieve)) {
				removeAchievement(achieve);
			}
		}
		getHandle().getStatisticManager().setStatistic(getHandle(), CraftStatistic.getNMSAchievement(achievement), 0);
	}

	@Override
	public boolean hasAchievement(Achievement achievement) {
		Validate.notNull(achievement, "Achievement cannot be null");
		return getHandle().getStatisticManager().hasAchievement(CraftStatistic.getNMSAchievement(achievement));
	}

	@Override
	public void incrementStatistic(Statistic statistic) {
		incrementStatistic(statistic, 1);
	}

	@Override
	public void decrementStatistic(Statistic statistic) {
		decrementStatistic(statistic, 1);
	}

	@Override
	public int getStatistic(Statistic statistic) {
		Validate.notNull(statistic, "Statistic cannot be null");
		Validate.isTrue(statistic.getType() == Type.UNTYPED, "Must supply additional paramater for this statistic");
		return getHandle().getStatisticManager().getStatisticValue(CraftStatistic.getNMSStatistic(statistic));
	}

	@Override
	public void incrementStatistic(Statistic statistic, int amount) {
		Validate.isTrue(amount > 0, "Amount must be greater than 0");
		setStatistic(statistic, getStatistic(statistic) + amount);
	}

	@Override
	public void decrementStatistic(Statistic statistic, int amount) {
		Validate.isTrue(amount > 0, "Amount must be greater than 0");
		setStatistic(statistic, getStatistic(statistic) - amount);
	}

	@Override
	public void setStatistic(Statistic statistic, int newValue) {
		Validate.notNull(statistic, "Statistic cannot be null");
		Validate.isTrue(statistic.getType() == Type.UNTYPED, "Must supply additional paramater for this statistic");
		Validate.isTrue(newValue >= 0, "Value must be greater than or equal to 0");
		net.minecraft.server.Statistic nmsStatistic = CraftStatistic.getNMSStatistic(statistic);
		getHandle().getStatisticManager().setStatistic(getHandle(), nmsStatistic, newValue);
	}

	@Override
	public void incrementStatistic(Statistic statistic, Material material) {
		incrementStatistic(statistic, material, 1);
	}

	@Override
	public void decrementStatistic(Statistic statistic, Material material) {
		decrementStatistic(statistic, material, 1);
	}

	@Override
	public int getStatistic(Statistic statistic, Material material) {
		Validate.notNull(statistic, "Statistic cannot be null");
		Validate.notNull(material, "Material cannot be null");
		Validate.isTrue(statistic.getType() == Type.BLOCK || statistic.getType() == Type.ITEM,
				"This statistic does not take a Material parameter");
		net.minecraft.server.Statistic nmsStatistic = CraftStatistic.getMaterialStatistic(statistic, material);
		Validate.notNull(nmsStatistic, "The supplied Material does not have a corresponding statistic");
		return getHandle().getStatisticManager().getStatisticValue(nmsStatistic);
	}

	@Override
	public void incrementStatistic(Statistic statistic, Material material, int amount) {
		Validate.isTrue(amount > 0, "Amount must be greater than 0");
		setStatistic(statistic, material, getStatistic(statistic, material) + amount);
	}

	@Override
	public void decrementStatistic(Statistic statistic, Material material, int amount) {
		Validate.isTrue(amount > 0, "Amount must be greater than 0");
		setStatistic(statistic, material, getStatistic(statistic, material) - amount);
	}

	@Override
	public void setStatistic(Statistic statistic, Material material, int newValue) {
		Validate.notNull(statistic, "Statistic cannot be null");
		Validate.notNull(material, "Material cannot be null");
		Validate.isTrue(newValue >= 0, "Value must be greater than or equal to 0");
		Validate.isTrue(statistic.getType() == Type.BLOCK || statistic.getType() == Type.ITEM,
				"This statistic does not take a Material parameter");
		net.minecraft.server.Statistic nmsStatistic = CraftStatistic.getMaterialStatistic(statistic, material);
		Validate.notNull(nmsStatistic, "The supplied Material does not have a corresponding statistic");
		getHandle().getStatisticManager().setStatistic(getHandle(), nmsStatistic, newValue);
	}

	@Override
	public void incrementStatistic(Statistic statistic, EntityType entityType) {
		incrementStatistic(statistic, entityType, 1);
	}

	@Override
	public void decrementStatistic(Statistic statistic, EntityType entityType) {
		decrementStatistic(statistic, entityType, 1);
	}

	@Override
	public int getStatistic(Statistic statistic, EntityType entityType) {
		Validate.notNull(statistic, "Statistic cannot be null");
		Validate.notNull(entityType, "EntityType cannot be null");
		Validate.isTrue(statistic.getType() == Type.ENTITY, "This statistic does not take an EntityType parameter");
		net.minecraft.server.Statistic nmsStatistic = CraftStatistic.getEntityStatistic(statistic, entityType);
		Validate.notNull(nmsStatistic, "The supplied EntityType does not have a corresponding statistic");
		return getHandle().getStatisticManager().getStatisticValue(nmsStatistic);
	}

	@Override
	public void incrementStatistic(Statistic statistic, EntityType entityType, int amount) {
		Validate.isTrue(amount > 0, "Amount must be greater than 0");
		setStatistic(statistic, entityType, getStatistic(statistic, entityType) + amount);
	}

	@Override
	public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) {
		Validate.isTrue(amount > 0, "Amount must be greater than 0");
		setStatistic(statistic, entityType, getStatistic(statistic, entityType) - amount);
	}

	@Override
	public void setStatistic(Statistic statistic, EntityType entityType, int newValue) {
		Validate.notNull(statistic, "Statistic cannot be null");
		Validate.notNull(entityType, "EntityType cannot be null");
		Validate.isTrue(newValue >= 0, "Value must be greater than or equal to 0");
		Validate.isTrue(statistic.getType() == Type.ENTITY, "This statistic does not take an EntityType parameter");
		net.minecraft.server.Statistic nmsStatistic = CraftStatistic.getEntityStatistic(statistic, entityType);
		Validate.notNull(nmsStatistic, "The supplied EntityType does not have a corresponding statistic");
		getHandle().getStatisticManager().setStatistic(getHandle(), nmsStatistic, newValue);
	}

	@Override
	public void setPlayerTime(long time, boolean relative) {
		getHandle().timeOffset = time;
		getHandle().relativeTime = relative;
	}

	@Override
	public long getPlayerTimeOffset() {
		return getHandle().timeOffset;
	}

	@Override
	public long getPlayerTime() {
		return getHandle().getPlayerTime();
	}

	@Override
	public boolean isPlayerTimeRelative() {
		return getHandle().relativeTime;
	}

	@Override
	public void resetPlayerTime() {
		setPlayerTime(0, true);
	}

	@Override
	public void setPlayerWeather(WeatherType type) {
		getHandle().setPlayerWeather(type, true);
	}

	@Override
	public WeatherType getPlayerWeather() {
		return getHandle().getPlayerWeather();
	}

	@Override
	public void resetPlayerWeather() {
		getHandle().resetPlayerWeather();
	}

	@Override
	public boolean isBanned() {
		return server.getBanList(BanList.Type.NAME).isBanned(getName());
	}

	@Override
	public void setBanned(boolean value) {
		if (value) {
			server.getBanList(BanList.Type.NAME).addBan(getName(), null, null, null);
		} else {
			server.getBanList(BanList.Type.NAME).pardon(getName());
		}
	}

	@Override
	public boolean isWhitelisted() {
		return server.getHandle().getWhitelist().isWhitelisted(getProfile());
	}

	@Override
	public void setWhitelisted(boolean value) {
		if (value) {
			server.getHandle().addWhitelist(getProfile());
		} else {
			server.getHandle().removeWhitelist(getProfile());
		}
	}

	@Override
	public void setGameMode(GameMode mode) {
		if (getHandle().playerConnection == null) {
			return;
		}

		if (mode == null) {
			throw new IllegalArgumentException("Mode cannot be null");
		}

		if (mode != getGameMode()) {
			PlayerGameModeChangeEvent event = new PlayerGameModeChangeEvent(this, mode);
			server.getPluginManager().callEvent(event);
			if (event.isCancelled()) {
				return;
			}

			getHandle().setSpectatorTarget(getHandle());
			getHandle().playerInteractManager.setGameMode(WorldSettings.EnumGamemode.getById(mode.getValue()));
			getHandle().fallDistance = 0;
			getHandle().playerConnection.sendPacket(new PacketPlayOutGameStateChange(3, mode.getValue()));
		}
	}

	@Override
	public GameMode getGameMode() {
		return GameMode.getByValue(getHandle().playerInteractManager.getGameMode().getId());
	}

	@Override
	public void giveExp(int exp) {
		getHandle().giveExp(exp);
	}

	@Override
	public void giveExpLevels(int levels) {
		getHandle().levelDown(levels);
	}

	@Override
	public float getExp() {
		return getHandle().exp;
	}

	@Override
	public void setExp(float exp) {
		getHandle().exp = exp;
		getHandle().lastSentExp = -1;
	}

	@Override
	public int getLevel() {
		return getHandle().expLevel;
	}

	@Override
	public void setLevel(int level) {
		getHandle().expLevel = level;
		getHandle().lastSentExp = -1;
	}

	@Override
	public int getTotalExperience() {
		return getHandle().expTotal;
	}

	@Override
	public void setTotalExperience(int exp) {
		getHandle().expTotal = exp;
	}

	@Override
	public float getExhaustion() {
		return getHandle().getFoodData().exhaustionLevel;
	}

	@Override
	public void setExhaustion(float value) {
		getHandle().getFoodData().exhaustionLevel = value;
	}

	@Override
	public float getSaturation() {
		return getHandle().getFoodData().saturationLevel;
	}

	@Override
	public void setSaturation(float value) {
		getHandle().getFoodData().saturationLevel = value;
	}

	@Override
	public int getFoodLevel() {
		return getHandle().getFoodData().foodLevel;
	}

	@Override
	public void setFoodLevel(int value) {
		getHandle().getFoodData().foodLevel = value;
	}

	@Override
	public Location getBedSpawnLocation() {
		World world = getServer().getWorld(getHandle().spawnWorld);
		BlockPosition bed = getHandle().getBed();

		if (world != null && bed != null) {
			bed = EntityHuman.getBed(((CraftWorld) world).getHandle(), bed, getHandle().isRespawnForced());
			if (bed != null) {
				return new Location(world, bed.getX(), bed.getY(), bed.getZ());
			}
		}
		return null;
	}

	@Override
	public void setBedSpawnLocation(Location location) {
		setBedSpawnLocation(location, false);
	}

	@Override
	public void setBedSpawnLocation(Location location, boolean override) {
		if (location == null) {
			getHandle().setRespawnPosition(null, override);
		} else {
			getHandle().setRespawnPosition(
					new BlockPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ()), override);
			getHandle().spawnWorld = location.getWorld().getName();
		}
	}

	@Override
	public void hidePlayer(Player player) {
		hidePlayer(player, true);
	}

	@Override
	public void hidePlayer(Player player, boolean onTab) {
		Validate.notNull(player, "hidden player cannot be null");
		if (getHandle().playerConnection == null) {
			return;
		}
		if (equals(player)) {
			return;
		}
		if (hiddenPlayers.contains(player.getUniqueId())) {
			return;
		}
		hiddenPlayers.add(player.getUniqueId());

		// remove this player from the hidden player's EntityTrackerEntry
		EntityTracker tracker = ((WorldServer) entity.world).tracker;
		EntityPlayer other = ((CraftPlayer) player).getHandle();
		
		EntityTrackerEntry entry = tracker.trackedEntities.get(other.getId());
		
		if (entry != null) {
			entry.clear(getHandle());
		}

		// remove the hidden player from this player user list
		if (onTab) {
			getHandle().playerConnection.sendPacket(
					new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, other));
		}
	}

	@Override
	public void showPlayer(Player player) {
		Validate.notNull(player, "shown player cannot be null");
		if (getHandle().playerConnection == null) {
			return;
		}
		if (equals(player)) {
			return;
		}
		if (!hiddenPlayers.contains(player.getUniqueId())) {
			return;
		}
		hiddenPlayers.remove(player.getUniqueId());

		EntityTracker tracker = ((WorldServer) entity.world).tracker;
		EntityPlayer other = ((CraftPlayer) player).getHandle();

		getHandle().playerConnection.sendPacket(
				new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, other));
		EntityTrackerEntry entry = tracker.trackedEntities.get(other.getId());
		
		if (entry != null && !entry.trackedPlayers.contains(getHandle())) {
			entry.updatePlayer(getHandle());
		}
	}

	public void removeDisconnectingPlayer(Player player) {
		hiddenPlayers.remove(player.getUniqueId());
	}

	@Override
	public boolean canSee(Player player) {
		return !hiddenPlayers.contains(player.getUniqueId());
	}

	@Override
	public boolean canSee(org.bukkit.entity.Entity entity) {
		Entity nmsEntity = ((CraftEntity) entity).getHandle();
		if (nmsEntity instanceof EntityPlayer) {
			return this.canSee((Player) entity);
		}

		// Projectiles from hidden players
		if (WindSpigotConfig.hideProjectilesFromHiddenPlayers) {
			if (nmsEntity instanceof EntityProjectile) {
				EntityProjectile entityProjectile = (EntityProjectile) nmsEntity;

				if (entityProjectile.getShooter() instanceof EntityPlayer) {
					return this.canSee(((EntityPlayer) entityProjectile.getShooter()).getBukkitEntity());
				}
			}

			if (nmsEntity instanceof EntityArrow) {
				EntityArrow entityProjectile = (EntityArrow) nmsEntity;

				if (entityProjectile.shooter instanceof EntityPlayer) {
					return this.canSee(((EntityPlayer) entityProjectile.shooter).getBukkitEntity());
				}
			}

		}

		return true;
	}

	@Override
	public Map<String, Object> serialize() {
		Map<String, Object> result = new LinkedHashMap<String, Object>();

		result.put("name", getName());

		return result;
	}

	@Override
	public Player getPlayer() {
		return this;
	}

	@Override
	public EntityPlayer getHandle() {
		return (EntityPlayer) entity;
	}

	public void setHandle(final EntityPlayer entity) {
		super.setHandle(entity);
	}

	@Override
	public String toString() {
		return "CraftPlayer{" + "name=" + getName() + '}';
	}

	@Override
	public int hashCode() {
		if (hash == 0 || hash == 485) {
			hash = 97 * 5 + (this.getUniqueId() != null ? this.getUniqueId().hashCode() : 0);
		}
		return hash;
	}

	@Override
	public long getFirstPlayed() {
		return firstPlayed;
	}

	@Override
	public long getLastPlayed() {
		return lastPlayed;
	}

	@Override
	public boolean hasPlayedBefore() {
		return hasPlayedBefore;
	}

	public void setFirstPlayed(long firstPlayed) {
		this.firstPlayed = firstPlayed;
	}

	public void readExtraData(NBTTagCompound nbttagcompound) {
		hasPlayedBefore = true;
		if (nbttagcompound.hasKey("bukkit")) {
			NBTTagCompound data = nbttagcompound.getCompound("bukkit");

			if (data.hasKey("firstPlayed")) {
				firstPlayed = data.getLong("firstPlayed");
				lastPlayed = data.getLong("lastPlayed");
			}

			if (data.hasKey("newExp")) {
				EntityPlayer handle = getHandle();
				handle.newExp = data.getInt("newExp");
				handle.newTotalExp = data.getInt("newTotalExp");
				handle.newLevel = data.getInt("newLevel");
				handle.expToDrop = data.getInt("expToDrop");
				handle.keepLevel = data.getBoolean("keepLevel");
			}
		}
	}

	public void setExtraData(NBTTagCompound nbttagcompound) {
		if (!nbttagcompound.hasKey("bukkit")) {
			nbttagcompound.set("bukkit", new NBTTagCompound());
		}

		NBTTagCompound data = nbttagcompound.getCompound("bukkit");
		EntityPlayer handle = getHandle();
		data.setInt("newExp", handle.newExp);
		data.setInt("newTotalExp", handle.newTotalExp);
		data.setInt("newLevel", handle.newLevel);
		data.setInt("expToDrop", handle.expToDrop);
		data.setBoolean("keepLevel", handle.keepLevel);
		data.setLong("firstPlayed", getFirstPlayed());
		data.setLong("lastPlayed", System.currentTimeMillis());
		data.setString("lastKnownName", handle.getName());
	}

	@Override
	public boolean beginConversation(Conversation conversation) {
		return conversationTracker.beginConversation(conversation);
	}

	@Override
	public void abandonConversation(Conversation conversation) {
		conversationTracker.abandonConversation(conversation,
				new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller()));
	}

	@Override
	public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) {
		conversationTracker.abandonConversation(conversation, details);
	}

	@Override
	public void acceptConversationInput(String input) {
		conversationTracker.acceptConversationInput(input);
	}

	@Override
	public boolean isConversing() {
		return conversationTracker.isConversing();
	}

	@Override
	public void sendPluginMessage(Plugin source, String channel, byte[] message) {
		StandardMessenger.validatePluginMessage(server.getMessenger(), source, channel, message);
		if (getHandle().playerConnection == null) {
			return;
		}

		if (channels.contains(channel)) {
			PacketPlayOutCustomPayload packet = new PacketPlayOutCustomPayload(channel,
					new PacketDataSerializer(Unpooled.wrappedBuffer(message)));
			getHandle().playerConnection.sendPacket(packet);
		}
	}

	@Override
	public void setTexturePack(String url) {
		setResourcePack(url);
	}

	@Override
	public void setResourcePack(String url) {
		Validate.notNull(url, "Resource pack URL cannot be null");

		getHandle().setResourcePack(url, "null");
	}

	public void addChannel(String channel) {
		com.google.common.base.Preconditions.checkState(channels.size() < 128, "Too many channels registered"); // Spigot
		if (channels.add(channel)) {
			server.getPluginManager().callEvent(new PlayerRegisterChannelEvent(this, channel));
		}
	}

	public void removeChannel(String channel) {
		if (channels.remove(channel)) {
			server.getPluginManager().callEvent(new PlayerUnregisterChannelEvent(this, channel));
		}
	}

	@Override
	public Set<String> getListeningPluginChannels() {
		return ImmutableSet.copyOf(channels);
	}

	public void sendSupportedChannels() {
		if (getHandle().playerConnection == null) {
			return;
		}
		Set<String> listening = server.getMessenger().getIncomingChannels();

		if (!listening.isEmpty()) {
			ByteArrayOutputStream stream = new ByteArrayOutputStream();

			for (String channel : listening) {
				try {
					stream.write(channel.getBytes("UTF8"));
					stream.write((byte) 0);
				} catch (IOException ex) {
					Logger.getLogger(CraftPlayer.class.getName()).log(Level.SEVERE,
							"Could not send Plugin Channel REGISTER to " + getName(), ex);
				}
			}

			getHandle().playerConnection.sendPacket(new PacketPlayOutCustomPayload("REGISTER",
					new PacketDataSerializer(Unpooled.wrappedBuffer(stream.toByteArray()))));
		}
	}

	@Override
	public EntityType getType() {
		return EntityType.PLAYER;
	}

	@Override
	public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
		server.getPlayerMetadata().setMetadata(this, metadataKey, newMetadataValue);
	}

	@Override
	public List<MetadataValue> getMetadata(String metadataKey) {
		return server.getPlayerMetadata().getMetadata(this, metadataKey);
	}

	@Override
	public boolean hasMetadata(String metadataKey) {
		return server.getPlayerMetadata().hasMetadata(this, metadataKey);
	}

	@Override
	public void removeMetadata(String metadataKey, Plugin owningPlugin) {
		server.getPlayerMetadata().removeMetadata(this, metadataKey, owningPlugin);
	}

	@Override
	public boolean setWindowProperty(Property prop, int value) {
		Container container = getHandle().activeContainer;
		if (container.getBukkitView().getType() != prop.getType()) {
			return false;
		}
		getHandle().setContainerData(container, prop.getId(), value);
		return true;
	}

	public void disconnect(String reason) {
		conversationTracker.abandonAllConversations();
		perm.clearPermissions();
	}

	@Override
	public boolean isFlying() {
		return getHandle().abilities.isFlying;
	}

	@Override
	public void setFlying(boolean value) {
		boolean needsUpdate = getHandle().abilities.canFly != value; // PaperSpigot - Only refresh abilities if needed
		if (!getAllowFlight() && value) {
			throw new IllegalArgumentException("Cannot make player fly if getAllowFlight() is false");
		}

		getHandle().abilities.isFlying = value;
		if (needsUpdate) {
			getHandle().updateAbilities(); // PaperSpigot - Only refresh abilities if needed
		}
	}

	@Override
	public boolean getAllowFlight() {
		return getHandle().abilities.canFly;
	}

	@Override
	public void setAllowFlight(boolean value) {
		if (isFlying() && !value) {
			getHandle().abilities.isFlying = false;
		}

		getHandle().abilities.canFly = value;
		getHandle().updateAbilities();
	}

	@Override
	public int getNoDamageTicks() {
		if (getHandle().invulnerableTicks > 0) {
			return Math.max(getHandle().invulnerableTicks, getHandle().noDamageTicks);
		} else {
			return getHandle().noDamageTicks;
		}
	}

	@Override
	public void setFlySpeed(float value) {
		validateSpeed(value);
		EntityPlayer player = getHandle();
		player.abilities.flySpeed = Math.max(value, 0.0001f) / 2f; // Spigot
		player.updateAbilities();

	}

	@Override
	public void setWalkSpeed(float value) {
		validateSpeed(value);
		EntityPlayer player = getHandle();
		player.abilities.walkSpeed = Math.max(value, 0.0001f) / 2f; // Spigot
		player.updateAbilities();
	}

	@Override
	public float getFlySpeed() {
		return getHandle().abilities.flySpeed * 2f;
	}

	@Override
	public float getWalkSpeed() {
		return getHandle().abilities.walkSpeed * 2f;
	}

	private void validateSpeed(float value) {
		if (value < 0) {
			if (value < -1f) {
				throw new IllegalArgumentException(value + " is too low");
			}
		} else if (value > 1f) {
			throw new IllegalArgumentException(value + " is too high");
		}
	}

	@Override
	public void setMaxHealth(double amount) {
		super.setMaxHealth(amount);
		this.health = Math.min(this.health, health);
		getHandle().triggerHealthUpdate();
	}

	@Override
	public void resetMaxHealth() {
		super.resetMaxHealth();
		getHandle().triggerHealthUpdate();
	}

	@Override
	public CraftScoreboard getScoreboard() {
		return this.server.getScoreboardManager().getPlayerBoard(this);
	}

	@Override
	public void setScoreboard(Scoreboard scoreboard) {
		Validate.notNull(scoreboard, "Scoreboard cannot be null");
		PlayerConnection playerConnection = getHandle().playerConnection;
		if (playerConnection == null) {
			throw new IllegalStateException("Cannot set scoreboard yet");
		}
		if (playerConnection.isDisconnected()) {
			// throw new IllegalStateException("Cannot set scoreboard for invalid
			// CraftPlayer"); // Spigot - remove this as Mojang's semi asynchronous Netty
			// implementation can lead to races
		}

		this.server.getScoreboardManager().setPlayerBoard(this, scoreboard);
	}

	@Override
	public void setHealthScale(double value) {
		Validate.isTrue((float) value > 0F, "Must be greater than 0");
		healthScale = value;
		scaledHealth = true;
		updateScaledHealth();
	}

	@Override
	public double getHealthScale() {
		return healthScale;
	}

	@Override
	public void setHealthScaled(boolean scale) {
		if (scaledHealth != (scaledHealth = scale)) {
			updateScaledHealth();
		}
	}

	@Override
	public boolean isHealthScaled() {
		return scaledHealth;
	}

	public float getScaledHealth() {
		return (float) (isHealthScaled() ? getHealth() * getHealthScale() / getMaxHealth() : getHealth());
	}

	@Override
	public double getHealth() {
		return health;
	}

	public void setRealHealth(double health) {
		this.health = health;
	}

	public void updateScaledHealth() {
		AttributeMapServer attributemapserver = (AttributeMapServer) getHandle().getAttributeMap();
		Set set = attributemapserver.getAttributes();

		injectScaledMaxHealth(set, true);

		getHandle().getDataWatcher().watch(6, getScaledHealth());
		getHandle().playerConnection.sendPacket(new PacketPlayOutUpdateHealth(getScaledHealth(),
				getHandle().getFoodData().getFoodLevel(), getHandle().getFoodData().getSaturationLevel()));
		getHandle().playerConnection.sendPacket(new PacketPlayOutUpdateAttributes(getHandle().getId(), set));

		set.clear();
		getHandle().maxHealthCache = getMaxHealth();
	}

	public void injectScaledMaxHealth(Collection collection, boolean force) {
		if (!scaledHealth && !force) {
			return;
		}
		for (Object genericInstance : collection) {
			IAttribute attribute = ((AttributeInstance) genericInstance).getAttribute();
			if ("generic.maxHealth".equals(attribute.getName())) {
				collection.remove(genericInstance);
				break;
			}
		}
		// Spigot start
		double healthMod = scaledHealth ? healthScale : getMaxHealth();
		if (healthMod >= Float.MAX_VALUE || healthMod <= 0) {
			healthMod = 20; // Reset health
			getServer().getLogger().warning(getName() + " tried to crash the server with a large health attribute");
		}
		collection.add(new AttributeModifiable(getHandle().getAttributeMap(),
				(new AttributeRanged(null, "generic.maxHealth", healthMod, 0.0D, Float.MAX_VALUE)).a("Max Health")
						.a(true)));
		// Spigot end
	}

	@Override
	public org.bukkit.entity.Entity getSpectatorTarget() {
		Entity followed = getHandle().C(); // PAIL
		return followed == getHandle() ? null : followed.getBukkitEntity();
	}

	@Override
	public void setSpectatorTarget(org.bukkit.entity.Entity entity) {
		Preconditions.checkArgument(getGameMode() == GameMode.SPECTATOR, "Player must be in spectator mode");
		getHandle().setSpectatorTarget((entity == null) ? null : ((CraftEntity) entity).getHandle());
	}

	@Override
	public void sendTitle(String title, String subtitle) {
		if (title != null) {
			PacketPlayOutTitle packetTitle = new PacketPlayOutTitle(EnumTitleAction.TITLE,
					CraftChatMessage.fromString(title)[0]);
			getHandle().playerConnection.sendPacket(packetTitle);
		}

		if (subtitle != null) {
			PacketPlayOutTitle packetSubtitle = new PacketPlayOutTitle(EnumTitleAction.SUBTITLE,
					CraftChatMessage.fromString(subtitle)[0]);
			getHandle().playerConnection.sendPacket(packetSubtitle);
		}
	}

	@Override
	public void resetTitle() {
		PacketPlayOutTitle packetReset = new PacketPlayOutTitle(EnumTitleAction.RESET, null);
		getHandle().playerConnection.sendPacket(packetReset);
	}

	// Spigot start
	private final Player.Spigot spigot = new Player.Spigot() {

		@Override
		public InetSocketAddress getRawAddress() {
			return (InetSocketAddress) getHandle().playerConnection.networkManager.getRawAddress();
		}

		@Override
		public boolean getCollidesWithEntities() {
			return getHandle().collidesWithEntities;
		}

		@Override
		public void setCollidesWithEntities(boolean collides) {
			getHandle().collidesWithEntities = collides;
			getHandle().k = collides; // First boolean of Entity
		}

		@Override
		public void respawn() {
			if (getHealth() <= 0 && isOnline()) {
				server.getServer().getPlayerList().moveToWorld(getHandle(), 0, false);
			}
		}

		@Override
		public void playEffect(Location location, Effect effect, int id, int data, float offsetX, float offsetY,
				float offsetZ, float speed, int particleCount, int radius) {
			Validate.notNull(location, "Location cannot be null");
			Validate.notNull(effect, "Effect cannot be null");
			Validate.notNull(location.getWorld(), "World cannot be null");
			Packet packet;
			if (effect.getType() != Effect.Type.PARTICLE) {
				int packetData = effect.getId();
				packet = new PacketPlayOutWorldEvent(packetData,
						new BlockPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ()), id, false);
			} else {
				net.minecraft.server.EnumParticle particle = null;
				int[] extra = null;
				for (net.minecraft.server.EnumParticle p : net.minecraft.server.EnumParticle.values()) {
					if (effect.getName().startsWith(p.b().replace("_", ""))) {
						particle = p;
						if (effect.getData() != null) {
							if (effect.getData().equals(org.bukkit.Material.class)) {
								extra = new int[] { id };
							} else {
								extra = new int[] { (data << 12) | (id & 0xFFF) };
							}
						}
						break;
					}
				}
				if (extra == null) {
					extra = Constants.EMPTY_ARRAY;
				}
				packet = new PacketPlayOutWorldParticles(particle, true, (float) location.getX(),
						(float) location.getY(), (float) location.getZ(), offsetX, offsetY, offsetZ, speed,
						particleCount, extra);
			}
			int distance;
			radius *= radius;
			if (getHandle().playerConnection == null) {
				return;
			}
			if (!location.getWorld().equals(getWorld())) {
				return;
			}

			distance = (int) getLocation().distanceSquared(location);
			if (distance <= radius) {
				getHandle().playerConnection.sendPacket(packet);
			}
		}

		@Override
		public String getLocale() {
			return getHandle().locale;
		}

		@Override
		public Set<Player> getHiddenPlayers() {
			Set<Player> ret = new HashSet<Player>();
			for (UUID u : hiddenPlayers) {
				ret.add(getServer().getPlayer(u));
			}

			return java.util.Collections.unmodifiableSet(ret);
		}

		@Override
		public void sendMessage(BaseComponent component) {
			sendMessage(new BaseComponent[] { component });
		}

		@Override
		public void sendMessage(BaseComponent... components) {
			if (getHandle().playerConnection == null) {
				return;
			}

			PacketPlayOutChat packet = new PacketPlayOutChat();
			packet.components = components;
			getHandle().playerConnection.sendPacket(packet);
		}

		// PaperSpigot start - Implement affects spawning API
		@Override
		public boolean getAffectsSpawning() {
			return getHandle().affectsSpawning;
		}

		@Override
		public void setAffectsSpawning(boolean affects) {
			getHandle().affectsSpawning = affects;
		}
		// PaperSpigot end

		// PaperSpigot start - Player view distance API
		@Override
		public int getViewDistance() {
			return getHandle().viewDistance;
		}

		@Override
		public void setViewDistance(int viewDistance) {
			((WorldServer) getHandle().world).getPlayerChunkMap().updateViewDistance(getHandle(), viewDistance);
		}
		// PaperSpigot end

		@Override
		public int getPing() {
			return getHandle().ping;
		}
	};

	// TacoSpigot start
	private org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus;
	private String resourcePackHash;

	@Override
	public void setResourcePack(String url, String hash) {
		Validate.notNull(url, "Resource pack URL cannot be null");
		Validate.notNull(hash, "Hash cannot be null");
		this.getHandle().setResourcePack(url, hash);
	}

	@Override
	public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status getResourcePackStatus() {
		return this.resourcePackStatus;
	}

	@Override
	public String getResourcePackHash() {
		return this.resourcePackHash;
	}

	@Override
	public boolean hasResourcePack() {
		return this.resourcePackStatus == org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.SUCCESSFULLY_LOADED;
	}

	public void setResourcePackStatus(org.bukkit.event.player.PlayerResourcePackStatusEvent.Status status,
			String hash) {
		this.resourcePackStatus = status;
		this.resourcePackHash = hash;
	}
	// TacoSpigot end

	@Override
	public Player.Spigot spigot() {
		return spigot;
	}

	private final NachoPlayer nacho = new NachoPlayer() {
		@Override
		public void sendActionBar(String message) {
			getHandle().playerConnection.sendPacket(new PacketPlayOutChat(new ChatComponentText(message), (byte) 2));
		}

		@Override
		public void jump() {
			EntityPlayer player = getHandle();
			double motX = player.motX;
			double motY = player.motY;
			double motZ = player.motZ;
			if (player.hasEffect(MobEffectList.JUMP)) {
				motY += (player.getEffect(MobEffectList.JUMP).getAmplifier() + 1) * 0.1F;
			}
			if (player.isSprinting()) {
				float f = player.yaw * 0.017453292F;
				motX -= MathHelper.sin(f) * 0.2F;
				motZ += MathHelper.cos(f) * 0.2F;
			}
			entity.motX = motX;
			entity.motY = motY;
			entity.motZ = motZ;
			entity.velocityChanged = true;
		}
	};

	@Override
	public NachoPlayer nacho() {
		return nacho;
	}

	private final Unsafe unsafe = new Unsafe() {
		@Override
		public void sendPacket(Object o) {
			try {
				if (!Packet.class.isAssignableFrom(o.getClass())) {
					throw new IllegalArgumentException("Packet sent does not implement the NMS Packet class!");
				}
				Packet packet = (Packet) o;
				getHandle().playerConnection.sendPacket(packet);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	};

	@Override
	public Unsafe unsafe() {
		return unsafe;
	}

	// Spigot end

	@Override
	public double getBlockReach() {
		return getGameMode() == GameMode.CREATIVE ? 6 : 4.5;
	}
	
	// WindSpigot start - more visible ping getting method
	public int getPing() {
		return this.spigot().getPing();
	}
	// WindSpigot end
}