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 Deque queue = new LinkedList(); + private final Deque queue = new ArrayDeque(); private final Map requested = new HashMap(35); - private final Object sync = new Object(); - - /** - * Get the singleton instance. - */ - public static ProfileFetchThread get() { - if (PROFILE_THREAD == null) { - PROFILE_THREAD = new ProfileFetchThread(); - Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD, - 11, 20); - } - return PROFILE_THREAD; - } + 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 subscriber Optional subscriber to be notified when a result is available. - * Subscriber always invoked from the main thread. + * @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 */ - public void fetch(String name, @Nullable ProfileFetchSubscriber subscriber) { + void fetch(String name, @Nullable ProfileFetchHandler handler) { Preconditions.checkNotNull(name); - ProfileRequest request = requested.get(name); - - if (request != null) { - - if (subscriber != null) { - - if (request.getResult() == ProfileFetchResult.PENDING) { - request.addSubscriber(subscriber); - } - else { - subscriber.onResult(request); - } - } - - return; - } - - request = new ProfileRequest(name, subscriber); + name = name.toLowerCase(); + ProfileRequest request; synchronized (sync) { - queue.add(request); + request = requested.get(name); + if (request == null) { + request = new ProfileRequest(name, handler); + queue.add(request); + requested.put(name, request); + return; + } } - requested.put(name, request); + if (handler != null) { + if (request.getResult() == ProfileFetchResult.PENDING) { + addHandler(request, handler); + } + else { + sendResult(handler, request); + } + } } @Override @@ -89,11 +78,38 @@ public class ProfileFetchThread implements Runnable { return; requests = new ArrayList(queue); + queue.clear(); } - profileFetcher.fetch(requests); + profileFetcher.fetchRequests(requests); } - private static ProfileFetchThread PROFILE_THREAD; + 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 index 344cf0415..af9dcfd7d 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java @@ -1,29 +1,53 @@ 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 javax.annotation.Nullable; -import java.util.Collection; +import org.bukkit.Bukkit; /** * Fetches game profiles that include skin data from Mojang servers. * * @see ProfileFetchThread */ -class ProfileFetcher { +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. */ - public void fetch(final Collection requests) { + void fetchRequests(final Collection requests) { Preconditions.checkNotNull(requests); final GameProfileRepository repo = NMS.getGameProfileRepository(); @@ -43,7 +67,7 @@ class ProfileFetcher { public void onProfileLookupFailed(GameProfile profile, Exception e) { if (Messaging.isDebugging()) { - Messaging.debug("Profile lookup for skin '" + + Messaging.debug("Profile lookup for player '" + profile.getName() + "' failed: " + getExceptionMsg(e)); } @@ -77,7 +101,7 @@ class ProfileFetcher { } catch (Exception e) { if (Messaging.isDebugging()) { - Messaging.debug("Profile lookup for skin '" + + Messaging.debug("Profile lookup for player '" + profile.getName() + "' failed: " + getExceptionMsg(e)); } @@ -125,4 +149,6 @@ class ProfileFetcher { 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 index 4a1fcdb11..8688a3ae7 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java @@ -1,13 +1,15 @@ 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; -import javax.annotation.Nullable; -import java.util.ArrayDeque; -import java.util.Deque; +import net.citizensnpcs.api.CitizensAPI; + +import org.bukkit.Bukkit; /** * Stores basic information about a single profile used to request @@ -18,24 +20,24 @@ import java.util.Deque; public class ProfileRequest { private final String playerName; - private Deque subscribers; + private Deque handlers; private GameProfile profile; - private ProfileFetchResult result = ProfileFetchResult.PENDING; + private volatile ProfileFetchResult result = ProfileFetchResult.PENDING; /** * Constructor. * * @param playerName The name of the player whose profile is being requested. - * @param subscriber Optional subscriber to be notified when a result is available - * for the profile. Subscriber always invoked from the main thread. + * @param handler Optional handler to handle the result for the profile. + * Handler always invoked from the main thread. */ - ProfileRequest(String playerName, @Nullable ProfileFetchSubscriber subscriber) { + ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) { Preconditions.checkNotNull(playerName); this.playerName = playerName; - if (subscriber != null) - addSubscriber(subscriber); + if (handler != null) + addHandler(handler); } /** @@ -64,19 +66,24 @@ public class ProfileRequest { } /** - * Add a result subscriber to be notified when a result is available. + * Add one time result handler. * - *

Subscriber 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 ArrayDeque(); + if (result != ProfileFetchResult.PENDING) { + handler.onResult(this); + return; + } - subscribers.addLast(subscriber); + if (handlers == null) + handlers = new ArrayDeque(); + + handlers.addLast(handler); } /** @@ -96,14 +103,14 @@ public class ProfileRequest { ProfileRequest.this.profile = profile; ProfileRequest.this.result = result; - if (subscribers == null) + if (handlers == null) return; - while (!subscribers.isEmpty()) { - subscribers.removeFirst().onResult(ProfileRequest.this); + while (!handlers.isEmpty()) { + handlers.removeFirst().onResult(ProfileRequest.this); } - subscribers = null; + handlers = null; } }); } diff --git a/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java b/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java index 5c1aeda73..d6682550c 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java +++ b/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java @@ -35,10 +35,10 @@ public class PlayerListRemover { /** * Send a remove packet to the specified player for the specified - * human NPC entity. + * skinnable entity. * - * @param player The player to send the packet to. - * @param entity The entity to remove. + * @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); @@ -58,6 +58,8 @@ public class PlayerListRemover { Preconditions.checkNotNull(player); PlayerEntry entry = pending.remove(player.getUniqueId()); + if (entry == null) + return; for (SkinnableEntity entity : entry.toRemove) { entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId()); @@ -66,10 +68,10 @@ public class PlayerListRemover { /** * Cancel packets pending to be sent to the specified player - * for the specified skinnable NPC. + * for the specified skinnable entity. * * @param player The player. - * @param skinnable The skinnable NPC. + * @param skinnable The skinnable entity. */ public void cancelPackets(Player player, SkinnableEntity skinnable) { Preconditions.checkNotNull(player); @@ -129,14 +131,13 @@ public class PlayerListRemover { Iterator skinIterator = entry.toRemove.iterator(); while (skinIterator.hasNext()) { + if (i >= maxPacketEntries) + break; + SkinnableEntity skinnable = skinIterator.next(); skinnableList.add(skinnable); skinIterator.remove(); - - if (i > maxPacketEntries) - break; - i++; } diff --git a/src/main/java/net/citizensnpcs/npc/skin/Skin.java b/src/main/java/net/citizensnpcs/npc/skin/Skin.java index a06ad7f10..3435ef3eb 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/Skin.java +++ b/src/main/java/net/citizensnpcs/npc/skin/Skin.java @@ -15,8 +15,8 @@ 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.ProfileFetchSubscriber; -import net.citizensnpcs.npc.profile.ProfileFetchThread; +import net.citizensnpcs.npc.profile.ProfileFetchHandler; +import net.citizensnpcs.npc.profile.ProfileFetcher; import net.citizensnpcs.npc.profile.ProfileRequest; /** @@ -28,15 +28,15 @@ public class Skin { private volatile Property skinData; private volatile UUID skinId; private volatile boolean isValid = true; - private final Map pending = new WeakHashMap(30); + private final Map pending = new WeakHashMap(15); /** - * Get a skin for a human NPC entity. + * 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 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 skinnableNPCs) { + public static void sendPlayerListRemove(Player recipient, + Collection skinnableNPCs) { Preconditions.checkNotNull(recipient); Preconditions.checkNotNull(skinnableNPCs);