This commit is contained in:
fullwall 2015-08-29 15:39:03 +08:00
parent 7ff746a5b6
commit ec3b184f72
8 changed files with 476 additions and 494 deletions

View File

@ -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;

View File

@ -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);
}

View File

@ -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.
*
* <p>Maintains a cache of profiles so that no profile is ever requested more than once
* during a single server session.</p>
* <p>
* Maintains a cache of profiles so that no profile is ever requested more than once during a single server session.
* </p>
*
* @see ProfileFetcher
*/
class ProfileFetchThread implements Runnable {
private final ProfileFetcher profileFetcher = new ProfileFetcher();
private final Deque<ProfileRequest> queue = new ArrayDeque<ProfileRequest>();
private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(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<ProfileRequest> requests;
synchronized (sync) {
if (queue.isEmpty())
return;
requests = new ArrayList<ProfileRequest>(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);
}
}

View File

@ -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<ProfileRequest> 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<ProfileRequest> 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();

View File

@ -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.
*
* <p>Also stores the result of the request.</p>
* <p>
* Also stores the result of the request.
* </p>
*/
public class ProfileRequest {
private final String playerName;
private Deque<ProfileFetchHandler> 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.
*
* <p>Handler is always invoked from the main thread.</p>
* <p>
* Handler is always invoked from the main thread.
* </p>
*
* @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.
*
* <p>Can be invoked from any thread, always executes on the main thread.</p>
* <p>
* Can be invoked from any thread, always executes on the main thread.
* </p>
*
* @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;

View File

@ -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.
*
* <p>Collects entities to remove and sends them all to the
* player in a single packet.</p>
* <p>
* Collects entities to remove and sends them all to the player in a single packet.
* </p>
*/
public class PlayerListRemover {
private final Map<UUID, PlayerEntry> pending =
new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
private final Map<UUID, PlayerEntry> pending = new HashMap<UUID, PlayerEntry>(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<SkinnableEntity> toRemove = new HashSet<SkinnableEntity>(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<SkinnableEntity> skinnableList = new ArrayList<SkinnableEntity>(listSize);
int i =0;
int i = 0;
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
while (skinIterator.hasNext()) {

View File

@ -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<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(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.
*
* <p>
* 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.
* </p>
*
* @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().<String> get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA),
npc.data().<String> get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA));
skinId = UUID.fromString(npc.data().<String> 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.
*
* <p>If a Skin instance does not exist, a new one is created and the
* skin data is automatically fetched.</p>
* <p>
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
* </p>
*
* @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.
*
* <p>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.</p>
*
* @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().<String>get(PLAYER_SKIN_TEXTURE_PROPERTIES),
npc.data().<String>get(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN));
skinId = UUID.fromString(npc.data().<String>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<String, Skin> CACHE = new HashMap<String, Skin>(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<String, Skin> CACHE = new HashMap<String, Skin>(20);
}

View File

@ -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.
*
* <p>Used as one instance per NPC entity.</p>
* <p>
* Used as one instance per NPC entity.
* </p>
*/
public class SkinPacketTracker {
private final SkinnableEntity entity;
private final Map<UUID, PlayerEntry> inProgress =
new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
private final Map<UUID, PlayerEntry> inProgress = new HashMap<UUID, PlayerEntry>(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.
*
* <p>Sends remove packets to all players.</p>
* <p>
* Sends remove packets to all players.
* </p>
*/
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;
}