From ec3b184f7220c17a6121a48ce4bca6b0404368c9 Mon Sep 17 00:00:00 2001 From: fullwall Date: Sat, 29 Aug 2015 15:39:03 +0800 Subject: [PATCH] Fix NPE --- .../net/citizensnpcs/npc/CitizensNPC.java | 1 - .../npc/profile/ProfileFetchHandler.java | 4 +- .../npc/profile/ProfileFetchThread.java | 65 ++-- .../npc/profile/ProfileFetcher.java | 151 ++++---- .../npc/profile/ProfileRequest.java | 94 ++--- .../npc/skin/PlayerListRemover.java | 63 +-- .../java/net/citizensnpcs/npc/skin/Skin.java | 360 +++++++++--------- .../npc/skin/SkinPacketTracker.java | 232 ++++++----- 8 files changed, 476 insertions(+), 494 deletions(-) diff --git a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java index 186ee4afa..4f6c29eb7 100644 --- a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java +++ b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java @@ -196,7 +196,6 @@ public class CitizensNPC extends AbstractNPC { Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { @Override public void run() { - if (getEntity() == null || !getEntity().isValid()) return; diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java index 8f7e1f7e8..5b2d00c1d 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java @@ -4,11 +4,11 @@ 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. + * @param request + * The profile request that was handled. */ void onResult(ProfileRequest request); } diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java index 39e9bb4f0..b69d83f77 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java @@ -6,37 +6,40 @@ import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; + import javax.annotation.Nullable; +import org.bukkit.Bukkit; + 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.

+ *

+ * 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() {} + 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. + * @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 */ @@ -60,8 +63,7 @@ class ProfileFetchThread implements Runnable { if (request.getResult() == ProfileFetchResult.PENDING) { addHandler(request, handler); - } - else { + } else { sendResult(handler, request); } } @@ -69,47 +71,34 @@ class ProfileFetchThread implements Runnable { @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); } - 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); + 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); } } diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java index af9dcfd7d..61b7f7389 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java @@ -1,8 +1,11 @@ package net.citizensnpcs.npc.profile; import java.util.Collection; + import javax.annotation.Nullable; +import org.bukkit.Bukkit; + import com.google.common.base.Preconditions; import com.mojang.authlib.Agent; import com.mojang.authlib.GameProfile; @@ -13,39 +16,20 @@ 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() { } - ProfileFetcher() {} - /** * Fetch one or more profiles. * - * @param requests The profile requests. + * @param requests + * The profile requests. */ void fetchRequests(final Collection requests) { Preconditions.checkNotNull(requests); @@ -54,70 +38,85 @@ public class ProfileFetcher { String[] playerNames = new String[requests.size()]; - int i=0; + int i = 0; for (ProfileRequest request : requests) { playerNames[i] = request.getPlayerName(); i++; } - repo.findProfilesByNames(playerNames, Agent.MINECRAFT, - new ProfileLookupCallback() { + repo.findProfilesByNames(playerNames, Agent.MINECRAFT, new ProfileLookupCallback() { - @Override - public void onProfileLookupFailed(GameProfile profile, Exception e) { + @Override + public void onProfileLookupFailed(GameProfile profile, Exception e) { - if (Messaging.isDebugging()) { - Messaging.debug("Profile lookup for player '" + - profile.getName() + "' failed: " + getExceptionMsg(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; + 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); - } + 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)); } - @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); - } - } + if (isTooManyRequests(e)) { + request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS); + } else { + request.setResult(null, ProfileFetchResult.FAILED); } - }); + } + } + }); + } + + /** + * 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); } @Nullable private static ProfileRequest findRequest(String name, Collection requests) { - name = name.toLowerCase(); for (ProfileRequest request : requests) { @@ -127,6 +126,12 @@ public class ProfileFetcher { return null; } + 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 isProfileNotFound(Exception e) { String message = e.getMessage(); String cause = e.getCause() != null ? e.getCause().getMessage() : null; @@ -135,12 +140,6 @@ public class ProfileFetcher { || (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(); diff --git a/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java b/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java index 8688a3ae7..37ecee863 100644 --- a/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java +++ b/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java @@ -2,75 +2,56 @@ package net.citizensnpcs.npc.profile; import java.util.ArrayDeque; import java.util.Deque; + import javax.annotation.Nullable; +import org.bukkit.Bukkit; + 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. + * Stores basic information about a single profile used to request profiles from the Mojang servers. * - *

Also stores the result of the request.

+ *

+ * Also stores the result of the request. + *

*/ public class ProfileRequest { - - private final String playerName; private Deque handlers; + private final String playerName; 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. + * @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) + 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.

+ *

+ * Handler is always invoked from the main thread. + *

* - * @param handler The result handler. + * @param handler + * The result handler. */ public void addHandler(ProfileFetchHandler handler) { Preconditions.checkNotNull(handler); @@ -86,20 +67,47 @@ public class ProfileRequest { handlers.addLast(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; + } + /** * Invoked to set the profile result. * - *

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

+ *

+ * 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. + * @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; diff --git a/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java b/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java index d6682550c..18346172c 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java +++ b/src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java @@ -9,50 +9,34 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + 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.

+ *

+ * 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); + 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. + * @param player + * The player. */ public void cancelPackets(Player player) { Preconditions.checkNotNull(player); @@ -67,11 +51,12 @@ public class PlayerListRemover { } /** - * Cancel packets pending to be sent to the specified player - * for the specified skinnable entity. + * Cancel packets pending to be sent to the specified player for the specified skinnable entity. * - * @param player The player. - * @param skinnable The skinnable entity. + * @param player + * The player. + * @param skinnable + * The skinnable entity. */ public void cancelPackets(Player player, SkinnableEntity skinnable) { Preconditions.checkNotNull(player); @@ -100,6 +85,23 @@ public class PlayerListRemover { return entry; } + /** + * 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); + } + private class PlayerEntry { Player player; Set toRemove = new HashSet(25); @@ -110,7 +112,6 @@ public class PlayerListRemover { } private class Sender implements Runnable { - @Override public void run() { @@ -127,7 +128,7 @@ public class PlayerListRemover { List skinnableList = new ArrayList(listSize); - int i =0; + int i = 0; Iterator skinIterator = entry.toRemove.iterator(); while (skinIterator.hasNext()) { diff --git a/src/main/java/net/citizensnpcs/npc/skin/Skin.java b/src/main/java/net/citizensnpcs/npc/skin/Skin.java index 3435ef3eb..394ec3ffa 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/Skin.java +++ b/src/main/java/net/citizensnpcs/npc/skin/Skin.java @@ -4,6 +4,7 @@ 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; @@ -11,11 +12,11 @@ import com.google.common.collect.Iterables; import com.mojang.authlib.GameProfile; import com.mojang.authlib.properties.Property; -import net.citizensnpcs.Settings; +import net.citizensnpcs.Settings.Setting; 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.ProfileFetchResult; import net.citizensnpcs.npc.profile.ProfileFetcher; import net.citizensnpcs.npc.profile.ProfileRequest; @@ -23,20 +24,176 @@ 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); + private volatile Property skinData; + private volatile UUID skinId; + private final String skinName; + + /** + * 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); + } + } + }); + } + + /** + * 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(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA), + npc.data(). get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA)); + + 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", Setting.NPC_SKIN_UPDATE.asBoolean())) { + // cache preferred + return true; + } + + if (!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()); + } + } + + /** + * 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; + } + + /** + * Get the name of the skin. + */ + public String getSkinName() { + return skinName; + } + + /** + * Determine if the skin data has been retrieved. + */ + public boolean hasSkinData() { + return skinData != null; + } + + /** + * Determine if the skin is valid. + */ + public boolean isValid() { + return isValid; + } + + 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); + } + } /** * 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.

+ *

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

* - * @param entity The skinnable entity. + * @param entity + * The skinnable entity. */ public static Skin get(SkinnableEntity entity) { Preconditions.checkNotNull(entity); @@ -55,187 +212,26 @@ public class Skin { 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) { - + 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()); + if (skinProperty.getValue() != null) { + npc.data().setPersistent(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA, skinProperty.getValue()); + npc.data().setPersistent(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA, skinProperty.getSignature()); - GameProfile profile = entity.getProfile(); - profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties. - profile.getProperties().put("textures", skinProperty); + GameProfile profile = entity.getProfile(); + profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties. + profile.getProperties().put("textures", skinProperty); + } else { + npc.data().remove(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA); + npc.data().remove(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA); + } } - public static final String PLAYER_SKIN_TEXTURE_PROPERTIES = "player-skin-textures"; - public static final String PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN = "player-skin-signature"; + private static final Map CACHE = new HashMap(20); 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 index d9fb3a960..45b0426f2 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java +++ b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java @@ -5,12 +5,6 @@ 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; @@ -19,25 +13,31 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.scheduler.BukkitTask; +import com.google.common.base.Preconditions; + +import net.citizensnpcs.Settings; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.util.NMS; + /** - * Handles and synchronizes add and remove packets for Player type NPC's - * in order to properly apply the NPC skin. + * 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.

+ *

+ * Used as one instance per NPC entity. + *

*/ public class SkinPacketTracker { - private final SkinnableEntity entity; - private final Map inProgress = - new HashMap(Bukkit.getMaxPlayers() / 2); + private final Map inProgress = new HashMap(Bukkit.getMaxPlayers() / 2); - private Skin skin; private boolean isRemoved; + private Skin skin; /** * Constructor. * - * @param entity The skinnable entity the instance belongs to. + * @param entity + * The skinnable entity the instance belongs to. */ public SkinPacketTracker(SkinnableEntity entity) { Preconditions.checkNotNull(entity); @@ -59,68 +59,53 @@ public class SkinPacketTracker { } /** - * Send skin related packets to a player. + * Notify the tracker that a remove packet has been sent to the specified player. * - * @param player The player. + * @param playerId + * The ID of 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); + void notifyRemovePacketCancelled(UUID playerId) { + inProgress.remove(playerId); } /** - * Send skin related packets to all nearby players within the specified block radius. + * Notify the tracker that a remove packet has been sent to the specified player. * - * @param radius The radius. + * @param playerId + * The ID of the player. */ - public void updateNearbyViewers(double radius) { + void notifyRemovePacketSent(UUID playerId) { + PlayerEntry entry = inProgress.get(playerId); + if (entry == null) + return; - radius *= radius; + if (entry.removeCount == 0) + return; - 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); + entry.removeCount -= 1; + if (entry.removeCount == 0) { + inProgress.remove(playerId); + } else { + scheduleRemovePacket(entry); } } + /** + * Notify that the NPC skin has been changed. + */ + public void notifySkinChange() { + this.skin = Skin.get(entity); + skin.applyAndRespawn(entity); + } + /** * Invoke when the NPC entity is removed. * - *

Sends remove packets to all players.

+ *

+ * Sends remove packets to all players. + *

*/ public void onRemoveNPC() { - isRemoved = true; Collection players = Bukkit.getOnlinePlayers(); @@ -136,50 +121,21 @@ 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) + private void scheduleRemovePacket(final PlayerEntry entry) { + if (isRemoved) 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); + 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 void scheduleRemovePacket(PlayerEntry entry, int count) { - if (!shouldRemoveFromPlayerList()) return; @@ -187,26 +143,7 @@ public class SkinPacketTracker { 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()); @@ -214,12 +151,67 @@ public class SkinPacketTracker { return isNpcRemoved && isTablistDisabled; } + /** + * 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); + } + } + + /** + * 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); + } + private class PlayerEntry { Player player; int removeCount; BukkitTask removeTask; - PlayerEntry (Player player) { + PlayerEntry(Player player) { this.player = player; } @@ -233,10 +225,8 @@ public class SkinPacketTracker { } 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()); @@ -244,7 +234,7 @@ public class SkinPacketTracker { } private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0); + private static PlayerListener LISTENER; private static final int PACKET_DELAY_REMOVE = 1; private static final PlayerListRemover PLAYER_LIST_REMOVER = new PlayerListRemover(); - private static PlayerListener LISTENER; }