Fix NPC's are not visible sometimes

...Fixed invisible NPC's by allowing new update tasks to cancel current
tasks instead of cancelling the new task in SkinPacketTracker#updateViewer

cancel packets in SkinPacketTracker#updateViewer after getting PlayerEntry to ensure current scheduled tasks are cancelled.

remove LinkedList, replace with ArrayDeque

ensure EventListen#recalculatePlayer does not execute for NPC's

do a little less work in EventListen.SkinUpdateTracker#shouldUpdate

don't add skinnable entity to pending map if using cache skin - Skin#apply

remove redundant fetch per NPC in Skin; Skin can already fetch once for itself

fix and improve thread safety for profile fetcher; prevent external access
to threads Runnable interface; honor subscriber always invoked from main
thread.

rename ProfileFetchSubscriber to ProfileFetchHandler since subscriber implies the handler will be used continuously
This commit is contained in:
JCThePants 2015-08-25 19:33:09 -07:00
parent 8d3ab22212
commit 3223ba53f6
12 changed files with 266 additions and 230 deletions

View File

@ -453,6 +453,9 @@ public class EventListen implements Listener {
public void recalculatePlayer(final Player player, long delay, final boolean isInitial) { public void recalculatePlayer(final Player player, long delay, final boolean isInitial) {
if (player.hasMetadata("NPC"))
return;
if (isInitial) { if (isInitial) {
skinUpdateTrackers.put(player.getUniqueId(), new SkinUpdateTracker(player)); skinUpdateTrackers.put(player.getUniqueId(), new SkinUpdateTracker(player));
} }
@ -492,7 +495,7 @@ public class EventListen implements Listener {
&& player.getLocation(CACHE_LOCATION) && player.getLocation(CACHE_LOCATION)
.distanceSquared(npc.getStoredLocation()) < viewDistance) { .distanceSquared(npc.getStoredLocation()) < viewDistance) {
SkinnableEntity skinnable = NMS.getSkinnableNPC(npcEntity); SkinnableEntity skinnable = NMS.getSkinnable(npcEntity);
results.add(skinnable); results.add(skinnable);
} }
@ -605,17 +608,20 @@ public class EventListen implements Listener {
Location currentLoc = player.getLocation(YAW_LOCATION); Location currentLoc = player.getLocation(YAW_LOCATION);
float currentYaw = currentLoc.getYaw(); float currentYaw = currentLoc.getYaw();
float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat(); if (rotationCount < 2) {
boolean hasRotated = float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat();
Math.abs(NMS.clampYaw(currentYaw - this.initialYaw)) < rotationDegrees;
// update the first 2 times the player rotates. helps load skins around player boolean hasRotated =
// after the player logs/teleports. Math.abs(NMS.clampYaw(currentYaw - this.initialYaw)) < rotationDegrees;
if (hasRotated && rotationCount < 2) {
rotationCount++; // update the first 2 times the player rotates. helps load skins around player
reset(player); // after the player logs/teleports.
return true; if (hasRotated) {
rotationCount++;
reset(player);
return true;
}
} }
// update every time a player moves a certain distance // update every time a player moves a certain distance

View File

@ -1314,7 +1314,7 @@ public class NPCCommands {
Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName); Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName);
if (npc.isSpawned()) { if (npc.isSpawned()) {
SkinnableEntity skinnable = NMS.getSkinnableNPC(npc.getEntity()); SkinnableEntity skinnable = NMS.getSkinnable(npc.getEntity());
if (skinnable != null) { if (skinnable != null) {
skinnable.setSkinName(skinName); skinnable.setSkinName(skinName);
} }

View File

@ -189,7 +189,7 @@ public class CitizensNPC extends AbstractNPC {
boolean couldSpawn = !Util.isLoaded(at) ? false : mcEntity.world.addEntity(mcEntity, SpawnReason.CUSTOM); boolean couldSpawn = !Util.isLoaded(at) ? false : mcEntity.world.addEntity(mcEntity, SpawnReason.CUSTOM);
// send skin packets, if applicable, before other NMS packets are sent // send skin packets, if applicable, before other NMS packets are sent
SkinnableEntity skinnable = NMS.getSkinnableNPC(getEntity()); SkinnableEntity skinnable = NMS.getSkinnable(getEntity());
if (skinnable != null) { if (skinnable != null) {
final double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); final double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
skinnable.getSkinTracker().updateNearbyViewers(viewDistance); skinnable.getSkinTracker().updateNearbyViewers(viewDistance);
@ -200,7 +200,7 @@ public class CitizensNPC extends AbstractNPC {
if (getEntity() == null || !getEntity().isValid()) if (getEntity() == null || !getEntity().isValid())
return; return;
SkinnableEntity npc = NMS.getSkinnableNPC(getEntity()); SkinnableEntity npc = NMS.getSkinnable(getEntity());
if (npc == null) if (npc == null)
return; return;

View File

@ -130,7 +130,7 @@ public class HumanController extends AbstractEntityController {
NMS.removeFromWorld(getBukkitEntity()); NMS.removeFromWorld(getBukkitEntity());
SkinnableEntity npc = NMS.getSkinnableNPC(getBukkitEntity()); SkinnableEntity npc = NMS.getSkinnable(getBukkitEntity());
npc.getSkinTracker().onRemoveNPC(); npc.getSkinTracker().onRemoveNPC();
super.remove(); super.remove();

View File

@ -3,7 +3,7 @@ package net.citizensnpcs.npc.profile;
/** /**
* Interface for a subscriber of the results of a profile fetch. * Interface for a subscriber of the results of a profile fetch.
*/ */
public interface ProfileFetchSubscriber { public interface ProfileFetchHandler {
/** /**
* Invoked when a result for a profile is ready. * Invoked when a result for a profile is ready.

View File

@ -1,9 +1,9 @@
package net.citizensnpcs.npc.profile; package net.citizensnpcs.npc.profile;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -19,63 +19,52 @@ import org.bukkit.Bukkit;
* *
* <p>Maintains a cache of profiles so that no profile is ever requested more than once * <p>Maintains a cache of profiles so that no profile is ever requested more than once
* during a single server session.</p> * during a single server session.</p>
*
* @see ProfileFetcher
*/ */
public class ProfileFetchThread implements Runnable { class ProfileFetchThread implements Runnable {
private final ProfileFetcher profileFetcher = new ProfileFetcher(); private final ProfileFetcher profileFetcher = new ProfileFetcher();
private final Deque<ProfileRequest> queue = new LinkedList<ProfileRequest>(); private final Deque<ProfileRequest> queue = new ArrayDeque<ProfileRequest>();
private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(35); private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(35);
private final Object sync = new Object(); private final Object sync = new Object(); // sync for queue & requested fields
/**
* Get the singleton instance.
*/
public static ProfileFetchThread get() {
if (PROFILE_THREAD == null) {
PROFILE_THREAD = new ProfileFetchThread();
Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD,
11, 20);
}
return PROFILE_THREAD;
}
ProfileFetchThread() {} ProfileFetchThread() {}
/** /**
* Fetch a profile. * Fetch a profile.
* *
* @param name The name of the player the profile belongs to. * @param name The name of the player the profile belongs to.
* @param subscriber Optional subscriber to be notified when a result is available. * @param handler Optional handler to handle result fetch result.
* Subscriber always invoked from the main thread. * Handler always invoked from the main thread.
*
* @see ProfileFetcher#fetch
*/ */
public void fetch(String name, @Nullable ProfileFetchSubscriber subscriber) { void fetch(String name, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(name); Preconditions.checkNotNull(name);
ProfileRequest request = requested.get(name); name = name.toLowerCase();
ProfileRequest request;
if (request != null) {
if (subscriber != null) {
if (request.getResult() == ProfileFetchResult.PENDING) {
request.addSubscriber(subscriber);
}
else {
subscriber.onResult(request);
}
}
return;
}
request = new ProfileRequest(name, subscriber);
synchronized (sync) { synchronized (sync) {
queue.add(request); request = requested.get(name);
if (request == null) {
request = new ProfileRequest(name, handler);
queue.add(request);
requested.put(name, request);
return;
}
} }
requested.put(name, request); if (handler != null) {
if (request.getResult() == ProfileFetchResult.PENDING) {
addHandler(request, handler);
}
else {
sendResult(handler, request);
}
}
} }
@Override @Override
@ -89,11 +78,38 @@ public class ProfileFetchThread implements Runnable {
return; return;
requests = new ArrayList<ProfileRequest>(queue); requests = new ArrayList<ProfileRequest>(queue);
queue.clear(); queue.clear();
} }
profileFetcher.fetch(requests); profileFetcher.fetchRequests(requests);
} }
private static ProfileFetchThread PROFILE_THREAD; private static void sendResult(final ProfileFetchHandler handler,
final ProfileRequest request) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
new Runnable() {
@Override
public void run() {
handler.onResult(request);
}
}, 1);
}
private static void addHandler(final ProfileRequest request,
final ProfileFetchHandler handler) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
new Runnable() {
@Override
public void run() {
request.addHandler(handler);
}
}, 1);
}
} }

View File

@ -1,29 +1,53 @@
package net.citizensnpcs.npc.profile; package net.citizensnpcs.npc.profile;
import java.util.Collection;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.mojang.authlib.Agent; import com.mojang.authlib.Agent;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.GameProfileRepository; import com.mojang.authlib.GameProfileRepository;
import com.mojang.authlib.ProfileLookupCallback; import com.mojang.authlib.ProfileLookupCallback;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.NMS;
import javax.annotation.Nullable; import org.bukkit.Bukkit;
import java.util.Collection;
/** /**
* Fetches game profiles that include skin data from Mojang servers. * Fetches game profiles that include skin data from Mojang servers.
* *
* @see ProfileFetchThread * @see ProfileFetchThread
*/ */
class ProfileFetcher { public class ProfileFetcher {
/**
* Fetch a profile.
*
* @param name The name of the player the profile belongs to.
* @param handler Optional handler to handle the result.
* Handler always invoked from the main thread.
*/
public static void fetch(String name, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(name);
if (PROFILE_THREAD == null) {
PROFILE_THREAD = new ProfileFetchThread();
Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD,
11, 20);
}
PROFILE_THREAD.fetch(name, handler);
}
ProfileFetcher() {}
/** /**
* Fetch one or more profiles. * Fetch one or more profiles.
* *
* @param requests The profile requests. * @param requests The profile requests.
*/ */
public void fetch(final Collection<ProfileRequest> requests) { void fetchRequests(final Collection<ProfileRequest> requests) {
Preconditions.checkNotNull(requests); Preconditions.checkNotNull(requests);
final GameProfileRepository repo = NMS.getGameProfileRepository(); final GameProfileRepository repo = NMS.getGameProfileRepository();
@ -43,7 +67,7 @@ class ProfileFetcher {
public void onProfileLookupFailed(GameProfile profile, Exception e) { public void onProfileLookupFailed(GameProfile profile, Exception e) {
if (Messaging.isDebugging()) { if (Messaging.isDebugging()) {
Messaging.debug("Profile lookup for skin '" + Messaging.debug("Profile lookup for player '" +
profile.getName() + "' failed: " + getExceptionMsg(e)); profile.getName() + "' failed: " + getExceptionMsg(e));
} }
@ -77,7 +101,7 @@ class ProfileFetcher {
} catch (Exception e) { } catch (Exception e) {
if (Messaging.isDebugging()) { if (Messaging.isDebugging()) {
Messaging.debug("Profile lookup for skin '" + Messaging.debug("Profile lookup for player '" +
profile.getName() + "' failed: " + getExceptionMsg(e)); profile.getName() + "' failed: " + getExceptionMsg(e));
} }
@ -125,4 +149,6 @@ class ProfileFetcher {
return (message != null && message.contains("too many requests")) return (message != null && message.contains("too many requests"))
|| (cause != null && cause.contains("too many requests")); || (cause != null && cause.contains("too many requests"));
} }
private static ProfileFetchThread PROFILE_THREAD;
} }

View File

@ -1,13 +1,15 @@
package net.citizensnpcs.npc.profile; package net.citizensnpcs.npc.profile;
import java.util.ArrayDeque;
import java.util.Deque;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import net.citizensnpcs.api.CitizensAPI;
import org.bukkit.Bukkit;
import javax.annotation.Nullable; import net.citizensnpcs.api.CitizensAPI;
import java.util.ArrayDeque;
import java.util.Deque; import org.bukkit.Bukkit;
/** /**
* Stores basic information about a single profile used to request * Stores basic information about a single profile used to request
@ -18,24 +20,24 @@ import java.util.Deque;
public class ProfileRequest { public class ProfileRequest {
private final String playerName; private final String playerName;
private Deque<ProfileFetchSubscriber> subscribers; private Deque<ProfileFetchHandler> handlers;
private GameProfile profile; private GameProfile profile;
private ProfileFetchResult result = ProfileFetchResult.PENDING; private volatile ProfileFetchResult result = ProfileFetchResult.PENDING;
/** /**
* Constructor. * Constructor.
* *
* @param playerName The name of the player whose profile is being requested. * @param playerName The name of the player whose profile is being requested.
* @param subscriber Optional subscriber to be notified when a result is available * @param handler Optional handler to handle the result for the profile.
* for the profile. Subscriber always invoked from the main thread. * Handler always invoked from the main thread.
*/ */
ProfileRequest(String playerName, @Nullable ProfileFetchSubscriber subscriber) { ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(playerName); Preconditions.checkNotNull(playerName);
this.playerName = playerName; this.playerName = playerName;
if (subscriber != null) if (handler != null)
addSubscriber(subscriber); addHandler(handler);
} }
/** /**
@ -64,19 +66,24 @@ public class ProfileRequest {
} }
/** /**
* Add a result subscriber to be notified when a result is available. * Add one time result handler.
* *
* <p>Subscriber is always invoked from the main thread.</p> * <p>Handler is always invoked from the main thread.</p>
* *
* @param subscriber The subscriber. * @param handler The result handler.
*/ */
public void addSubscriber(ProfileFetchSubscriber subscriber) { public void addHandler(ProfileFetchHandler handler) {
Preconditions.checkNotNull(subscriber); Preconditions.checkNotNull(handler);
if (subscribers == null) if (result != ProfileFetchResult.PENDING) {
subscribers = new ArrayDeque<ProfileFetchSubscriber>(); handler.onResult(this);
return;
}
subscribers.addLast(subscriber); if (handlers == null)
handlers = new ArrayDeque<ProfileFetchHandler>();
handlers.addLast(handler);
} }
/** /**
@ -96,14 +103,14 @@ public class ProfileRequest {
ProfileRequest.this.profile = profile; ProfileRequest.this.profile = profile;
ProfileRequest.this.result = result; ProfileRequest.this.result = result;
if (subscribers == null) if (handlers == null)
return; return;
while (!subscribers.isEmpty()) { while (!handlers.isEmpty()) {
subscribers.removeFirst().onResult(ProfileRequest.this); handlers.removeFirst().onResult(ProfileRequest.this);
} }
subscribers = null; handlers = null;
} }
}); });
} }

View File

@ -35,10 +35,10 @@ public class PlayerListRemover {
/** /**
* Send a remove packet to the specified player for the specified * Send a remove packet to the specified player for the specified
* human NPC entity. * skinnable entity.
* *
* @param player The player to send the packet to. * @param player The player to send the packet to.
* @param entity The entity to remove. * @param entity The entity to remove.
*/ */
public void sendPacket(Player player, SkinnableEntity entity) { public void sendPacket(Player player, SkinnableEntity entity) {
Preconditions.checkNotNull(player); Preconditions.checkNotNull(player);
@ -58,6 +58,8 @@ public class PlayerListRemover {
Preconditions.checkNotNull(player); Preconditions.checkNotNull(player);
PlayerEntry entry = pending.remove(player.getUniqueId()); PlayerEntry entry = pending.remove(player.getUniqueId());
if (entry == null)
return;
for (SkinnableEntity entity : entry.toRemove) { for (SkinnableEntity entity : entry.toRemove) {
entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId()); entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
@ -66,10 +68,10 @@ public class PlayerListRemover {
/** /**
* Cancel packets pending to be sent to the specified player * Cancel packets pending to be sent to the specified player
* for the specified skinnable NPC. * for the specified skinnable entity.
* *
* @param player The player. * @param player The player.
* @param skinnable The skinnable NPC. * @param skinnable The skinnable entity.
*/ */
public void cancelPackets(Player player, SkinnableEntity skinnable) { public void cancelPackets(Player player, SkinnableEntity skinnable) {
Preconditions.checkNotNull(player); Preconditions.checkNotNull(player);
@ -129,14 +131,13 @@ public class PlayerListRemover {
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator(); Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
while (skinIterator.hasNext()) { while (skinIterator.hasNext()) {
if (i >= maxPacketEntries)
break;
SkinnableEntity skinnable = skinIterator.next(); SkinnableEntity skinnable = skinIterator.next();
skinnableList.add(skinnable); skinnableList.add(skinnable);
skinIterator.remove(); skinIterator.remove();
if (i > maxPacketEntries)
break;
i++; i++;
} }

View File

@ -15,8 +15,8 @@ import net.citizensnpcs.Settings;
import net.citizensnpcs.api.event.DespawnReason; import net.citizensnpcs.api.event.DespawnReason;
import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.npc.profile.ProfileFetchResult; import net.citizensnpcs.npc.profile.ProfileFetchResult;
import net.citizensnpcs.npc.profile.ProfileFetchSubscriber; import net.citizensnpcs.npc.profile.ProfileFetchHandler;
import net.citizensnpcs.npc.profile.ProfileFetchThread; import net.citizensnpcs.npc.profile.ProfileFetcher;
import net.citizensnpcs.npc.profile.ProfileRequest; import net.citizensnpcs.npc.profile.ProfileRequest;
/** /**
@ -28,15 +28,15 @@ public class Skin {
private volatile Property skinData; private volatile Property skinData;
private volatile UUID skinId; private volatile UUID skinId;
private volatile boolean isValid = true; private volatile boolean isValid = true;
private final Map<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(30); private final Map<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(15);
/** /**
* Get a skin for a human NPC entity. * Get a skin for a skinnable entity.
* *
* <p>If a Skin instance does not exist, a new one is created and the * <p>If a Skin instance does not exist, a new one is created and the
* skin data is automatically fetched.</p> * skin data is automatically fetched.</p>
* *
* @param entity The human NPC entity. * @param entity The skinnable entity.
*/ */
public static Skin get(SkinnableEntity entity) { public static Skin get(SkinnableEntity entity) {
Preconditions.checkNotNull(entity); Preconditions.checkNotNull(entity);
@ -65,13 +65,13 @@ public class Skin {
this.skinName = skinName.toLowerCase(); this.skinName = skinName.toLowerCase();
synchronized (CACHE) { synchronized (CACHE) {
if (CACHE.containsKey(skinName)) if (CACHE.containsKey(this.skinName))
throw new IllegalArgumentException("There is already a skin named " + skinName); throw new IllegalArgumentException("There is already a skin named " + skinName);
CACHE.put(skinName, this); CACHE.put(this.skinName, this);
} }
ProfileFetchThread.get().fetch(skinName, new ProfileFetchSubscriber() { ProfileFetcher.fetch(this.skinName, new ProfileFetchHandler() {
@Override @Override
public void onResult(ProfileRequest request) { public void onResult(ProfileRequest request) {
@ -82,11 +82,8 @@ public class Skin {
} }
if (request.getResult() == ProfileFetchResult.SUCCESS) { if (request.getResult() == ProfileFetchResult.SUCCESS) {
GameProfile profile = request.getProfile(); GameProfile profile = request.getProfile();
setData(profile);
skinId = profile.getId();
skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
} }
} }
}); });
@ -125,47 +122,15 @@ public class Skin {
} }
/** /**
* Set skin data. * Apply the skin data to the specified skinnable entity.
*
* @param profile The profile that contains the skin data. If set to null,
* it's assumed that the skin is not valid.
*
* @throws IllegalStateException if not invoked from the main thread.
* @throws IllegalArgumentException if the profile name does not match the skin data.
*/
public void setData(@Nullable GameProfile profile) {
if (profile == null) {
isValid = false;
return;
}
if (!profile.getName().toLowerCase().equals(skinName)) {
throw new IllegalArgumentException(
"GameProfile name (" + profile.getName() + ") and "
+ "skin name (" + skinName + ") do not match.");
}
skinId = profile.getId();
skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
for (SkinnableEntity entity : pending.keySet()) {
applyAndRespawn(entity);
}
}
/**
* Apply the skin data to the specified human NPC entity.
* *
* <p>If invoked before the skin data is ready, the skin is retrieved * <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> * and the skin is automatically applied to the entity at a later time.</p>
* *
* @param entity The human NPC entity. * @param entity The skinnable entity.
* *
* @return True if the skin data was available and applied, false if * @return True if the skin data was available and applied, false if
* the data is being retrieved. * the data is being retrieved.
*
* @throws IllegalStateException if not invoked from the main thread.
*/ */
public boolean apply(SkinnableEntity entity) { public boolean apply(SkinnableEntity entity) {
Preconditions.checkNotNull(entity); Preconditions.checkNotNull(entity);
@ -173,7 +138,6 @@ public class Skin {
NPC npc = entity.getNPC(); NPC npc = entity.getNPC();
if (!hasSkinData()) { if (!hasSkinData()) {
pending.put(entity, null);
// Use npc cached skin if available. // Use npc cached skin if available.
// If npc requires latest skin, cache is used for faster // If npc requires latest skin, cache is used for faster
@ -202,9 +166,7 @@ public class Skin {
} }
} }
// get latest skin pending.put(entity, null);
fetchSkinFor(entity);
return false; return false;
} }
@ -233,20 +195,25 @@ public class Skin {
} }
} }
private void fetchSkinFor(final SkinnableEntity entity) { private void setData(@Nullable GameProfile profile) {
ProfileFetchThread.get().fetch(skinName, new ProfileFetchSubscriber() { if (profile == null) {
isValid = false;
return;
}
@Override if (!profile.getName().toLowerCase().equals(skinName)) {
public void onResult(ProfileRequest request) { throw new IllegalArgumentException(
"GameProfile name (" + profile.getName() + ") and "
+ "skin name (" + skinName + ") do not match.");
}
if (request.getResult() != ProfileFetchResult.SUCCESS) skinId = profile.getId();
return; skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); for (SkinnableEntity entity : pending.keySet()) {
entity.getSkinTracker().updateNearbyViewers(viewDistance); applyAndRespawn(entity);
} }
});
} }
private static void setNPCSkinData(SkinnableEntity entity, private static void setNPCSkinData(SkinnableEntity entity,

View File

@ -17,6 +17,7 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitTask;
/** /**
* Handles and synchronizes add and remove packets for Player type NPC's * Handles and synchronizes add and remove packets for Player type NPC's
@ -36,7 +37,7 @@ public class SkinPacketTracker {
/** /**
* Constructor. * Constructor.
* *
* @param entity The human NPC entity the instance belongs to. * @param entity The skinnable entity the instance belongs to.
*/ */
public SkinPacketTracker(SkinnableEntity entity) { public SkinPacketTracker(SkinnableEntity entity) {
Preconditions.checkNotNull(entity); Preconditions.checkNotNull(entity);
@ -57,48 +58,6 @@ public class SkinPacketTracker {
return skin; return skin;
} }
/**
* Notify that the NPC skin has been changed.
*/
public void notifySkinChange() {
this.skin = Skin.get(entity);
skin.applyAndRespawn(entity);
}
/**
* Notify the tracker that a remove packet has been sent to the
* specified player.
*
* @param playerId The ID of the player.
*/
void notifyRemovePacketSent(UUID playerId) {
PlayerEntry entry = inProgress.get(playerId);
if (entry == null)
return;
if (entry.removeCount == 0)
return;
entry.removeCount -= 1;
if (entry.removeCount == 0) {
inProgress.remove(playerId);
}
else {
scheduleRemovePacket(entry);
}
}
/**
* Notify the tracker that a remove packet has been sent to the
* specified player.
*
* @param playerId The ID of the player.
*/
void notifyRemovePacketCancelled(UUID playerId) {
inProgress.remove(playerId);
}
/** /**
* Send skin related packets to a player. * Send skin related packets to a player.
* *
@ -107,19 +66,21 @@ public class SkinPacketTracker {
public void updateViewer(final Player player) { public void updateViewer(final Player player) {
Preconditions.checkNotNull(player); Preconditions.checkNotNull(player);
if (player.hasMetadata("NPC")) if (isRemoved || player.hasMetadata("NPC"))
return; return;
if (isRemoved || inProgress.containsKey(player.getUniqueId())) PlayerEntry entry = inProgress.get(player.getUniqueId());
return; if (entry != null) {
entry.cancel();
PlayerEntry entry = new PlayerEntry(player); }
inProgress.put(player.getUniqueId(), entry); else {
entry = new PlayerEntry(player);
}
PLAYER_LIST_REMOVER.cancelPackets(player, entity); PLAYER_LIST_REMOVER.cancelPackets(player, entity);
inProgress.put(player.getUniqueId(), entry);
skin.apply(entity); skin.apply(entity);
NMS.sendPlayerListAdd(player, entity.getBukkitEntity()); NMS.sendPlayerListAdd(player, entity.getBukkitEntity());
scheduleRemovePacket(entry, 2); scheduleRemovePacket(entry, 2);
@ -175,6 +136,48 @@ public class SkinPacketTracker {
} }
} }
/**
* Notify that the NPC skin has been changed.
*/
public void notifySkinChange() {
this.skin = Skin.get(entity);
skin.applyAndRespawn(entity);
}
/**
* Notify the tracker that a remove packet has been sent to the
* specified player.
*
* @param playerId The ID of the player.
*/
void notifyRemovePacketSent(UUID playerId) {
PlayerEntry entry = inProgress.get(playerId);
if (entry == null)
return;
if (entry.removeCount == 0)
return;
entry.removeCount -= 1;
if (entry.removeCount == 0) {
inProgress.remove(playerId);
}
else {
scheduleRemovePacket(entry);
}
}
/**
* Notify the tracker that a remove packet has been sent to the
* specified player.
*
* @param playerId The ID of the player.
*/
void notifyRemovePacketCancelled(UUID playerId) {
inProgress.remove(playerId);
}
private void scheduleRemovePacket(PlayerEntry entry, int count) { private void scheduleRemovePacket(PlayerEntry entry, int count) {
if (!shouldRemoveFromPlayerList()) if (!shouldRemoveFromPlayerList())
@ -189,7 +192,7 @@ public class SkinPacketTracker {
if (isRemoved) if (isRemoved)
return; return;
Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(),
new Runnable() { new Runnable() {
@Override @Override
@ -214,9 +217,19 @@ public class SkinPacketTracker {
private class PlayerEntry { private class PlayerEntry {
Player player; Player player;
int removeCount; int removeCount;
BukkitTask removeTask;
PlayerEntry (Player player) { PlayerEntry (Player player) {
this.player = player; this.player = player;
} }
// cancel previous packet tasks so they do not interfere with
// new tasks
void cancel() {
if (removeTask != null)
removeTask.cancel();
removeCount = 0;
}
} }
private static class PlayerListener implements Listener { private static class PlayerListener implements Listener {

View File

@ -12,8 +12,10 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.GameProfileRepository; import com.mojang.authlib.GameProfileRepository;
import com.mojang.authlib.HttpAuthenticationService; import com.mojang.authlib.HttpAuthenticationService;
import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mojang.authlib.minecraft.MinecraftSessionService;
@ -21,26 +23,6 @@ import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse; import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse;
import com.mojang.util.UUIDTypeAdapter; import com.mojang.util.UUIDTypeAdapter;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
import org.bukkit.craftbukkit.v1_8_R3.CraftSound;
import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.plugin.PluginLoadOrder;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.api.command.exception.CommandException; import net.citizensnpcs.api.command.exception.CommandException;
import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPC;
@ -48,6 +30,7 @@ import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.npc.ai.NPCHolder; import net.citizensnpcs.npc.ai.NPCHolder;
import net.citizensnpcs.npc.entity.EntityHumanNPC; import net.citizensnpcs.npc.entity.EntityHumanNPC;
import net.citizensnpcs.npc.network.EmptyChannel; import net.citizensnpcs.npc.network.EmptyChannel;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.util.nms.PlayerlistTrackerEntry; import net.citizensnpcs.util.nms.PlayerlistTrackerEntry;
import net.minecraft.server.v1_8_R3.AttributeInstance; import net.minecraft.server.v1_8_R3.AttributeInstance;
import net.minecraft.server.v1_8_R3.Block; import net.minecraft.server.v1_8_R3.Block;
@ -75,7 +58,23 @@ import net.minecraft.server.v1_8_R3.PathfinderGoalSelector;
import net.minecraft.server.v1_8_R3.World; import net.minecraft.server.v1_8_R3.World;
import net.minecraft.server.v1_8_R3.WorldServer; import net.minecraft.server.v1_8_R3.WorldServer;
import javax.annotation.Nullable; import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
import org.bukkit.craftbukkit.v1_8_R3.CraftSound;
import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.plugin.PluginLoadOrder;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class NMS { public class NMS {
@ -108,7 +107,7 @@ public class NMS {
} }
@Nullable @Nullable
public static SkinnableEntity getSkinnableNPC(org.bukkit.entity.Entity entity) { public static SkinnableEntity getSkinnable(org.bukkit.entity.Entity entity) {
Preconditions.checkNotNull(entity); Preconditions.checkNotNull(entity);
Entity nmsEntity = ((CraftEntity) entity).getHandle(); Entity nmsEntity = ((CraftEntity) entity).getHandle();
@ -138,7 +137,8 @@ public class NMS {
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entity)); PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entity));
} }
public static void sendPlayerListRemove(Player recipient, Collection<? extends SkinnableEntity> skinnableNPCs) { public static void sendPlayerListRemove(Player recipient,
Collection<? extends SkinnableEntity> skinnableNPCs) {
Preconditions.checkNotNull(recipient); Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(skinnableNPCs); Preconditions.checkNotNull(skinnableNPCs);