diff --git a/src/main/java/net/citizensnpcs/EventListen.java b/src/main/java/net/citizensnpcs/EventListen.java index cc4f64197..1eb609b73 100644 --- a/src/main/java/net/citizensnpcs/EventListen.java +++ b/src/main/java/net/citizensnpcs/EventListen.java @@ -2,47 +2,12 @@ package net.citizensnpcs; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.EntityCombustByBlockEvent; -import org.bukkit.event.entity.EntityCombustByEntityEvent; -import org.bukkit.event.entity.EntityCombustEvent; -import org.bukkit.event.entity.EntityDamageByBlockEvent; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityTargetEvent; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerInteractEntityEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerRespawnEvent; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.event.vehicle.VehicleDestroyEvent; -import org.bukkit.event.vehicle.VehicleEnterEvent; -import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.event.world.ChunkUnloadEvent; -import org.bukkit.event.world.WorldLoadEvent; -import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scoreboard.Team; - import com.google.common.base.Predicates; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; @@ -75,17 +40,54 @@ import net.citizensnpcs.api.trait.trait.Owner; import net.citizensnpcs.api.util.DataKey; import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.editor.Editor; +import net.citizensnpcs.npc.skin.SkinnableEntity; import net.citizensnpcs.trait.Controllable; import net.citizensnpcs.trait.CurrentLocation; import net.citizensnpcs.util.Messages; import net.citizensnpcs.util.NMS; -import net.minecraft.server.v1_8_R3.EntityPlayer; -import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityCombustByBlockEvent; +import org.bukkit.event.entity.EntityCombustByEntityEvent; +import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.entity.EntityDamageByBlockEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.vehicle.VehicleDestroyEvent; +import org.bukkit.event.vehicle.VehicleEnterEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scoreboard.Team; public class EventListen implements Listener { private final NPCRegistry npcRegistry = CitizensAPI.getNPCRegistry(); private final Map registries; private final ListMultimap toRespawn = ArrayListMultimap.create(); + private final Map skinUpdateTrackers = + new HashMap(Bukkit.getMaxPlayers() / 2); EventListen(Map registries) { this.registries = registries; @@ -339,7 +341,7 @@ public class EventListen implements Listener { @EventHandler(priority = EventPriority.MONITOR) public void onPlayerChangeWorld(PlayerChangedWorldEvent event) { - recalculatePlayer(event.getPlayer()); + recalculatePlayer(event.getPlayer(), 20, true); } @EventHandler(ignoreCancelled = true) @@ -361,7 +363,7 @@ public class EventListen implements Listener { @EventHandler(priority = EventPriority.MONITOR) public void onPlayerJoin(PlayerJoinEvent event) { - recalculatePlayer(event.getPlayer()); + recalculatePlayer(event.getPlayer(), 20, true); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -373,16 +375,17 @@ public class EventListen implements Listener { event.getPlayer().leaveVehicle(); } } + skinUpdateTrackers.remove(event.getPlayer().getUniqueId()); } @EventHandler(priority = EventPriority.MONITOR) public void onPlayerRespawn(PlayerRespawnEvent event) { - recalculatePlayer(event.getPlayer()); + recalculatePlayer(event.getPlayer(), 15, true); } @EventHandler(priority = EventPriority.MONITOR) public void onPlayerTeleport(PlayerTeleportEvent event) { - recalculatePlayer(event.getPlayer()); + recalculatePlayer(event.getPlayer(), 15, true); } @EventHandler @@ -433,34 +436,71 @@ public class EventListen implements Listener { } } - public void recalculatePlayer(final Player player) { + // recalculate player NPCs the first time a player moves and every time + // a player moves a certain distance from their last position. + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerMove(final PlayerMoveEvent event) { + + SkinUpdateTracker updateTracker = skinUpdateTrackers.get(event.getPlayer().getUniqueId()); + if (updateTracker == null) + return; + + if (!updateTracker.shouldUpdate(event.getPlayer())) + return; + + recalculatePlayer(event.getPlayer(), 10, false); + } + + public void recalculatePlayer(final Player player, long delay, final boolean isInitial) { + + if (player.hasMetadata("NPC")) + return; + + if (isInitial) { + skinUpdateTrackers.put(player.getUniqueId(), new SkinUpdateTracker(player)); + } + new BukkitRunnable() { + @Override public void run() { - final List nearbyNPCs = new ArrayList(); - for (NPC npc : getAllNPCs()) { - Entity npcEntity = npc.getEntity(); - if (npcEntity instanceof Player && player.canSee((Player) npcEntity) - && player.getWorld().equals(npcEntity.getWorld()) - && player.getLocation().distanceSquared(npcEntity.getLocation()) < 100 * 100) { - nearbyNPCs.add(((CraftPlayer) npcEntity).getHandle()); - } - } - new BukkitRunnable() { - @Override - public void run() { - sendToPlayer(player, nearbyNPCs); - } - }.runTaskLater(CitizensAPI.getPlugin(), 30); - new BukkitRunnable() { - @Override - public void run() { - sendToPlayer(player, nearbyNPCs); - } - }.runTaskLater(CitizensAPI.getPlugin(), 70); - } - }.runTaskLater(CitizensAPI.getPlugin(), 10); + List nearbyNPCs = getNearbySkinnableNPCs(player); + for (SkinnableEntity npc : nearbyNPCs) { + npc.getSkinTracker().updateViewer(player); + } + + if (!nearbyNPCs.isEmpty() && isInitial) { + // one more time to help when resource pack load times + // prevent immediate skin loading + recalculatePlayer(player, 40, false); + } + } + }.runTaskLater(CitizensAPI.getPlugin(), delay); + } + + private List getNearbySkinnableNPCs(Player player) { + + List results = new ArrayList(); + + double viewDistance = Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); + viewDistance *= viewDistance; + + for (NPC npc : getAllNPCs()) { + + Entity npcEntity = npc.getEntity(); + if (npcEntity instanceof Player + && player.canSee((Player) npcEntity) + && player.getWorld().equals(npcEntity.getWorld()) + && player.getLocation(CACHE_LOCATION) + .distanceSquared(npc.getStoredLocation()) < viewDistance) { + + SkinnableEntity skinnable = NMS.getSkinnable(npcEntity); + + results.add(skinnable); + } + } + return results; } private void respawnAllFromCoord(ChunkCoord coord) { @@ -482,30 +522,6 @@ public class EventListen implements Listener { } } - void sendToPlayer(final Player player, final List nearbyNPCs) { - if (!player.isValid()) - return; - for (EntityPlayer nearbyNPC : nearbyNPCs) { - if (nearbyNPC.isAlive()) - NMS.sendPacket(player, new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, nearbyNPC)); - } - if (Setting.DISABLE_TABLIST.asBoolean()) { - new BukkitRunnable() { - @Override - public void run() { - if (!player.isValid()) - return; - for (EntityPlayer nearbyNPC : nearbyNPCs) { - if (nearbyNPC.isAlive()) - NMS.sendPacket(player, new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, nearbyNPC)); - } - } - }.runTaskLater(CitizensAPI.getPlugin(), 2); - } - } - private boolean spawn(NPC npc) { Location spawn = npc.getTrait(CurrentLocation.class).getLocation(); if (spawn == null) { @@ -569,4 +585,65 @@ public class EventListen implements Listener { return prime * (prime * (prime + ((worldName == null) ? 0 : worldName.hashCode())) + x) + z; } } + + private class SkinUpdateTracker { + float initialYaw; + final Location location = new Location(null, 0, 0, 0); + boolean hasMoved; + int rotationCount; + + SkinUpdateTracker(Player player) { + reset(player); + } + + boolean shouldUpdate(Player player) { + + // check if this is the first time the player has moved + if (!hasMoved) { + hasMoved = true; + reset(player); + return true; + } + + Location currentLoc = player.getLocation(YAW_LOCATION); + float currentYaw = currentLoc.getYaw(); + + if (rotationCount < 2) { + + float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat(); + + boolean hasRotated = + Math.abs(NMS.clampYaw(currentYaw - this.initialYaw)) < rotationDegrees; + + // update the first 2 times the player rotates. helps load skins around player + // after the player logs/teleports. + if (hasRotated) { + rotationCount++; + reset(player); + return true; + } + } + + // update every time a player moves a certain distance + double distance = currentLoc.distanceSquared(this.location); + if (distance > MOVEMENT_SKIN_UPDATE_DISTANCE) { + reset(player); + return true; + } + else { + return false; + } + } + + // resets initial yaw and location to the players + // current location and yaw. + void reset(Player player) { + player.getLocation(location); + this.initialYaw = location.getYaw(); + } + } + + private static final Location YAW_LOCATION = new Location(null, 0, 0, 0); + private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0); + private static final int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50; } diff --git a/src/main/java/net/citizensnpcs/Settings.java b/src/main/java/net/citizensnpcs/Settings.java index 05843b0f4..6b3fc35e9 100644 --- a/src/main/java/net/citizensnpcs/Settings.java +++ b/src/main/java/net/citizensnpcs/Settings.java @@ -88,14 +88,16 @@ public class Settings { KEEP_CHUNKS_LOADED("npc.chunks.always-keep-loaded", false), LOCALE("general.translation.locale", ""), MAX_NPC_LIMIT_CHECKS("npc.limits.max-permission-checks", 100), - MAX_NPC_SKIN_RETRIES("npc.skins.max-retries", -1), + MAX_PACKET_ENTRIES("npc.limits.max-packet-entries", 15), MAX_SPEED("npc.limits.max-speed", 100), MAX_TEXT_RANGE("npc.chat.options.max-text-range", 500), MESSAGE_COLOUR("general.color-scheme.message", ""), NEW_PATHFINDER_OPENS_DOORS("npc.pathfinding.new-finder-open-doors", false), NPC_ATTACK_DISTANCE("npc.pathfinding.attack-range", 1.75 * 1.75), NPC_COST("economy.npc.cost", 100D), - NPC_SKIN_RETRY_DELAY("npc.skins.retry-delay", 120), + NPC_SKIN_UPDATE("npc.skins.update", false), + NPC_SKIN_VIEW_DISTANCE("npc.skins.view-distance", 100D), + NPC_SKIN_ROTATION_UPDATE_DEGREES("npc.skins.rotation-update-degrees", 90f), PACKET_UPDATE_DELAY("npc.packets.update-delay", 30), QUICK_SELECT("npc.selection.quick-select", false), REMOVE_PLAYERS_FROM_PLAYER_LIST("npc.player.remove-from-list", true), diff --git a/src/main/java/net/citizensnpcs/commands/NPCCommands.java b/src/main/java/net/citizensnpcs/commands/NPCCommands.java index bd75dfd73..d74e4d4a9 100644 --- a/src/main/java/net/citizensnpcs/commands/NPCCommands.java +++ b/src/main/java/net/citizensnpcs/commands/NPCCommands.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import net.citizensnpcs.npc.skin.SkinnableEntity; import org.apache.commons.lang3.StringUtils; import org.bukkit.Bukkit; import org.bukkit.DyeColor; @@ -987,11 +988,13 @@ public class NPCCommands { boolean remove = !npc.data().get("removefromplayerlist", Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean()); if (args.hasFlag('a')) { remove = false; - } else if (args.hasFlag('r')) + } else if (args.hasFlag('r')) { remove = true; + } npc.data().setPersistent("removefromplayerlist", remove); if (npc.isSpawned()) { - NMS.addOrRemoveFromPlayerList(npc.getEntity(), remove); + npc.despawn(DespawnReason.PENDING_RESPAWN); + npc.spawn(npc.getTrait(CurrentLocation.class).getLocation()); } Messaging.sendTr(sender, remove ? Messages.REMOVED_FROM_PLAYERLIST : Messages.ADDED_TO_PLAYERLIST, npc.getName()); @@ -1310,8 +1313,11 @@ public class NPCCommands { } Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName); if (npc.isSpawned()) { - npc.despawn(DespawnReason.PENDING_RESPAWN); - npc.spawn(npc.getStoredLocation()); + + SkinnableEntity skinnable = NMS.getSkinnable(npc.getEntity()); + if (skinnable != null) { + skinnable.setSkinName(skinName); + } } } diff --git a/src/main/java/net/citizensnpcs/npc/AbstractEntityController.java b/src/main/java/net/citizensnpcs/npc/AbstractEntityController.java index 62113e5aa..70294206a 100644 --- a/src/main/java/net/citizensnpcs/npc/AbstractEntityController.java +++ b/src/main/java/net/citizensnpcs/npc/AbstractEntityController.java @@ -2,7 +2,6 @@ package net.citizensnpcs.npc; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.util.NMS; - import org.bukkit.Location; import org.bukkit.entity.Entity; diff --git a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java index 5c9eaa479..186ee4afa 100644 --- a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java +++ b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java @@ -1,28 +1,13 @@ package net.citizensnpcs.npc; -import java.util.Arrays; import java.util.Collection; import java.util.UUID; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.block.Block; -import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity; -import org.bukkit.craftbukkit.v1_8_R3.entity.CraftLivingEntity; -import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; -import org.bukkit.metadata.FixedMetadataValue; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scoreboard.NameTagVisibility; - import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import net.citizensnpcs.NPCNeedsRespawnEvent; +import net.citizensnpcs.Settings; import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.ai.Navigator; @@ -41,13 +26,26 @@ import net.citizensnpcs.api.util.DataKey; import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.npc.ai.CitizensBlockBreaker; import net.citizensnpcs.npc.ai.CitizensNavigator; +import net.citizensnpcs.npc.skin.SkinnableEntity; import net.citizensnpcs.trait.CurrentLocation; import net.citizensnpcs.util.Messages; import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.Util; -import net.minecraft.server.v1_8_R3.Packet; import net.minecraft.server.v1_8_R3.PacketPlayOutEntityTeleport; -import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftLivingEntity; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.scoreboard.NameTagVisibility; public class CitizensNPC extends AbstractNPC { private EntityController entityController; @@ -190,6 +188,27 @@ public class CitizensNPC extends AbstractNPC { net.minecraft.server.v1_8_R3.Entity mcEntity = ((CraftEntity) getEntity()).getHandle(); boolean couldSpawn = !Util.isLoaded(at) ? false : mcEntity.world.addEntity(mcEntity, SpawnReason.CUSTOM); + // send skin packets, if applicable, before other NMS packets are sent + SkinnableEntity skinnable = NMS.getSkinnable(getEntity()); + if (skinnable != null) { + final double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); + skinnable.getSkinTracker().updateNearbyViewers(viewDistance); + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { + @Override + public void run() { + + if (getEntity() == null || !getEntity().isValid()) + return; + + SkinnableEntity npc = NMS.getSkinnable(getEntity()); + if (npc == null) + return; + + npc.getSkinTracker().updateNearbyViewers(viewDistance); + } + }, 20); + } + mcEntity.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch()); if (!couldSpawn) { @@ -244,29 +263,9 @@ public class CitizensNPC extends AbstractNPC { if (getEntity() instanceof Player) { final CraftPlayer player = (CraftPlayer) getEntity(); NMS.replaceTrackerEntry(player); - new BukkitRunnable() { - @Override - public void run() { - NMS.sendPacketsNearby(player, player.getLocation(), - Arrays.asList((Packet) new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, player.getHandle())), - 200.0); - if (Setting.DISABLE_TABLIST.asBoolean()) { - new BukkitRunnable() { - @Override - public void run() { - NMS.sendPacketsNearby(player, player.getLocation(), - Arrays.asList((Packet) new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, - player.getHandle())), - 200.0); - } - }.runTaskLater(CitizensAPI.getPlugin(), 2); - } - } - }.runTaskLater(CitizensAPI.getPlugin(), 2); } } + return true; } diff --git a/src/main/java/net/citizensnpcs/npc/entity/EntityHumanNPC.java b/src/main/java/net/citizensnpcs/npc/entity/EntityHumanNPC.java index 501315cbe..120a1d048 100644 --- a/src/main/java/net/citizensnpcs/npc/entity/EntityHumanNPC.java +++ b/src/main/java/net/citizensnpcs/npc/entity/EntityHumanNPC.java @@ -4,9 +4,13 @@ import java.io.IOException; import java.net.Socket; import java.util.List; +import com.google.common.base.Preconditions; +import com.mojang.authlib.GameProfile; + import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.event.NPCPushEvent; +import net.citizensnpcs.api.npc.MetadataStore; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.trait.trait.Inventory; import net.citizensnpcs.npc.CitizensNPC; @@ -14,6 +18,8 @@ import net.citizensnpcs.npc.ai.NPCHolder; import net.citizensnpcs.npc.network.EmptyNetHandler; import net.citizensnpcs.npc.network.EmptyNetworkManager; import net.citizensnpcs.npc.network.EmptySocket; +import net.citizensnpcs.npc.skin.SkinPacketTracker; +import net.citizensnpcs.npc.skin.SkinnableEntity; import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.Util; import net.citizensnpcs.util.nms.PlayerControllerJump; @@ -40,16 +46,16 @@ import net.minecraft.server.v1_8_R3.WorldServer; import net.minecraft.server.v1_8_R3.WorldSettings.EnumGamemode; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.craftbukkit.v1_8_R3.CraftServer; import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; import org.bukkit.metadata.MetadataValue; import org.bukkit.plugin.Plugin; import org.bukkit.util.Vector; -import com.mojang.authlib.GameProfile; - -public class EntityHumanNPC extends EntityPlayer implements NPCHolder { +public class EntityHumanNPC extends EntityPlayer implements NPCHolder, SkinnableEntity { private PlayerControllerJump controllerJump; private PlayerControllerLook controllerLook; private PlayerControllerMove controllerMove; @@ -58,16 +64,21 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder { private PlayerNavigation navigation; private final CitizensNPC npc; private final Location packetLocationCache = new Location(null, 0, 0, 0); + private final SkinPacketTracker skinTracker; public EntityHumanNPC(MinecraftServer minecraftServer, WorldServer world, GameProfile gameProfile, - PlayerInteractManager playerInteractManager, NPC npc) { + PlayerInteractManager playerInteractManager, NPC npc) { super(minecraftServer, world, gameProfile, playerInteractManager); this.npc = (CitizensNPC) npc; if (npc != null) { + skinTracker = new SkinPacketTracker(this); playerInteractManager.setGameMode(EnumGamemode.SURVIVAL); initialise(minecraftServer); } + else { + skinTracker = null; + } } @Override @@ -185,6 +196,31 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder { return npc; } + @Override + public SkinPacketTracker getSkinTracker() { + return skinTracker; + } + + @Override + public String getSkinName() { + + MetadataStore meta = npc.data(); + + String skinName = meta.get(NPC.PLAYER_SKIN_UUID_METADATA); + if (skinName == null) { + skinName = ChatColor.stripColor(getName()); + } + return skinName.toLowerCase(); + } + + @Override + public void setSkinName(String name) { + Preconditions.checkNotNull(name); + + npc.data().setPersistent(NPC.PLAYER_SKIN_UUID_METADATA, name.toLowerCase()); + skinTracker.notifySkinChange(); + } + private void initialise(MinecraftServer minecraftServer) { Socket socket = new EmptySocket(); NetworkManager conn = null; @@ -296,6 +332,7 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder { } private void updatePackets(boolean navigating) { + if (world.getWorld().getFullTime() % Setting.PACKET_UPDATE_DELAY.asInt() == 0) { // set skin flag byte to all visible (DataWatcher API is lacking so // catch the NPE as a sign that this is a MC 1.7 server without the @@ -316,10 +353,6 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder { packets[i] = new PacketPlayOutEntityEquipment(getId(), i, getEquipment(i)); } - boolean removeFromPlayerList = npc.data().get("removefromplayerlist", - Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean()); - NMS.addOrRemoveFromPlayerList(getBukkitEntity(), removeFromPlayerList); - NMS.sendPlayerlistPacket(false, getBukkitEntity()); NMS.sendPacketsNearby(getBukkitEntity(), current, packets); } } @@ -328,7 +361,7 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder { this.navigation.setRange(pathfindingRange); } - public static class PlayerNPC extends CraftPlayer implements NPCHolder { + public static class PlayerNPC extends CraftPlayer implements NPCHolder, SkinnableEntity { private final CraftServer cserver; private final CitizensNPC npc; @@ -372,6 +405,26 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder { public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { cserver.getEntityMetadata().setMetadata(this, metadataKey, newMetadataValue); } + + @Override + public SkinPacketTracker getSkinTracker() { + return ((SkinnableEntity)this.entity).getSkinTracker(); + } + + @Override + public Player getBukkitEntity() { + return this; + } + + @Override + public String getSkinName() { + return ((SkinnableEntity)this.entity).getSkinName(); + } + + @Override + public void setSkinName(String name) { + ((SkinnableEntity)this.entity).setSkinName(name); + } } private static final float EPSILON = 0.005F; diff --git a/src/main/java/net/citizensnpcs/npc/entity/HumanController.java b/src/main/java/net/citizensnpcs/npc/entity/HumanController.java index f977bdcdc..c0ad8602c 100644 --- a/src/main/java/net/citizensnpcs/npc/entity/HumanController.java +++ b/src/main/java/net/citizensnpcs/npc/entity/HumanController.java @@ -1,57 +1,32 @@ package net.citizensnpcs.npc.entity; -import java.lang.reflect.Method; -import java.net.URL; -import java.util.Iterator; -import java.util.Map; import java.util.UUID; -import java.util.concurrent.BlockingDeque; -import java.util.concurrent.Callable; -import java.util.concurrent.LinkedBlockingDeque; import java.util.regex.Pattern; +import com.mojang.authlib.GameProfile; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Colorizer; +import net.citizensnpcs.npc.AbstractEntityController; +import net.citizensnpcs.npc.skin.Skin; +import net.citizensnpcs.npc.skin.SkinnableEntity; +import net.citizensnpcs.util.NMS; +import net.minecraft.server.v1_8_R3.PlayerInteractManager; +import net.minecraft.server.v1_8_R3.WorldServer; + import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; -import org.bukkit.craftbukkit.v1_8_R3.CraftServer; import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.scoreboard.Scoreboard; import org.bukkit.scoreboard.Team; -import com.google.common.collect.Iterables; -import com.google.common.collect.Maps; -import com.mojang.authlib.Agent; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.GameProfileRepository; -import com.mojang.authlib.HttpAuthenticationService; -import com.mojang.authlib.ProfileLookupCallback; -import com.mojang.authlib.minecraft.MinecraftSessionService; -import com.mojang.authlib.properties.Property; -import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; -import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; -import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse; -import com.mojang.util.UUIDTypeAdapter; - -import net.citizensnpcs.Settings.Setting; -import net.citizensnpcs.api.CitizensAPI; -import net.citizensnpcs.api.event.DespawnReason; -import net.citizensnpcs.api.npc.NPC; -import net.citizensnpcs.api.util.Colorizer; -import net.citizensnpcs.api.util.Messaging; -import net.citizensnpcs.npc.AbstractEntityController; -import net.citizensnpcs.util.NMS; -import net.minecraft.server.v1_8_R3.PlayerInteractManager; -import net.minecraft.server.v1_8_R3.WorldServer; - public class HumanController extends AbstractEntityController { public HumanController() { super(); - if (SKIN_THREAD == null) { - Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), SKIN_THREAD = new SkinThread(), - 10, 10); - } } @Override @@ -100,20 +75,26 @@ public class HumanController extends AbstractEntityController { msb |= 0x0000000000002000L; uuid = new UUID(msb, uuid.getLeastSignificantBits()); } - GameProfile profile = new GameProfile(uuid, coloredName); - updateSkin(npc, nmsWorld, profile); + GameProfile profile = new GameProfile(uuid, coloredName); + final EntityHumanNPC handle = new EntityHumanNPC(nmsWorld.getServer().getServer(), nmsWorld, profile, new PlayerInteractManager(nmsWorld), npc); + + Skin skin = handle.getSkinTracker().getSkin(); + if (skin != null) { + skin.apply(handle); + } + handle.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch()); + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { + @Override public void run() { + if (getBukkitEntity() == null || !getBukkitEntity().isValid()) return; - boolean removeFromPlayerList = Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean(); - NMS.addOrRemoveFromPlayerList(getBukkitEntity(), - npc.data().get("removefromplayerlist", removeFromPlayerList)); if (prefixCapture != null) { Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard(); @@ -133,6 +114,7 @@ public class HumanController extends AbstractEntityController { } } }, 1); + handle.getBukkitEntity().setSleepingIgnored(true); return handle.getBukkitEntity(); @@ -145,235 +127,14 @@ public class HumanController extends AbstractEntityController { @Override public void remove() { - NMS.sendPlayerlistPacket(false, getBukkitEntity()); + + NMS.removeFromWorld(getBukkitEntity()); + + SkinnableEntity npc = NMS.getSkinnable(getBukkitEntity()); + npc.getSkinTracker().onRemoveNPC(); + super.remove(); } - private void updateSkin(final NPC npc, final WorldServer nmsWorld, GameProfile profile) { - - String skinUUID = npc.data().get(NPC.PLAYER_SKIN_UUID_METADATA); - if (skinUUID == null) { - skinUUID = npc.getName(); - } - if (npc.data().has(CACHED_SKIN_UUID_METADATA) && npc.data().has(CACHED_SKIN_UUID_NAME_METADATA) - && ChatColor.stripColor(skinUUID).equalsIgnoreCase( - ChatColor.stripColor(npc.data(). get(CACHED_SKIN_UUID_NAME_METADATA)))) { - skinUUID = npc.data().get(CACHED_SKIN_UUID_METADATA); - } - if (npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES) - && npc.data(). get(PLAYER_SKIN_TEXTURE_PROPERTIES).equals("cache")) { - SKIN_THREAD.addRunnable( - new SkinFetcher(new UUIDFetcher(skinUUID, npc), nmsWorld.getMinecraftServer().aD(), npc)); - return; - } - Property cached = TEXTURE_CACHE.get(skinUUID); - if (npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES) && npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN)) { - cached = new Property("textures", npc.data(). get(PLAYER_SKIN_TEXTURE_PROPERTIES), - npc.data(). get(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN)); - } - if (cached != null) { - profile.getProperties().put("textures", cached); - } else { - SKIN_THREAD.addRunnable( - new SkinFetcher(new UUIDFetcher(skinUUID, npc), nmsWorld.getMinecraftServer().aD(), npc)); - } - } - - private static class SkinFetcher implements Runnable { - private final NPC npc; - private final MinecraftSessionService repo; - private final Callable uuid; - - public SkinFetcher(Callable uuid, MinecraftSessionService repo, NPC npc) { - this.uuid = uuid; - this.repo = repo; - this.npc = npc; - } - - /* - * Yggdrasil's default implementation of this method silently fails instead of throwing an Exception like it should. - */ - private GameProfile fillProfileProperties(YggdrasilAuthenticationService auth, GameProfile profile, - boolean requireSecure) throws Exception { - URL url = HttpAuthenticationService.constantURL( - new StringBuilder().append("https://sessionserver.mojang.com/session/minecraft/profile/") - .append(UUIDTypeAdapter.fromUUID(profile.getId())).toString()); - url = HttpAuthenticationService.concatenateURL(url, - new StringBuilder().append("unsigned=").append(!requireSecure).toString()); - MinecraftProfilePropertiesResponse response = (MinecraftProfilePropertiesResponse) MAKE_REQUEST.invoke(auth, - url, null, MinecraftProfilePropertiesResponse.class); - if (response == null) { - return profile; - } - GameProfile result = new GameProfile(response.getId(), response.getName()); - result.getProperties().putAll(response.getProperties()); - profile.getProperties().putAll(response.getProperties()); - return result; - } - - @Override - public void run() { - String realUUID; - try { - realUUID = uuid.call(); - } catch (Exception e) { - return; - } - GameProfile skinProfile = null; - Property cached = TEXTURE_CACHE.get(realUUID); - if (cached != null && !(npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES) - && npc.data(). get(PLAYER_SKIN_TEXTURE_PROPERTIES).equals("cache"))) { - if (Messaging.isDebugging()) { - Messaging - .debug("Using cached skin texture for NPC " + npc.getName() + " UUID " + npc.getUniqueId()); - } - skinProfile = new GameProfile(UUID.fromString(realUUID), ""); - skinProfile.getProperties().put("textures", cached); - } else { - try { - skinProfile = fillProfileProperties( - ((YggdrasilMinecraftSessionService) repo).getAuthenticationService(), - new GameProfile(UUID.fromString(realUUID), ""), true); - } catch (Exception e) { - if ((e.getMessage() != null && e.getMessage().contains("too many requests")) - || (e.getCause() != null && e.getCause().getMessage() != null - && e.getCause().getMessage().contains("too many requests"))) { - SKIN_THREAD.delay(); - SKIN_THREAD.addRunnable(this); - } - return; - } - - if (skinProfile == null || !skinProfile.getProperties().containsKey("textures")) - return; - Property textures = Iterables.getFirst(skinProfile.getProperties().get("textures"), null); - if (textures.getValue() == null || textures.getSignature() == null) - return; - if (npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES) - && npc.data(). get(PLAYER_SKIN_TEXTURE_PROPERTIES).equals("cache")) { - npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES, textures.getValue()); - npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN, textures.getSignature()); - } - if (Messaging.isDebugging()) { - Messaging.debug("Fetched skin texture for UUID " + realUUID + " for NPC " + npc.getName() + " UUID " - + npc.getUniqueId()); - } - TEXTURE_CACHE.put(realUUID, new Property("textures", textures.getValue(), textures.getSignature())); - } - if (CitizensAPI.getPlugin().isEnabled()) { - Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { - @Override - public void run() { - if (npc.isSpawned()) { - npc.despawn(DespawnReason.PENDING_RESPAWN); - npc.spawn(npc.getStoredLocation()); - } - } - }); - } - } - } - - public static class SkinThread implements Runnable { - private volatile int delay = 0; - private volatile int retryTimes = 0; - private final BlockingDeque tasks = new LinkedBlockingDeque(); - - public void addRunnable(Runnable r) { - Iterator itr = tasks.iterator(); - while (itr.hasNext()) { - if (((SkinFetcher) itr.next()).npc.getUniqueId().equals(((SkinFetcher) r).npc.getUniqueId())) { - itr.remove(); - } - } - tasks.offer(r); - } - - public void delay() { - delay = Setting.NPC_SKIN_RETRY_DELAY.asInt(); - // need to wait before Mojang accepts API calls again - retryTimes++; - if (Setting.MAX_NPC_SKIN_RETRIES.asInt() >= 0 && retryTimes > Setting.MAX_NPC_SKIN_RETRIES.asInt()) { - tasks.clear(); - retryTimes = 0; - } - } - - @Override - public void run() { - if (delay > 0) { - delay--; - return; - } - Runnable r = tasks.pollFirst(); - if (r == null) { - return; - } - r.run(); - } - - } - - public static class UUIDFetcher implements Callable { - private final NPC npc; - private String reportedUUID; - - public UUIDFetcher(String reportedUUID, NPC npc) { - this.reportedUUID = reportedUUID; - this.npc = npc; - } - - @Override - public String call() throws Exception { - String skinUUID = UUID_CACHE.get(reportedUUID); - if (skinUUID != null) { - npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinUUID); - npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, reportedUUID); - reportedUUID = skinUUID; - } - if (reportedUUID.contains("-")) { - return reportedUUID; - } - final GameProfileRepository repo = ((CraftServer) Bukkit.getServer()).getServer() - .getGameProfileRepository(); - repo.findProfilesByNames(new String[] { ChatColor.stripColor(reportedUUID) }, Agent.MINECRAFT, - new ProfileLookupCallback() { - @Override - public void onProfileLookupFailed(GameProfile arg0, Exception arg1) { - } - - @Override - public void onProfileLookupSucceeded(final GameProfile profile) { - UUID_CACHE.put(reportedUUID, profile.getId().toString()); - if (Messaging.isDebugging()) { - Messaging.debug("Fetched UUID " + profile.getId() + " for NPC " + npc.getName() - + " UUID " + npc.getUniqueId()); - } - npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, profile.getId().toString()); - npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, profile.getName()); - } - }); - return npc.data().get(CACHED_SKIN_UUID_METADATA, reportedUUID); - } - } - - private static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid"; - private static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name"; - private static Method MAKE_REQUEST; private static Pattern NON_ALPHABET_MATCHER = Pattern.compile(".*[^A-Za-z0-9_].*"); - private static final String PLAYER_SKIN_TEXTURE_PROPERTIES = "player-skin-textures"; - private static final String PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN = "player-skin-signature"; - private static SkinThread SKIN_THREAD; - private static final Map TEXTURE_CACHE = Maps.newConcurrentMap(); - private static final Map UUID_CACHE = Maps.newConcurrentMap(); - - static { - try { - MAKE_REQUEST = YggdrasilAuthenticationService.class.getDeclaredMethod("makeRequest", URL.class, - Object.class, Class.class); - MAKE_REQUEST.setAccessible(true); - } catch (Exception ex) { - ex.printStackTrace(); - } - } } diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java new file mode 100644 index 000000000..8f7e1f7e8 --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java @@ -0,0 +1,14 @@ +package net.citizensnpcs.npc.profile; + +/** + * Interface for a subscriber of the results of a profile fetch. + */ +public interface ProfileFetchHandler { + + /** + * Invoked when a result for a profile is ready. + * + * @param request The profile request that was handled. + */ + void onResult(ProfileRequest request); +} diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchResult.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchResult.java new file mode 100644 index 000000000..73c8188d3 --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchResult.java @@ -0,0 +1,29 @@ +package net.citizensnpcs.npc.profile; + +/** + * The result status of a profile fetch. + */ +public enum ProfileFetchResult { + /** + * The profile has not been fetched yet. + */ + PENDING, + /** + * The profile was successfully fetched. + */ + SUCCESS, + /** + * The profile request failed for unknown reasons. + */ + FAILED, + /** + * The profile request failed because the profile + * was not found. + */ + NOT_FOUND, + /** + * The profile request failed because too many requests + * were sent. + */ + TOO_MANY_REQUESTS +} diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java new file mode 100644 index 000000000..39e9bb4f0 --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java @@ -0,0 +1,115 @@ +package net.citizensnpcs.npc.profile; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; + +import net.citizensnpcs.api.CitizensAPI; + +import org.bukkit.Bukkit; + +/** + * Thread used to fetch profiles from the Mojang servers. + * + *

Maintains a cache of profiles so that no profile is ever requested more than once + * during a single server session.

+ * + * @see ProfileFetcher + */ +class ProfileFetchThread implements Runnable { + + private final ProfileFetcher profileFetcher = new ProfileFetcher(); + private final Deque queue = new ArrayDeque(); + private final Map requested = new HashMap(35); + private final Object sync = new Object(); // sync for queue & requested fields + + ProfileFetchThread() {} + + /** + * Fetch a profile. + * + * @param name The name of the player the profile belongs to. + * @param handler Optional handler to handle result fetch result. + * Handler always invoked from the main thread. + * + * @see ProfileFetcher#fetch + */ + void fetch(String name, @Nullable ProfileFetchHandler handler) { + Preconditions.checkNotNull(name); + + name = name.toLowerCase(); + ProfileRequest request; + + synchronized (sync) { + request = requested.get(name); + if (request == null) { + request = new ProfileRequest(name, handler); + queue.add(request); + requested.put(name, request); + return; + } + } + + if (handler != null) { + + if (request.getResult() == ProfileFetchResult.PENDING) { + addHandler(request, handler); + } + else { + sendResult(handler, request); + } + } + } + + @Override + public void run() { + + List requests; + + synchronized (sync) { + + if (queue.isEmpty()) + return; + + requests = new ArrayList(queue); + + queue.clear(); + } + + profileFetcher.fetchRequests(requests); + } + + private static void sendResult(final ProfileFetchHandler handler, + final ProfileRequest request) { + + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), + new Runnable() { + + @Override + public void run() { + + handler.onResult(request); + } + }, 1); + } + + private static void addHandler(final ProfileRequest request, + final ProfileFetchHandler handler) { + + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), + new Runnable() { + + @Override + public void run() { + + request.addHandler(handler); + } + }, 1); + } +} diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java new file mode 100644 index 000000000..af9dcfd7d --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java @@ -0,0 +1,154 @@ +package net.citizensnpcs.npc.profile; + +import java.util.Collection; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; +import com.mojang.authlib.Agent; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.GameProfileRepository; +import com.mojang.authlib.ProfileLookupCallback; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.NMS; + +import org.bukkit.Bukkit; + +/** + * Fetches game profiles that include skin data from Mojang servers. + * + * @see ProfileFetchThread + */ +public class ProfileFetcher { + + /** + * Fetch a profile. + * + * @param name The name of the player the profile belongs to. + * @param handler Optional handler to handle the result. + * Handler always invoked from the main thread. + */ + public static void fetch(String name, @Nullable ProfileFetchHandler handler) { + Preconditions.checkNotNull(name); + + if (PROFILE_THREAD == null) { + PROFILE_THREAD = new ProfileFetchThread(); + Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD, + 11, 20); + } + PROFILE_THREAD.fetch(name, handler); + } + + ProfileFetcher() {} + + /** + * Fetch one or more profiles. + * + * @param requests The profile requests. + */ + void fetchRequests(final Collection requests) { + Preconditions.checkNotNull(requests); + + final GameProfileRepository repo = NMS.getGameProfileRepository(); + + String[] playerNames = new String[requests.size()]; + + int i=0; + for (ProfileRequest request : requests) { + playerNames[i] = request.getPlayerName(); + i++; + } + + repo.findProfilesByNames(playerNames, Agent.MINECRAFT, + new ProfileLookupCallback() { + + @Override + public void onProfileLookupFailed(GameProfile profile, Exception e) { + + if (Messaging.isDebugging()) { + Messaging.debug("Profile lookup for player '" + + profile.getName() + "' failed: " + getExceptionMsg(e)); + } + + ProfileRequest request = findRequest(profile.getName(), requests); + if (request == null) + return; + + if (isProfileNotFound(e)) { + request.setResult(null, ProfileFetchResult.NOT_FOUND); + } else if (isTooManyRequests(e)) { + request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS); + } else { + request.setResult(null, ProfileFetchResult.FAILED); + } + } + + @Override + public void onProfileLookupSucceeded(final GameProfile profile) { + + if (Messaging.isDebugging()) { + Messaging.debug("Fetched profile " + profile.getId() + + " for player " + profile.getName()); + } + + ProfileRequest request = findRequest(profile.getName(), requests); + if (request == null) + return; + + try { + request.setResult(NMS.fillProfileProperties(profile, true), ProfileFetchResult.SUCCESS); + } catch (Exception e) { + + if (Messaging.isDebugging()) { + Messaging.debug("Profile lookup for player '" + + profile.getName() + "' failed: " + getExceptionMsg(e)); + } + + if (isTooManyRequests(e)) { + request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS); + } else { + request.setResult(null, ProfileFetchResult.FAILED); + } + } + } + }); + } + + @Nullable + private static ProfileRequest findRequest(String name, Collection requests) { + + name = name.toLowerCase(); + + for (ProfileRequest request : requests) { + if (request.getPlayerName().equals(name)) + return request; + } + return null; + } + + private static boolean isProfileNotFound(Exception e) { + String message = e.getMessage(); + String cause = e.getCause() != null ? e.getCause().getMessage() : null; + + return (message != null && message.contains("did not find")) + || (cause != null && cause.contains("did not find")); + } + + private static String getExceptionMsg(Exception e) { + String message = e.getMessage(); + String cause = e.getCause() != null ? e.getCause().getMessage() : null; + return cause != null ? cause : message; + } + + private static boolean isTooManyRequests(Exception e) { + + String message = e.getMessage(); + String cause = e.getCause() != null ? e.getCause().getMessage() : null; + + return (message != null && message.contains("too many requests")) + || (cause != null && cause.contains("too many requests")); + } + + private static ProfileFetchThread PROFILE_THREAD; +} diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java new file mode 100644 index 000000000..8688a3ae7 --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java @@ -0,0 +1,117 @@ +package net.citizensnpcs.npc.profile; + +import java.util.ArrayDeque; +import java.util.Deque; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; +import com.mojang.authlib.GameProfile; + +import net.citizensnpcs.api.CitizensAPI; + +import org.bukkit.Bukkit; + +/** + * Stores basic information about a single profile used to request + * profiles from the Mojang servers. + * + *

Also stores the result of the request.

+ */ +public class ProfileRequest { + + private final String playerName; + private Deque handlers; + private GameProfile profile; + private volatile ProfileFetchResult result = ProfileFetchResult.PENDING; + + /** + * Constructor. + * + * @param playerName The name of the player whose profile is being requested. + * @param handler Optional handler to handle the result for the profile. + * Handler always invoked from the main thread. + */ + ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) { + Preconditions.checkNotNull(playerName); + + this.playerName = playerName; + + if (handler != null) + addHandler(handler); + } + + /** + * Get the name of the player the requested profile belongs to. + */ + public String getPlayerName() { + return playerName; + } + + /** + * Get the game profile that was requested. + * + * @return The game profile or null if the profile has not been retrieved + * yet or there was an error while retrieving the profile. + */ + @Nullable + public GameProfile getProfile() { + return profile; + } + + /** + * Get the result of the profile fetch. + */ + public ProfileFetchResult getResult() { + return result; + } + + /** + * Add one time result handler. + * + *

Handler is always invoked from the main thread.

+ * + * @param handler The result handler. + */ + public void addHandler(ProfileFetchHandler handler) { + Preconditions.checkNotNull(handler); + + if (result != ProfileFetchResult.PENDING) { + handler.onResult(this); + return; + } + + if (handlers == null) + handlers = new ArrayDeque(); + + handlers.addLast(handler); + } + + /** + * Invoked to set the profile result. + * + *

Can be invoked from any thread, always executes on the main thread.

+ * + * @param profile The profile. Null if there was an error. + * @param result The result of the request. + */ + void setResult(final @Nullable GameProfile profile, final ProfileFetchResult result) { + + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { + @Override + public void run() { + + ProfileRequest.this.profile = profile; + ProfileRequest.this.result = result; + + if (handlers == null) + return; + + while (!handlers.isEmpty()) { + handlers.removeFirst().onResult(ProfileRequest.this); + } + + handlers = null; + } + }); + } +} diff --git a/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java b/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java new file mode 100644 index 000000000..d6682550c --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java @@ -0,0 +1,157 @@ +package net.citizensnpcs.npc.skin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.base.Preconditions; + +import net.citizensnpcs.Settings; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.util.NMS; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +/** + * Sends remove packets in batch per player. + * + *

Collects entities to remove and sends them all to the + * player in a single packet.

+ */ +public class PlayerListRemover { + + private final Map pending = + new HashMap(Bukkit.getMaxPlayers() / 2); + + PlayerListRemover() { + Bukkit.getScheduler().runTaskTimer(CitizensAPI.getPlugin(), new Sender(), 2, 2); + } + + /** + * Send a remove packet to the specified player for the specified + * skinnable entity. + * + * @param player The player to send the packet to. + * @param entity The entity to remove. + */ + public void sendPacket(Player player, SkinnableEntity entity) { + Preconditions.checkNotNull(player); + Preconditions.checkNotNull(entity); + + PlayerEntry entry = getEntry(player); + + entry.toRemove.add(entity); + } + + /** + * Cancel packets pending to be sent to the specified player. + * + * @param player The player. + */ + public void cancelPackets(Player player) { + Preconditions.checkNotNull(player); + + PlayerEntry entry = pending.remove(player.getUniqueId()); + if (entry == null) + return; + + for (SkinnableEntity entity : entry.toRemove) { + entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId()); + } + } + + /** + * Cancel packets pending to be sent to the specified player + * for the specified skinnable entity. + * + * @param player The player. + * @param skinnable The skinnable entity. + */ + public void cancelPackets(Player player, SkinnableEntity skinnable) { + Preconditions.checkNotNull(player); + Preconditions.checkNotNull(skinnable); + + PlayerEntry entry = pending.get(player.getUniqueId()); + if (entry == null) + return; + + if (entry.toRemove.remove(skinnable)) { + skinnable.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId()); + } + + if (entry.toRemove.isEmpty()) + pending.remove(player.getUniqueId()); + } + + private PlayerEntry getEntry(Player player) { + + PlayerEntry entry = pending.get(player.getUniqueId()); + if (entry == null) { + entry = new PlayerEntry(player); + pending.put(player.getUniqueId(), entry); + } + + return entry; + } + + private class PlayerEntry { + Player player; + Set toRemove = new HashSet(25); + + PlayerEntry(Player player) { + this.player = player; + } + } + + private class Sender implements Runnable { + + @Override + public void run() { + + int maxPacketEntries = Settings.Setting.MAX_PACKET_ENTRIES.asInt(); + + Iterator> entryIterator = pending.entrySet().iterator(); + while (entryIterator.hasNext()) { + + Map.Entry mapEntry = entryIterator.next(); + PlayerEntry entry = mapEntry.getValue(); + + int listSize = Math.min(maxPacketEntries, entry.toRemove.size()); + boolean sendAll = listSize == entry.toRemove.size(); + + List skinnableList = new ArrayList(listSize); + + int i =0; + Iterator skinIterator = entry.toRemove.iterator(); + while (skinIterator.hasNext()) { + + if (i >= maxPacketEntries) + break; + + SkinnableEntity skinnable = skinIterator.next(); + skinnableList.add(skinnable); + + skinIterator.remove(); + i++; + } + + if (entry.player.isOnline()) + NMS.sendPlayerListRemove(entry.player, skinnableList); + + // notify skin trackers that a remove packet has been sent to a player + for (SkinnableEntity entity : skinnableList) { + entity.getSkinTracker().notifyRemovePacketSent(entry.player.getUniqueId()); + } + + if (sendAll) + entryIterator.remove(); + } + } + } +} diff --git a/src/main/java/net/citizensnpcs/npc/skin/Skin.java b/src/main/java/net/citizensnpcs/npc/skin/Skin.java new file mode 100644 index 000000000..3435ef3eb --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/skin/Skin.java @@ -0,0 +1,241 @@ +package net.citizensnpcs.npc.skin; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; +import javax.annotation.Nullable; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; + +import net.citizensnpcs.Settings; +import net.citizensnpcs.api.event.DespawnReason; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.npc.profile.ProfileFetchResult; +import net.citizensnpcs.npc.profile.ProfileFetchHandler; +import net.citizensnpcs.npc.profile.ProfileFetcher; +import net.citizensnpcs.npc.profile.ProfileRequest; + +/** + * Stores data for a single skin. + */ +public class Skin { + + private final String skinName; + private volatile Property skinData; + private volatile UUID skinId; + private volatile boolean isValid = true; + private final Map pending = new WeakHashMap(15); + + /** + * Get a skin for a skinnable entity. + * + *

If a Skin instance does not exist, a new one is created and the + * skin data is automatically fetched.

+ * + * @param entity The skinnable entity. + */ + public static Skin get(SkinnableEntity entity) { + Preconditions.checkNotNull(entity); + + String skinName = entity.getSkinName().toLowerCase(); + + Skin skin; + synchronized (CACHE) { + skin = CACHE.get(skinName); + } + + if (skin == null) { + skin = new Skin(skinName); + } + + return skin; + } + + /** + * Constructor. + * + * @param skinName The name of the player the skin belongs to. + */ + Skin(String skinName) { + + this.skinName = skinName.toLowerCase(); + + synchronized (CACHE) { + if (CACHE.containsKey(this.skinName)) + throw new IllegalArgumentException("There is already a skin named " + skinName); + + CACHE.put(this.skinName, this); + } + + ProfileFetcher.fetch(this.skinName, new ProfileFetchHandler() { + + @Override + public void onResult(ProfileRequest request) { + + if (request.getResult() == ProfileFetchResult.NOT_FOUND) { + isValid = false; + return; + } + + if (request.getResult() == ProfileFetchResult.SUCCESS) { + GameProfile profile = request.getProfile(); + setData(profile); + } + } + }); + } + + /** + * Get the name of the skin. + */ + public String getSkinName() { + return skinName; + } + + /** + * Get the ID of the player the skin belongs to. + * + * @return The skin ID or null if it has not been retrieved yet or + * the skin is invalid. + */ + @Nullable + public UUID getSkinId() { + return skinId; + } + + /** + * Determine if the skin is valid. + */ + public boolean isValid() { + return isValid; + } + + /** + * Determine if the skin data has been retrieved. + */ + public boolean hasSkinData() { + return skinData != null; + } + + /** + * Apply the skin data to the specified skinnable entity. + * + *

If invoked before the skin data is ready, the skin is retrieved + * and the skin is automatically applied to the entity at a later time.

+ * + * @param entity The skinnable entity. + * + * @return True if the skin data was available and applied, false if + * the data is being retrieved. + */ + public boolean apply(SkinnableEntity entity) { + Preconditions.checkNotNull(entity); + + NPC npc = entity.getNPC(); + + if (!hasSkinData()) { + + // Use npc cached skin if available. + // If npc requires latest skin, cache is used for faster + // availability until the latest skin can be loaded. + String cachedName = npc.data().get(CACHED_SKIN_UUID_NAME_METADATA); + if (this.skinName.equals(cachedName)) { + + skinData = new Property(this.skinName, + npc.data().get(PLAYER_SKIN_TEXTURE_PROPERTIES), + npc.data().get(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN)); + + skinId = UUID.fromString(npc.data().get(CACHED_SKIN_UUID_METADATA)); + + setNPCSkinData(entity, skinName, skinId, skinData); + + // check if NPC prefers to use cached skin over the latest skin. + if (!entity.getNPC().data().get("update-skin", + Settings.Setting.NPC_SKIN_UPDATE.asBoolean())) { + // cache preferred + return true; + } + + if (!Settings.Setting.NPC_SKIN_UPDATE.asBoolean()) { + // cache preferred + return true; + } + } + + pending.put(entity, null); + return false; + } + + setNPCSkinData(entity, skinName, skinId, skinData); + + return true; + } + + /** + * Apply the skin data to the specified skinnable entity + * and respawn the NPC. + * + * @param entity The skinnable entity. + */ + public void applyAndRespawn(SkinnableEntity entity) { + Preconditions.checkNotNull(entity); + + if (!apply(entity)) + return; + + NPC npc = entity.getNPC(); + + if (npc.isSpawned()) { + npc.despawn(DespawnReason.PENDING_RESPAWN); + npc.spawn(npc.getStoredLocation()); + } + } + + private void setData(@Nullable GameProfile profile) { + + if (profile == null) { + isValid = false; + return; + } + + if (!profile.getName().toLowerCase().equals(skinName)) { + throw new IllegalArgumentException( + "GameProfile name (" + profile.getName() + ") and " + + "skin name (" + skinName + ") do not match."); + } + + skinId = profile.getId(); + skinData = Iterables.getFirst(profile.getProperties().get("textures"), null); + + for (SkinnableEntity entity : pending.keySet()) { + applyAndRespawn(entity); + } + } + + private static void setNPCSkinData(SkinnableEntity entity, + String skinName, UUID skinId, Property skinProperty) { + + NPC npc = entity.getNPC(); + + // cache skins for faster initial skin availability + npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName); + npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString()); + npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES, skinProperty.getValue()); + npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN, skinProperty.getSignature()); + + GameProfile profile = entity.getProfile(); + profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties. + profile.getProperties().put("textures", skinProperty); + } + + public static final String PLAYER_SKIN_TEXTURE_PROPERTIES = "player-skin-textures"; + public static final String PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN = "player-skin-signature"; + public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid"; + public static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name"; + + private static final Map CACHE = new HashMap(20); +} diff --git a/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java new file mode 100644 index 000000000..d9fb3a960 --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java @@ -0,0 +1,250 @@ +package net.citizensnpcs.npc.skin; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.google.common.base.Preconditions; + +import net.citizensnpcs.Settings; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.util.NMS; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitTask; + +/** + * Handles and synchronizes add and remove packets for Player type NPC's + * in order to properly apply the NPC skin. + * + *

Used as one instance per NPC entity.

+ */ +public class SkinPacketTracker { + + private final SkinnableEntity entity; + private final Map inProgress = + new HashMap(Bukkit.getMaxPlayers() / 2); + + private Skin skin; + private boolean isRemoved; + + /** + * Constructor. + * + * @param entity The skinnable entity the instance belongs to. + */ + public SkinPacketTracker(SkinnableEntity entity) { + Preconditions.checkNotNull(entity); + + this.entity = entity; + this.skin = Skin.get(entity); + + if (LISTENER == null) { + LISTENER = new PlayerListener(); + Bukkit.getPluginManager().registerEvents(LISTENER, CitizensAPI.getPlugin()); + } + } + + /** + * Get the NPC skin. + */ + public Skin getSkin() { + return skin; + } + + /** + * Send skin related packets to a player. + * + * @param player The player. + */ + public void updateViewer(final Player player) { + Preconditions.checkNotNull(player); + + if (isRemoved || player.hasMetadata("NPC")) + return; + + PlayerEntry entry = inProgress.get(player.getUniqueId()); + if (entry != null) { + entry.cancel(); + } + else { + entry = new PlayerEntry(player); + } + + PLAYER_LIST_REMOVER.cancelPackets(player, entity); + + inProgress.put(player.getUniqueId(), entry); + skin.apply(entity); + NMS.sendPlayerListAdd(player, entity.getBukkitEntity()); + + scheduleRemovePacket(entry, 2); + } + + /** + * Send skin related packets to all nearby players within the specified block radius. + * + * @param radius The radius. + */ + public void updateNearbyViewers(double radius) { + + radius *= radius; + + org.bukkit.World world = entity.getBukkitEntity().getWorld(); + Player from = entity.getBukkitEntity(); + Location location = from.getLocation(); + + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + + if (player == null || player.hasMetadata("NPC")) + continue; + + if (world != player.getWorld() || !player.canSee(from)) + continue; + + if (location.distanceSquared(player.getLocation(CACHE_LOCATION)) > radius) + continue; + + updateViewer(player); + } + } + + /** + * Invoke when the NPC entity is removed. + * + *

Sends remove packets to all players.

+ */ + public void onRemoveNPC() { + + isRemoved = true; + + Collection players = Bukkit.getOnlinePlayers(); + + for (Player player : players) { + + if (player.hasMetadata("NPC")) + continue; + + // send packet now and later to ensure removal from player list + NMS.sendPlayerListRemove(player, entity.getBukkitEntity()); + PLAYER_LIST_REMOVER.sendPacket(player, entity); + } + } + + /** + * Notify that the NPC skin has been changed. + */ + public void notifySkinChange() { + this.skin = Skin.get(entity); + skin.applyAndRespawn(entity); + } + + /** + * Notify the tracker that a remove packet has been sent to the + * specified player. + * + * @param playerId The ID of the player. + */ + void notifyRemovePacketSent(UUID playerId) { + + PlayerEntry entry = inProgress.get(playerId); + if (entry == null) + return; + + if (entry.removeCount == 0) + return; + + entry.removeCount -= 1; + if (entry.removeCount == 0) { + inProgress.remove(playerId); + } + else { + scheduleRemovePacket(entry); + } + } + + /** + * Notify the tracker that a remove packet has been sent to the + * specified player. + * + * @param playerId The ID of the player. + */ + void notifyRemovePacketCancelled(UUID playerId) { + inProgress.remove(playerId); + } + + private void scheduleRemovePacket(PlayerEntry entry, int count) { + + if (!shouldRemoveFromPlayerList()) + return; + + entry.removeCount = count; + scheduleRemovePacket(entry); + } + + private void scheduleRemovePacket(final PlayerEntry entry) { + + if (isRemoved) + return; + + entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), + new Runnable() { + + @Override + public void run() { + + if (shouldRemoveFromPlayerList()) { + PLAYER_LIST_REMOVER.sendPacket(entry.player, entity); + } + } + }, PACKET_DELAY_REMOVE); + } + + private boolean shouldRemoveFromPlayerList() { + + boolean isTablistDisabled = Settings.Setting.DISABLE_TABLIST.asBoolean(); + boolean isNpcRemoved = entity.getNPC().data().get("removefromplayerlist", + Settings.Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean()); + + return isNpcRemoved && isTablistDisabled; + } + + private class PlayerEntry { + Player player; + int removeCount; + BukkitTask removeTask; + + PlayerEntry (Player player) { + this.player = player; + } + + // cancel previous packet tasks so they do not interfere with + // new tasks + void cancel() { + if (removeTask != null) + removeTask.cancel(); + removeCount = 0; + } + } + + private static class PlayerListener implements Listener { + + @EventHandler + private void onPlayerQuit(PlayerQuitEvent event) { + + // this also causes any entries in the "inProgress" field to + // be removed. + PLAYER_LIST_REMOVER.cancelPackets(event.getPlayer()); + } + } + + private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0); + private static final int PACKET_DELAY_REMOVE = 1; + private static final PlayerListRemover PLAYER_LIST_REMOVER = new PlayerListRemover(); + private static PlayerListener LISTENER; +} diff --git a/src/main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java b/src/main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java new file mode 100644 index 000000000..e98a54f2e --- /dev/null +++ b/src/main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java @@ -0,0 +1,40 @@ +package net.citizensnpcs.npc.skin; + +import com.mojang.authlib.GameProfile; +import net.citizensnpcs.npc.ai.NPCHolder; +import org.bukkit.entity.Player; + +/** + * Interface for player entities that are skinnable. + */ +public interface SkinnableEntity extends NPCHolder { + + /** + * Get the entities skin packet tracker. + */ + SkinPacketTracker getSkinTracker(); + + /** + * Get the bukkit entity. + */ + Player getBukkitEntity(); + + /** + * Get entity game profile. + */ + GameProfile getProfile(); + + /** + * Get the name of the player whose skin the NPC uses. + */ + String getSkinName(); + + /** + * Set the name of the player whose skin the NPC + * uses. + * + *

Setting the skin name automatically updates and + * respawn the NPC.

+ */ + void setSkinName(String name); +} diff --git a/src/main/java/net/citizensnpcs/util/NMS.java b/src/main/java/net/citizensnpcs/util/NMS.java index 107c84f6f..90c22aa2b 100644 --- a/src/main/java/net/citizensnpcs/util/NMS.java +++ b/src/main/java/net/citizensnpcs/util/NMS.java @@ -2,7 +2,9 @@ package net.citizensnpcs.util; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.SocketAddress; +import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -10,33 +12,25 @@ import java.util.Map; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; +import javax.annotation.Nullable; -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.craftbukkit.v1_8_R3.CraftServer; -import org.bukkit.craftbukkit.v1_8_R3.CraftSound; -import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; -import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity; -import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Horse; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.plugin.PluginLoadOrder; - +import com.google.common.base.Preconditions; import com.mojang.authlib.GameProfile; +import com.mojang.authlib.GameProfileRepository; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; +import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; +import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse; +import com.mojang.util.UUIDTypeAdapter; -import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.api.command.exception.CommandException; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.npc.ai.NPCHolder; import net.citizensnpcs.npc.entity.EntityHumanNPC; import net.citizensnpcs.npc.network.EmptyChannel; +import net.citizensnpcs.npc.skin.SkinnableEntity; import net.citizensnpcs.util.nms.PlayerlistTrackerEntry; import net.minecraft.server.v1_8_R3.AttributeInstance; import net.minecraft.server.v1_8_R3.Block; @@ -60,28 +54,136 @@ import net.minecraft.server.v1_8_R3.NavigationAbstract; import net.minecraft.server.v1_8_R3.NetworkManager; import net.minecraft.server.v1_8_R3.Packet; import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; -import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo.EnumPlayerInfoAction; import net.minecraft.server.v1_8_R3.PathfinderGoalSelector; import net.minecraft.server.v1_8_R3.World; import net.minecraft.server.v1_8_R3.WorldServer; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.craftbukkit.v1_8_R3.CraftServer; +import org.bukkit.craftbukkit.v1_8_R3.CraftSound; +import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Horse; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.plugin.PluginLoadOrder; + @SuppressWarnings("unchecked") public class NMS { + private NMS() { // util class } - public static void addOrRemoveFromPlayerList(org.bukkit.entity.Entity entity, boolean remove) { - if (entity == null) - return; - EntityHuman handle = (EntityHuman) getHandle(entity); - if (handle.world == null) - return; - if (remove) { - handle.world.players.remove(handle); - } else if (!handle.world.players.contains(handle)) { - handle.world.players.add(handle); + public static GameProfileRepository getGameProfileRepository() { + return ((CraftServer) Bukkit.getServer()).getServer() + .getGameProfileRepository(); + } + + public static boolean addToWorld(org.bukkit.World world, + org.bukkit.entity.Entity entity, + CreatureSpawnEvent.SpawnReason reason) { + Preconditions.checkNotNull(world); + Preconditions.checkNotNull(entity); + Preconditions.checkNotNull(reason); + + Entity nmsEntity = ((CraftEntity)entity).getHandle(); + return ((CraftWorld)world).getHandle().addEntity(nmsEntity, reason); + } + + public static void removeFromWorld(org.bukkit.entity.Entity entity) { + Preconditions.checkNotNull(entity); + + Entity nmsEntity = ((CraftEntity)entity).getHandle(); + nmsEntity.world.removeEntity(nmsEntity); + } + + @Nullable + public static SkinnableEntity getSkinnable(org.bukkit.entity.Entity entity) { + Preconditions.checkNotNull(entity); + + Entity nmsEntity = ((CraftEntity) entity).getHandle(); + if (nmsEntity instanceof SkinnableEntity) { + return (SkinnableEntity)nmsEntity; } + return null; + } + + public static void sendPlayerListAdd(Player recipient, Player listPlayer) { + Preconditions.checkNotNull(recipient); + Preconditions.checkNotNull(listPlayer); + + EntityPlayer entity = ((CraftPlayer)listPlayer).getHandle(); + + sendPacket(recipient, new PacketPlayOutPlayerInfo( + PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, entity)); + } + + public static void sendPlayerListRemove(Player recipient, Player listPlayer) { + Preconditions.checkNotNull(recipient); + Preconditions.checkNotNull(listPlayer); + + EntityPlayer entity = ((CraftPlayer)listPlayer).getHandle(); + + sendPacket(recipient, new PacketPlayOutPlayerInfo( + PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entity)); + } + + public static void sendPlayerListRemove(Player recipient, + Collection skinnableNPCs) { + Preconditions.checkNotNull(recipient); + Preconditions.checkNotNull(skinnableNPCs); + + EntityPlayer[] entities = new EntityPlayer[skinnableNPCs.size()]; + int i=0; + for (SkinnableEntity skinnable : skinnableNPCs) { + entities[i] = (EntityPlayer)skinnable; + i++; + } + + sendPacket(recipient, new PacketPlayOutPlayerInfo( + PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entities)); + } + + /* + * Yggdrasil's default implementation of this method silently fails instead of throwing + * an Exception like it should. + */ + public static GameProfile fillProfileProperties(GameProfile profile, + boolean requireSecure) throws Exception { + + if (Bukkit.isPrimaryThread()) + throw new IllegalStateException("NMS.fillProfileProperties cannot be invoked from the main thread."); + + MinecraftSessionService sessionService = ((CraftServer) Bukkit.getServer()).getServer().aD(); + + YggdrasilAuthenticationService auth = ((YggdrasilMinecraftSessionService) sessionService) + .getAuthenticationService(); + + URL url = HttpAuthenticationService.constantURL( + "https://sessionserver.mojang.com/session/minecraft/profile/" + + UUIDTypeAdapter.fromUUID(profile.getId())); + + url = HttpAuthenticationService.concatenateURL(url, "unsigned=" + !requireSecure); + + MinecraftProfilePropertiesResponse response = (MinecraftProfilePropertiesResponse) + MAKE_REQUEST.invoke(auth, url, null, MinecraftProfilePropertiesResponse.class); + if (response == null) + return profile; + + GameProfile result = new GameProfile(response.getId(), response.getName()); + result.getProperties().putAll(response.getProperties()); + profile.getProperties().putAll(response.getProperties()); + + return result; } public static void attack(EntityLiving handle, Entity target) { @@ -506,15 +608,6 @@ public class NMS { NMS.sendPacketsNearby(from, location, Arrays.asList(packets), 64); } - public static void sendPlayerlistPacket(boolean showInPlayerlist, Player npc) { - if (!showInPlayerlist && !Setting.DISABLE_TABLIST.asBoolean()) - return; - PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo( - showInPlayerlist ? EnumPlayerInfoAction.ADD_PLAYER : EnumPlayerInfoAction.REMOVE_PLAYER, - ((CraftPlayer) npc).getHandle()); - sendToOnline(packet); - } - public static void sendToOnline(Packet... packets) { Validate.notNull(packets, "packets cannot be null"); for (Player player : Bukkit.getOnlinePlayers()) { @@ -703,6 +796,7 @@ public class NMS { private static Field SKULL_PROFILE_FIELD; private static Field TRACKED_ENTITY_SET = NMS.getField(EntityTracker.class, "c"); + private static Method MAKE_REQUEST; static { try { @@ -713,5 +807,13 @@ public class NMS { } catch (Exception e) { Messaging.logTr(Messages.ERROR_GETTING_ID_MAPPING, e.getMessage()); } + + try { + MAKE_REQUEST = YggdrasilAuthenticationService.class.getDeclaredMethod("makeRequest", URL.class, + Object.class, Class.class); + MAKE_REQUEST.setAccessible(true); + } catch (Exception ex) { + ex.printStackTrace(); + } } } diff --git a/src/main/java/net/citizensnpcs/util/nms/PlayerlistTrackerEntry.java b/src/main/java/net/citizensnpcs/util/nms/PlayerlistTrackerEntry.java index 3e6758b0e..22eb2c9dd 100644 --- a/src/main/java/net/citizensnpcs/util/nms/PlayerlistTrackerEntry.java +++ b/src/main/java/net/citizensnpcs/util/nms/PlayerlistTrackerEntry.java @@ -1,17 +1,14 @@ package net.citizensnpcs.util.nms; -import java.lang.reflect.Field; - -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; - -import net.citizensnpcs.Settings.Setting; -import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.npc.entity.EntityHumanNPC; +import net.citizensnpcs.npc.skin.SkinnableEntity; import net.citizensnpcs.util.NMS; import net.minecraft.server.v1_8_R3.Entity; import net.minecraft.server.v1_8_R3.EntityPlayer; import net.minecraft.server.v1_8_R3.EntityTrackerEntry; -import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; public class PlayerlistTrackerEntry extends EntityTrackerEntry { public PlayerlistTrackerEntry(Entity entity, int i, int j, boolean flag) { @@ -24,28 +21,26 @@ public class PlayerlistTrackerEntry extends EntityTrackerEntry { @Override public void updatePlayer(final EntityPlayer entityplayer) { + + // prevent updates to NPC "viewers" + if (entityplayer instanceof EntityHumanNPC) + return; + if (entityplayer != this.tracker && c(entityplayer)) { + if (!this.trackedPlayers.contains(entityplayer) && ((entityplayer.u().getPlayerChunkMap().a(entityplayer, this.tracker.ae, this.tracker.ag)) || (this.tracker.attachedToPlayer))) { - if ((this.tracker instanceof EntityPlayer)) { - Player player = ((EntityPlayer) this.tracker).getBukkitEntity(); - if (!entityplayer.getBukkitEntity().canSee(player)) { - return; - } - entityplayer.playerConnection.sendPacket(new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, (EntityPlayer) this.tracker)); - if (Setting.DISABLE_TABLIST.asBoolean()) { - new BukkitRunnable() { - @Override - public void run() { - entityplayer.playerConnection.sendPacket(new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, - (EntityPlayer) tracker)); - } - }.runTaskLater(CitizensAPI.getPlugin(), 2); - } + if ((this.tracker instanceof SkinnableEntity)) { + + SkinnableEntity skinnable = (SkinnableEntity)this.tracker; + + Player player = skinnable.getBukkitEntity(); + if (!entityplayer.getBukkitEntity().canSee(player)) + return; + + skinnable.getSkinTracker().updateViewer(entityplayer.getBukkitEntity()); } } }