diff --git a/src/main/java/net/citizensnpcs/EventListen.java b/src/main/java/net/citizensnpcs/EventListen.java index 4ecd274e5..1eb609b73 100644 --- a/src/main/java/net/citizensnpcs/EventListen.java +++ b/src/main/java/net/citizensnpcs/EventListen.java @@ -453,6 +453,9 @@ public class EventListen implements Listener { 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)); } @@ -492,7 +495,7 @@ public class EventListen implements Listener { && player.getLocation(CACHE_LOCATION) .distanceSquared(npc.getStoredLocation()) < viewDistance) { - SkinnableEntity skinnable = NMS.getSkinnableNPC(npcEntity); + SkinnableEntity skinnable = NMS.getSkinnable(npcEntity); results.add(skinnable); } @@ -605,17 +608,20 @@ public class EventListen implements Listener { Location currentLoc = player.getLocation(YAW_LOCATION); float currentYaw = currentLoc.getYaw(); - float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat(); + if (rotationCount < 2) { - boolean hasRotated = - Math.abs(NMS.clampYaw(currentYaw - this.initialYaw)) < rotationDegrees; + float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat(); - // update the first 2 times the player rotates. helps load skins around player - // after the player logs/teleports. - if (hasRotated && rotationCount < 2) { - rotationCount++; - reset(player); - return true; + 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 diff --git a/src/main/java/net/citizensnpcs/commands/NPCCommands.java b/src/main/java/net/citizensnpcs/commands/NPCCommands.java index 15c3fb76c..d74e4d4a9 100644 --- a/src/main/java/net/citizensnpcs/commands/NPCCommands.java +++ b/src/main/java/net/citizensnpcs/commands/NPCCommands.java @@ -1314,7 +1314,7 @@ public class NPCCommands { Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName); if (npc.isSpawned()) { - SkinnableEntity skinnable = NMS.getSkinnableNPC(npc.getEntity()); + SkinnableEntity skinnable = NMS.getSkinnable(npc.getEntity()); if (skinnable != null) { skinnable.setSkinName(skinName); } diff --git a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java index 7ab1036b7..186ee4afa 100644 --- a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java +++ b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java @@ -189,7 +189,7 @@ public class CitizensNPC extends AbstractNPC { 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.getSkinnableNPC(getEntity()); + SkinnableEntity skinnable = NMS.getSkinnable(getEntity()); if (skinnable != null) { final double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); skinnable.getSkinTracker().updateNearbyViewers(viewDistance); @@ -200,7 +200,7 @@ public class CitizensNPC extends AbstractNPC { if (getEntity() == null || !getEntity().isValid()) return; - SkinnableEntity npc = NMS.getSkinnableNPC(getEntity()); + SkinnableEntity npc = NMS.getSkinnable(getEntity()); if (npc == null) return; diff --git a/src/main/java/net/citizensnpcs/npc/entity/HumanController.java b/src/main/java/net/citizensnpcs/npc/entity/HumanController.java index 41f68ebb8..c0ad8602c 100644 --- a/src/main/java/net/citizensnpcs/npc/entity/HumanController.java +++ b/src/main/java/net/citizensnpcs/npc/entity/HumanController.java @@ -130,7 +130,7 @@ public class HumanController extends AbstractEntityController { NMS.removeFromWorld(getBukkitEntity()); - SkinnableEntity npc = NMS.getSkinnableNPC(getBukkitEntity()); + SkinnableEntity npc = NMS.getSkinnable(getBukkitEntity()); npc.getSkinTracker().onRemoveNPC(); super.remove(); diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchSubscriber.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java similarity index 87% rename from src/main/java/net/citizensnpcs/npc/profile/ProfileFetchSubscriber.java rename to src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java index 9b8a29262..8f7e1f7e8 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchSubscriber.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java @@ -3,7 +3,7 @@ package net.citizensnpcs.npc.profile; /** * Interface for a subscriber of the results of a profile fetch. */ -public interface ProfileFetchSubscriber { +public interface ProfileFetchHandler { /** * Invoked when a result for a profile is ready. diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java index d3838592e..39e9bb4f0 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java @@ -1,9 +1,9 @@ package net.citizensnpcs.npc.profile; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -19,63 +19,52 @@ import org.bukkit.Bukkit; * *
Maintains a cache of profiles so that no profile is ever requested more than once * during a single server session.
+ * + * @see ProfileFetcher */ -public class ProfileFetchThread implements Runnable { +class ProfileFetchThread implements Runnable { private final ProfileFetcher profileFetcher = new ProfileFetcher(); - private final DequeSubscriber is always invoked from the main thread.
+ *Handler is always invoked from the main thread.
* - * @param subscriber The subscriber. + * @param handler The result handler. */ - public void addSubscriber(ProfileFetchSubscriber subscriber) { - Preconditions.checkNotNull(subscriber); + public void addHandler(ProfileFetchHandler handler) { + Preconditions.checkNotNull(handler); - if (subscribers == null) - subscribers = new ArrayDequeIf a Skin instance does not exist, a new one is created and the * skin data is automatically fetched.
* - * @param entity The human NPC entity. + * @param entity The skinnable entity. */ public static Skin get(SkinnableEntity entity) { Preconditions.checkNotNull(entity); @@ -65,13 +65,13 @@ public class Skin { this.skinName = skinName.toLowerCase(); synchronized (CACHE) { - if (CACHE.containsKey(skinName)) + if (CACHE.containsKey(this.skinName)) throw new IllegalArgumentException("There is already a skin named " + skinName); - CACHE.put(skinName, this); + CACHE.put(this.skinName, this); } - ProfileFetchThread.get().fetch(skinName, new ProfileFetchSubscriber() { + ProfileFetcher.fetch(this.skinName, new ProfileFetchHandler() { @Override public void onResult(ProfileRequest request) { @@ -82,11 +82,8 @@ public class Skin { } if (request.getResult() == ProfileFetchResult.SUCCESS) { - GameProfile profile = request.getProfile(); - - skinId = profile.getId(); - skinData = Iterables.getFirst(profile.getProperties().get("textures"), null); + setData(profile); } } }); @@ -125,47 +122,15 @@ public class Skin { } /** - * Set skin data. - * - * @param profile The profile that contains the skin data. If set to null, - * it's assumed that the skin is not valid. - * - * @throws IllegalStateException if not invoked from the main thread. - * @throws IllegalArgumentException if the profile name does not match the skin data. - */ - public 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); - } - } - - /** - * Apply the skin data to the specified human NPC entity. + * 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 human NPC entity. + * @param entity The skinnable entity. * * @return True if the skin data was available and applied, false if * the data is being retrieved. - * - * @throws IllegalStateException if not invoked from the main thread. */ public boolean apply(SkinnableEntity entity) { Preconditions.checkNotNull(entity); @@ -173,7 +138,6 @@ public class Skin { NPC npc = entity.getNPC(); if (!hasSkinData()) { - pending.put(entity, null); // Use npc cached skin if available. // If npc requires latest skin, cache is used for faster @@ -202,9 +166,7 @@ public class Skin { } } - // get latest skin - fetchSkinFor(entity); - + pending.put(entity, null); return false; } @@ -233,20 +195,25 @@ public class Skin { } } - private void fetchSkinFor(final SkinnableEntity entity) { + private void setData(@Nullable GameProfile profile) { - ProfileFetchThread.get().fetch(skinName, new ProfileFetchSubscriber() { + if (profile == null) { + isValid = false; + return; + } - @Override - public void onResult(ProfileRequest request) { + if (!profile.getName().toLowerCase().equals(skinName)) { + throw new IllegalArgumentException( + "GameProfile name (" + profile.getName() + ") and " + + "skin name (" + skinName + ") do not match."); + } - if (request.getResult() != ProfileFetchResult.SUCCESS) - return; + skinId = profile.getId(); + skinData = Iterables.getFirst(profile.getProperties().get("textures"), null); - double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); - entity.getSkinTracker().updateNearbyViewers(viewDistance); - } - }); + for (SkinnableEntity entity : pending.keySet()) { + applyAndRespawn(entity); + } } private static void setNPCSkinData(SkinnableEntity entity, diff --git a/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java index 4b4e37311..d9fb3a960 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java +++ b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java @@ -17,6 +17,7 @@ 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 @@ -36,7 +37,7 @@ public class SkinPacketTracker { /** * Constructor. * - * @param entity The human NPC entity the instance belongs to. + * @param entity The skinnable entity the instance belongs to. */ public SkinPacketTracker(SkinnableEntity entity) { Preconditions.checkNotNull(entity); @@ -57,48 +58,6 @@ public class SkinPacketTracker { return skin; } - /** - * 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); - } - /** * Send skin related packets to a player. * @@ -107,19 +66,21 @@ public class SkinPacketTracker { public void updateViewer(final Player player) { Preconditions.checkNotNull(player); - if (player.hasMetadata("NPC")) + if (isRemoved || player.hasMetadata("NPC")) return; - if (isRemoved || inProgress.containsKey(player.getUniqueId())) - return; - - PlayerEntry entry = new PlayerEntry(player); - inProgress.put(player.getUniqueId(), entry); + 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); @@ -175,6 +136,48 @@ public class SkinPacketTracker { } } + /** + * 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()) @@ -189,7 +192,7 @@ public class SkinPacketTracker { if (isRemoved) return; - Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), + entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() { @Override @@ -214,9 +217,19 @@ public class SkinPacketTracker { 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 { diff --git a/src/main/java/net/citizensnpcs/util/NMS.java b/src/main/java/net/citizensnpcs/util/NMS.java index b9ef23d78..90c22aa2b 100644 --- a/src/main/java/net/citizensnpcs/util/NMS.java +++ b/src/main/java/net/citizensnpcs/util/NMS.java @@ -12,8 +12,10 @@ import java.util.Map; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; +import javax.annotation.Nullable; 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; @@ -21,26 +23,6 @@ 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.npc.skin.SkinnableEntity; -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; - -import com.mojang.authlib.GameProfile; import net.citizensnpcs.api.command.exception.CommandException; import net.citizensnpcs.api.npc.NPC; @@ -48,6 +30,7 @@ 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; @@ -75,7 +58,23 @@ 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 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.event.entity.CreatureSpawnEvent; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.plugin.PluginLoadOrder; @SuppressWarnings("unchecked") public class NMS { @@ -108,7 +107,7 @@ public class NMS { } @Nullable - public static SkinnableEntity getSkinnableNPC(org.bukkit.entity.Entity entity) { + public static SkinnableEntity getSkinnable(org.bukkit.entity.Entity entity) { Preconditions.checkNotNull(entity); Entity nmsEntity = ((CraftEntity) entity).getHandle(); @@ -138,7 +137,8 @@ public class NMS { PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entity)); } - public static void sendPlayerListRemove(Player recipient, Collection extends SkinnableEntity> skinnableNPCs) { + public static void sendPlayerListRemove(Player recipient, + Collection extends SkinnableEntity> skinnableNPCs) { Preconditions.checkNotNull(recipient); Preconditions.checkNotNull(skinnableNPCs);