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 extends Player> 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;
}