diff --git a/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java b/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java index 1b318ce13..64b93a1e0 100644 --- a/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java +++ b/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java @@ -170,19 +170,19 @@ public class AkarinGlobalConfig { legacyWorldTimings = getBoolean("alternative.legacy-world-timings-required", false); } - public static int timeUpdateInterval; + public static long timeUpdateInterval; private static void timeUpdateInterval() { - timeUpdateInterval = getSeconds(getString("core.world-time-update-interval", "1s")); + timeUpdateInterval = getSeconds(getString("core.tick-rate.world-time-update-interval", "1s")) * 10; } - public static int keepAliveSendInterval; + public static long keepAliveSendInterval; private static void keepAliveSendInterval() { - keepAliveSendInterval = getSeconds(getString("core.keep-alive-packet-send-interval", "15s")); + keepAliveSendInterval = getSeconds(getString("core.tick-rate.keep-alive-packet-send-interval", "15s")) * 1000; } - public static int keepAliveTimeout; + public static long keepAliveTimeout; private static void keepAliveTimeout() { - keepAliveTimeout = getSeconds(getString("core.keep-alive-response-timeout", "30s")); + keepAliveTimeout = getSeconds(getString("core.keep-alive-response-timeout", "30s")) * 1000; } public static int asyncLightingThreads; @@ -289,4 +289,9 @@ public class AkarinGlobalConfig { private static void primaryThreadPriority() { primaryThreadPriority = getInt("core.primary-thread-priority", 7); } + + public static long playersInfoUpdateInterval; + private static void playersInfoUpdateInterval() { + playersInfoUpdateInterval = getSeconds(getString("core.tick-rate.players-info-update-interval", "30s")) * 10; + } } diff --git a/sources/src/main/java/io/akarin/server/core/AkarinSlackScheduler.java b/sources/src/main/java/io/akarin/server/core/AkarinSlackScheduler.java index ece3bfddc..5f1fecdd6 100644 --- a/sources/src/main/java/io/akarin/server/core/AkarinSlackScheduler.java +++ b/sources/src/main/java/io/akarin/server/core/AkarinSlackScheduler.java @@ -1,10 +1,14 @@ package io.akarin.server.core; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + import io.akarin.api.internal.Akari; import net.minecraft.server.EntityPlayer; import net.minecraft.server.EnumDifficulty; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PacketPlayOutKeepAlive; +import net.minecraft.server.PacketPlayOutPlayerInfo; import net.minecraft.server.PacketPlayOutUpdateTime; import net.minecraft.server.PlayerConnection; import net.minecraft.server.WorldServer; @@ -26,7 +30,11 @@ public class AkarinSlackScheduler extends Thread { private static final AkarinSlackScheduler instance = new AkarinSlackScheduler(); } + /* + * Timers + */ private long updateTime; + private long resendPlayersInfo; @Override public void run() { @@ -34,7 +42,7 @@ public class AkarinSlackScheduler extends Thread { while (server.isRunning()) { // Send time updates to everyone, it will get the right time from the world the player is in. - if (++updateTime >= AkarinGlobalConfig.timeUpdateInterval * 10) { + if (++updateTime >= AkarinGlobalConfig.timeUpdateInterval) { for (EntityPlayer player : server.getPlayerList().players) { player.playerConnection.sendPacket(new PacketPlayOutUpdateTime(player.world.getTime(), player.getPlayerTime(), player.world.getGameRules().getBoolean("doDaylightCycle"))); // Add support for per player time } @@ -49,14 +57,14 @@ public class AkarinSlackScheduler extends Thread { long elapsedTime = currentTime - conn.getLastPing(); if (conn.isPendingPing()) { // We're pending a ping from the client - if (!conn.processedDisconnect && elapsedTime >= AkarinGlobalConfig.keepAliveTimeout * 1000L) { // check keepalive limit, don't fire if already disconnected + if (!conn.processedDisconnect && elapsedTime >= AkarinGlobalConfig.keepAliveTimeout) { // check keepalive limit, don't fire if already disconnected Akari.callbackQueue.add(() -> { Akari.logger.warn("{} was kicked due to keepalive timeout!", conn.player.getName()); // more info conn.disconnect("disconnect.timeout"); }); } } else { - if (elapsedTime >= AkarinGlobalConfig.keepAliveSendInterval * 1000L) { // 15 seconds default + if (elapsedTime >= AkarinGlobalConfig.keepAliveSendInterval) { // 15 seconds default conn.setPendingPing(true); conn.setLastPing(currentTime); conn.setKeepAliveID(currentTime); @@ -71,6 +79,18 @@ public class AkarinSlackScheduler extends Thread { } } + if (++resendPlayersInfo > AkarinGlobalConfig.playersInfoUpdateInterval) { + for (EntityPlayer target : server.getPlayerList().players) { + target.playerConnection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_LATENCY, Iterables.filter(server.getPlayerList().players, new Predicate() { + @Override + public boolean apply(EntityPlayer input) { + return target.getBukkitEntity().canSee(input.getBukkitEntity()); + } + }))); + } + resendPlayersInfo = 0; + } + try { Thread.sleep(100); } catch (InterruptedException ex) { diff --git a/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java b/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java index 49eead9db..87d1ea87f 100644 --- a/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java +++ b/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java @@ -193,10 +193,6 @@ public abstract class MixinMinecraftServer { this.an().c(); MinecraftTimings.connectionTimer.stopTiming(); - MinecraftTimings.playerListTimer.startTiming(); - this.v.tick(); - MinecraftTimings.playerListTimer.stopTiming(); - MinecraftTimings.commandFunctionsTimer.startTiming(); this.aL().e(); MinecraftTimings.commandFunctionsTimer.stopTiming(); diff --git a/sources/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/sources/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java new file mode 100644 index 000000000..c93ca6c70 --- /dev/null +++ b/sources/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -0,0 +1,1940 @@ +package org.bukkit.craftbukkit.entity; + +import com.destroystokyo.paper.Title; +import com.destroystokyo.paper.profile.CraftPlayerProfile; +import com.destroystokyo.paper.profile.PlayerProfile; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.io.BaseEncoding; +import com.mojang.authlib.GameProfile; +import io.netty.buffer.Unpooled; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +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.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import net.md_5.bungee.api.chat.BaseComponent; + +import net.minecraft.server.*; +import net.minecraft.server.PacketPlayOutTitle.EnumTitleAction; + +import org.apache.commons.lang.Validate; +import org.apache.commons.lang.NotImplementedException; +import org.bukkit.*; +import org.bukkit.Achievement; +import org.bukkit.BanList; +import org.bukkit.Statistic; +import org.bukkit.Material; +import org.bukkit.Statistic.Type; +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.*; +import org.bukkit.craftbukkit.block.CraftSign; +import org.bukkit.craftbukkit.conversations.ConversationTracker; +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.advancement.CraftAdvancement; +import org.bukkit.craftbukkit.advancement.CraftAdvancementProgress; +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.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; + +import javax.annotation.Nullable; + +/** + * Akarin Changes Note
+ *
+ * 1) Make hidden-players-set concurrent
+ * @author cakoyo + */ +@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 channels = new HashSet(); + private final Map>> hiddenPlayers = Maps.newConcurrentMap(); // new HashMap<>(); // Akarin + private static final WeakHashMap> pluginWeakReferences = new WeakHashMap<>(); + private int hash = 0; + private double health = 20; + private boolean scaledHealth = false; + private double healthScale = 20; + // Paper start + private org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus; + private String resourcePackHash; + private static final boolean DISABLE_CHANNEL_LIMIT = System.getProperty("paper.disableChannelLimit") != null; // Paper - add a flag to disable the channel limit + // Paper end + + 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.getPlayer(getUniqueId()) != null; + } + + @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; + } + } + + // Paper start - Implement NetworkClient + @Override + public int getProtocolVersion() { + if (getHandle().playerConnection == null) return -1; + return getHandle().playerConnection.networkManager.protocolVersion; + } + + @Override + public InetSocketAddress getVirtualHost() { + if (getHandle().playerConnection == null) return null; + return getHandle().playerConnection.networkManager.virtualHost; + } + // Paper end + + @Override + public double getEyeHeight(boolean ignorePose) { + if (ignorePose) { + return 1.62D; + } else { + return getEyeHeight(); + } + } + + @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 sendActionBar(String message) { + if (getHandle().playerConnection == null || message == null || message.isEmpty()) return; + getHandle().playerConnection.sendPacket(new PacketPlayOutChat(new ChatComponentText(message), ChatMessageType.GAME_INFO)); + } + + @Override + public void sendActionBar(char alternateChar, String message) { + if (message == null || message.isEmpty()) return; + sendActionBar(org.bukkit.ChatColor.translateAlternateColorCodes(alternateChar, message)); + } + + @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, EnumChatFormat.WHITE); + } + + @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 = "basedrum"; + break; + case 2: + instrumentName = "snare"; + break; + case 3: + instrumentName = "hat"; + break; + case 4: + instrumentName = "bass"; + break; + case 5: + instrumentName = "flute"; + break; + case 6: + instrumentName = "bell"; + break; + case 7: + instrumentName = "guitar"; + break; + case 8: + instrumentName = "chime"; + break; + case 9: + instrumentName = "xylophone"; + break; + } + + float f = (float) Math.pow(2.0D, (note - 12.0D) / 12.0D); + getHandle().playerConnection.sendPacket(new PacketPlayOutNamedSoundEffect(CraftSound.getSoundEffect("block.note." + instrumentName), net.minecraft.server.SoundCategory.RECORDS, 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 = "basedrum"; + break; + case 2: + instrumentName = "snare"; + break; + case 3: + instrumentName = "hat"; + break; + case 4: + instrumentName = "bass"; + break; + case 5: + instrumentName = "flute"; + break; + case 6: + instrumentName = "bell"; + break; + case 7: + instrumentName = "guitar"; + break; + case 8: + instrumentName = "chime"; + break; + case 9: + instrumentName = "xylophone"; + break; + } + float f = (float) Math.pow(2.0D, (note.getId() - 12.0D) / 12.0D); + getHandle().playerConnection.sendPacket(new PacketPlayOutNamedSoundEffect(CraftSound.getSoundEffect("block.note." + instrumentName), net.minecraft.server.SoundCategory.RECORDS, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), 3.0f, f)); + } + + @Override + public void playSound(Location loc, Sound sound, float volume, float pitch) { + playSound(loc, sound, org.bukkit.SoundCategory.MASTER, volume, pitch); + } + + @Override + public void playSound(Location loc, String sound, float volume, float pitch) { + playSound(loc, sound, org.bukkit.SoundCategory.MASTER, volume, pitch); + } + + @Override + public void playSound(Location loc, Sound sound, org.bukkit.SoundCategory category, float volume, float pitch) { + if (loc == null || sound == null || category == null || getHandle().playerConnection == null) return; + + PacketPlayOutNamedSoundEffect packet = new PacketPlayOutNamedSoundEffect(CraftSound.getSoundEffect(CraftSound.getSound(sound)), net.minecraft.server.SoundCategory.valueOf(category.name()), loc.getX(), loc.getY(), loc.getZ(), volume, pitch); + getHandle().playerConnection.sendPacket(packet); + } + + @Override + public void playSound(Location loc, String sound, org.bukkit.SoundCategory category, float volume, float pitch) { + if (loc == null || sound == null || category == null || getHandle().playerConnection == null) return; + + PacketPlayOutCustomSoundEffect packet = new PacketPlayOutCustomSoundEffect(sound, net.minecraft.server.SoundCategory.valueOf(category.name()), loc.getX(), loc.getY(), loc.getZ(), volume, pitch); + getHandle().playerConnection.sendPacket(packet); + } + + @Override + public void stopSound(Sound sound) { + stopSound(sound, null); + } + + @Override + public void stopSound(String sound) { + stopSound(sound, null); + } + + @Override + public void stopSound(Sound sound, org.bukkit.SoundCategory category) { + stopSound(CraftSound.getSound(sound), category); + } + + @Override + public void stopSound(String sound, org.bukkit.SoundCategory category) { + if (getHandle().playerConnection == null) return; + PacketDataSerializer packetdataserializer = new PacketDataSerializer(Unpooled.buffer()); + + packetdataserializer.a(category == null ? "" : net.minecraft.server.SoundCategory.valueOf(category.name()).a()); + packetdataserializer.a(sound); + getHandle().playerConnection.sendPacket(new PacketPlayOutCustomPayload("MC|StopSound", packetdataserializer)); + } + + @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 void playEffect(Location loc, Effect effect, T data) { + if (data != null) { + Validate.isTrue(effect.getData() != null && effect.getData().isAssignableFrom(data.getClass()), "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); + TileEntitySign sign = new TileEntitySign(); + sign.setPosition(new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ())); + System.arraycopy(components, 0, sign.lines, 0, sign.lines.length); + + getHandle().playerConnection.sendPacket(sign.getUpdatePacket()); + } + + @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 icons = new ArrayList(); + for (MapCursor cursor : data.cursors) { + if (cursor.isVisible()) { + icons.add(new MapIcon(MapIcon.Type.a(cursor.getRawType()), cursor.getX(), cursor.getY(), cursor.getDirection())); + } + } + + PacketPlayOutMap packet = new PacketPlayOutMap(map.getId(), map.getScale().getValue(), true, icons, data.buffer, 0, 0, 128, 128); + 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) { + return false; + } + + if (entity.isVehicle()) { + 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; + } + + // If this player is riding another entity, we must dismount before teleporting. + entity.stopRiding(); + + // 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 { + // Paper - Configurable suffocation check + server.getHandle().moveToWorld(entity, toWorld.dimension, true, to, !toWorld.paperConfig.disableTeleportationSuffocationCheck); + } + return true; + } + + // Paper start - Ugly workaround for SPIGOT-1915 & GH-114 + @Override + public boolean setPassenger(org.bukkit.entity.Entity passenger) { + boolean wasSet = super.setPassenger(passenger); + if (wasSet) { + this.getHandle().playerConnection.sendPacket(new net.minecraft.server.PacketPlayOutMount(this.getHandle())); + } + return wasSet; + } + // Paper end + + @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) { + throw new UnsupportedOperationException("Not supported in this Minecraft version."); + } + + @Override + public void removeAchievement(Achievement achievement) { + throw new UnsupportedOperationException("Not supported in this Minecraft version."); + } + + @Override + public boolean hasAchievement(Achievement achievement) { + throw new UnsupportedOperationException("Not supported in this Minecraft version."); + } + + @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 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"); + } + + getHandle().a(EnumGamemode.getById(mode.getValue())); + } + + @Override + public GameMode getGameMode() { + return GameMode.getByValue(getHandle().playerInteractManager.getGameMode().getId()); + } + + // Paper start + @Override + public int applyMending(int amount) { + EntityPlayer handle = getHandle(); + // Logic copied from EntityExperienceOrb and remapped to unobfuscated methods/properties + ItemStack itemstack = EnchantmentManager.getRandomEquippedItemWithEnchant(Enchantments.MENDING, handle); + if (!itemstack.isEmpty() && itemstack.hasDamage()) { + + EntityExperienceOrb orb = new EntityExperienceOrb(handle.world); + orb.value = amount; + orb.spawnReason = org.bukkit.entity.ExperienceOrb.SpawnReason.CUSTOM; + orb.locX = handle.locX; + orb.locY = handle.locY; + orb.locZ = handle.locZ; + + int i = Math.min(orb.xpToDur(amount), itemstack.getDamage()); + org.bukkit.event.player.PlayerItemMendEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemMendEvent(handle, orb, itemstack, i); + i = event.getRepairAmount(); + orb.dead = true; + if (!event.isCancelled()) { + amount -= orb.durToXp(i); + itemstack.setDamage(itemstack.getDamage() - i); + } + } + return amount; + } + + @Override + public void giveExp(int exp, boolean applyMending) { + if (applyMending) { + exp = this.applyMending(exp); + } + // Paper end + 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) { + Preconditions.checkArgument(exp >= 0.0 && exp <= 1.0, "Experience progress must be between 0.0 and 1.0 (%s)", 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(); + } + } + + @Nullable + private static WeakReference getPluginWeakReference(@Nullable Plugin plugin) { + return (plugin == null) ? null : pluginWeakReferences.computeIfAbsent(plugin, WeakReference::new); + } + + @Override + @Deprecated + public void hidePlayer(Player player) { + hidePlayer0(null, player); + } + + @Override + public void hidePlayer(Plugin plugin, Player player) { + Validate.notNull(plugin, "Plugin cannot be null"); + Validate.isTrue(plugin.isEnabled(), "Plugin attempted to hide player while disabled"); + + hidePlayer0(plugin, player); + } + + private void hidePlayer0(@Nullable Plugin plugin, Player player) { + Validate.notNull(player, "hidden player cannot be null"); + if (getHandle().playerConnection == null) return; + if (equals(player)) return; + + Set> hidingPlugins = hiddenPlayers.get(player.getUniqueId()); + if (hidingPlugins != null) { + // Some plugins are already hiding the player. Just mark that this + // plugin wants the player hidden too and end. + hidingPlugins.add(getPluginWeakReference(plugin)); + return; + } + hidingPlugins = new HashSet<>(); + hidingPlugins.add(getPluginWeakReference(plugin)); + hiddenPlayers.put(player.getUniqueId(), hidingPlugins); + + // Remove this player from the hidden player's EntityTrackerEntry + EntityPlayer other = ((CraftPlayer) player).getHandle(); + // Paper start + unregisterPlayer(other); + } + private void unregisterPlayer(EntityPlayer other) { + EntityTracker tracker = ((WorldServer) entity.world).tracker; + // Paper end + + EntityTrackerEntry entry = tracker.trackedEntities.get(other.getId()); + if (entry != null) { + entry.clear(getHandle()); + } + + // Remove the hidden player from this player user list, if they're on it + if (other.sentListPacket) { + getHandle().playerConnection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, other)); + } + } + + @Override + @Deprecated + public void showPlayer(Player player) { + showPlayer0(null, player); + } + + @Override + public void showPlayer(Plugin plugin, Player player) { + Validate.notNull(plugin, "Plugin cannot be null"); + // Don't require that plugin be enabled. A plugin must be allowed to call + // showPlayer during its onDisable() method. + showPlayer0(plugin, player); + } + + private void showPlayer0(@Nullable Plugin plugin, Player player) { + Validate.notNull(player, "shown player cannot be null"); + if (getHandle().playerConnection == null) return; + if (equals(player)) return; + + Set> hidingPlugins = hiddenPlayers.get(player.getUniqueId()); + if (hidingPlugins == null) { + return; // Player isn't hidden + } + hidingPlugins.remove(getPluginWeakReference(plugin)); + if (!hidingPlugins.isEmpty()) { + return; // Some other plugins still want the player hidden + } + hiddenPlayers.remove(player.getUniqueId()); + + // Paper start + EntityPlayer other = ((CraftPlayer) player).getHandle(); + registerPlayer(other); + } + private void registerPlayer(EntityPlayer other) { + EntityTracker tracker = ((WorldServer) entity.world).tracker; + // Paper end + + 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()); + } + } + // Paper start + private void reregisterPlayer(EntityPlayer player) { + if (!hiddenPlayers.containsKey(player.getUniqueID())) { + unregisterPlayer(player); + registerPlayer(player); + } + } + @Override + public void setPlayerProfile(PlayerProfile profile) { + EntityPlayer self = getHandle(); + self.setProfile(CraftPlayerProfile.asAuthlibCopy(profile)); + List players = server.getServer().getPlayerList().players; + for (EntityPlayer player : players) { + player.getBukkitEntity().reregisterPlayer(self); + } + } + @Override + public PlayerProfile getPlayerProfile() { + return new CraftPlayerProfile(this).clone(); + } + // Paper end + + public void removeDisconnectingPlayer(Player player) { + hiddenPlayers.remove(player.getUniqueId()); + } + + @Override + public boolean canSee(Player player) { + return !hiddenPlayers.containsKey(player.getUniqueId()); + } + + @Override + public Map serialize() { + Map result = new LinkedHashMap(); + + 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"); + } + + @Override + public void setResourcePack(String url, byte[] hash) { + Validate.notNull(url, "Resource pack URL cannot be null"); + Validate.notNull(hash, "Resource pack hash cannot be null"); + Validate.isTrue(hash.length == 20, "Resource pack hash should be 20 bytes long but was " + hash.length); + + getHandle().setResourcePack(url, BaseEncoding.base16().lowerCase().encode(hash)); + } + + public void addChannel(String channel) { + com.google.common.base.Preconditions.checkState( DISABLE_CHANNEL_LIMIT || channels.size() < 128, "Too many channels registered" ); // Spigot // Paper - flag to disable channel limit + 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 getListeningPluginChannels() { + return ImmutableSet.copyOf(channels); + } + + public void sendSupportedChannels() { + if (getHandle().playerConnection == null) return; + Set 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 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; + } + // Paper start + if (prop == Property.REPAIR_COST && container instanceof net.minecraft.server.ContainerAnvil) { + ((ContainerAnvil) container).levelCost = value; + } + // Paper end + 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.isFlying != value; // Paper - 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(); // Paper - 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 = value / 2f; + player.updateAbilities(); + + } + + @Override + public void setWalkSpeed(float value) { + validateSpeed(value); + EntityPlayer player = getHandle(); + player.abilities.walkSpeed = value / 2f; + 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) { + if (Double.isNaN(health)) {return;} // Paper + this.health = health; + } + + public void updateScaledHealth() { + AttributeMapServer attributemapserver = (AttributeMapServer) getHandle().getAttributeMap(); + Collection set = attributemapserver.c(); // PAIL: Rename + + injectScaledMaxHealth(set, true); + + // SPIGOT-3813: Attributes before health + if (getHandle().playerConnection != null) { + getHandle().playerConnection.sendPacket(new PacketPlayOutUpdateAttributes(getHandle().getId(), set)); + sendHealthUpdate(); + } + getHandle().getDataWatcher().set(EntityLiving.HEALTH, getScaledHealth()); + + getHandle().maxHealthCache = getMaxHealth(); + } + + public void sendHealthUpdate() { + getHandle().playerConnection.sendPacket(new PacketPlayOutUpdateHealth(getScaledHealth(), getHandle().getFoodData().getFoodLevel(), getHandle().getFoodData().getSaturationLevel())); + } + + public void injectScaledMaxHealth(Collection collection, boolean force) { + if (!scaledHealth && !force) { + return; + } + for (AttributeInstance genericInstance : collection) { + if (genericInstance.getAttribute().getName().equals("generic.maxHealth")) { + 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().getSpecatorTarget(); + 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) { + sendTitle(title, subtitle, 10, 70, 20); + } + + @Override + public void sendTitle(String title, String subtitle, int fadeIn, int stay, int fadeOut) { + PacketPlayOutTitle times = new PacketPlayOutTitle(fadeIn, stay, fadeOut); + getHandle().playerConnection.sendPacket(times); + + 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); + } + + @Override + public void spawnParticle(Particle particle, Location location, int count) { + spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count); + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count) { + spawnParticle(particle, x, y, z, count, null); + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, T data) { + spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count, data); + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, T data) { + spawnParticle(particle, x, y, z, count, 0, 0, 0, data); + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ) { + spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count, offsetX, offsetY, offsetZ); + } + + @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, null); + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, T data) { + spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count, offsetX, offsetY, offsetZ, data); + } + + @Override + public 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, offsetZ, 1, data); + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, double extra) { + spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count, offsetX, offsetY, offsetZ, extra); + } + + @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 void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, double extra, T data) { + spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count, offsetX, offsetY, offsetZ, extra, data); + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, T data) { + if (data != null && !particle.getDataType().isInstance(data)) { + throw new IllegalArgumentException("data should be " + particle.getDataType() + " got " + data.getClass()); + } + PacketPlayOutWorldParticles packetplayoutworldparticles = new PacketPlayOutWorldParticles(CraftParticle.toNMS(particle), true, (float) x, (float) y, (float) z, (float) offsetX, (float) offsetY, (float) offsetZ, (float) extra, count, CraftParticle.toData(particle, data)); + getHandle().playerConnection.sendPacket(packetplayoutworldparticles); + + } + + @Override + public org.bukkit.advancement.AdvancementProgress getAdvancementProgress(org.bukkit.advancement.Advancement advancement) { + Preconditions.checkArgument(advancement != null, "advancement"); + + CraftAdvancement craft = (CraftAdvancement) advancement; + AdvancementDataPlayer data = getHandle().getAdvancementData(); + AdvancementProgress progress = data.getProgress(craft.getHandle()); + + return new CraftAdvancementProgress(craft, data, progress); + } + + @Override + public String getLocale() { + // Paper start - Locale change event + final String locale = getHandle().locale; + return locale != null ? locale : "en_us"; + // Paper end + } + + @Override + public void setAffectsSpawning(boolean affects) { + this.getHandle().affectsSpawning = affects; + } + + @Override + public boolean getAffectsSpawning() { + return this.getHandle().affectsSpawning; + } + + @Override + public int getViewDistance() { + return getHandle().getViewDistance(); + } + + @Override + public void setViewDistance(int viewDistance) { + ((WorldServer) getHandle().world).getPlayerChunkMap().updateViewDistance(getHandle(), viewDistance); + } + + @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) { + this.resourcePackStatus = status; + } + + // 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 CraftPlayer.this.isCollidable(); + } + + @Override + public void setCollidesWithEntities(boolean collides) { + CraftPlayer.this.setCollidable(collides); + } + + @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 = new int[0]; + } + 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 CraftPlayer.this.getLocale(); // Paper + } + + @Override + public Set getHiddenPlayers() + { + Set ret = new HashSet(); + for ( UUID u : hiddenPlayers.keySet() ) + { + 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(null, ChatMessageType.CHAT); + packet.components = components; + getHandle().playerConnection.sendPacket(packet); + } + + @Override + public void sendMessage(net.md_5.bungee.api.ChatMessageType position, BaseComponent component) { + sendMessage( position, new BaseComponent[] { component } ); + } + + @Override + public void sendMessage(net.md_5.bungee.api.ChatMessageType position, BaseComponent... components) { + if ( getHandle().playerConnection == null ) return; + + PacketPlayOutChat packet = new PacketPlayOutChat(null, ChatMessageType.a((byte) position.ordinal())); + // Action bar doesn't render colours, replace colours with legacy section symbols + if (position == net.md_5.bungee.api.ChatMessageType.ACTION_BAR) { + components = new BaseComponent[]{new net.md_5.bungee.api.chat.TextComponent(BaseComponent.toLegacyText(components))}; + } + packet.components = components; + getHandle().playerConnection.sendPacket(packet); + } + + @Override + public int getPing() + { + return getHandle().ping; + } + }; + + @Override + public Player.Spigot spigot() + { + return spigot; + } + // Spigot end +}