mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2024-10-24 05:20:55 +02:00
Fix NPE
This commit is contained in:
parent
7ff746a5b6
commit
ec3b184f72
@ -196,7 +196,6 @@ public class CitizensNPC extends AbstractNPC {
|
|||||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
if (getEntity() == null || !getEntity().isValid())
|
if (getEntity() == null || !getEntity().isValid())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -4,11 +4,11 @@ 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 ProfileFetchHandler {
|
public interface ProfileFetchHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when a result for a profile is ready.
|
* 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);
|
void onResult(ProfileRequest request);
|
||||||
}
|
}
|
||||||
|
@ -6,37 +6,40 @@ import java.util.Deque;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
import net.citizensnpcs.api.CitizensAPI;
|
import net.citizensnpcs.api.CitizensAPI;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thread used to fetch profiles from the Mojang servers.
|
* 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
|
* <p>
|
||||||
* during a single server session.</p>
|
* Maintains a cache of profiles so that no profile is ever requested more than once during a single server session.
|
||||||
|
* </p>
|
||||||
*
|
*
|
||||||
* @see ProfileFetcher
|
* @see ProfileFetcher
|
||||||
*/
|
*/
|
||||||
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 ArrayDeque<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(); // sync for queue & requested fields
|
private final Object sync = new Object(); // sync for queue & requested fields
|
||||||
|
|
||||||
ProfileFetchThread() {}
|
ProfileFetchThread() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a profile.
|
* Fetch a profile.
|
||||||
*
|
*
|
||||||
* @param name The name of the player the profile belongs to.
|
* @param name
|
||||||
* @param handler Optional handler to handle result fetch result.
|
* The name of the player the profile belongs to.
|
||||||
* Handler always invoked from the main thread.
|
* @param handler
|
||||||
|
* Optional handler to handle result fetch result. Handler always invoked from the main thread.
|
||||||
*
|
*
|
||||||
* @see ProfileFetcher#fetch
|
* @see ProfileFetcher#fetch
|
||||||
*/
|
*/
|
||||||
@ -60,8 +63,7 @@ class ProfileFetchThread implements Runnable {
|
|||||||
|
|
||||||
if (request.getResult() == ProfileFetchResult.PENDING) {
|
if (request.getResult() == ProfileFetchResult.PENDING) {
|
||||||
addHandler(request, handler);
|
addHandler(request, handler);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
sendResult(handler, request);
|
sendResult(handler, request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,47 +71,34 @@ class ProfileFetchThread implements Runnable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
List<ProfileRequest> requests;
|
List<ProfileRequest> requests;
|
||||||
|
|
||||||
synchronized (sync) {
|
synchronized (sync) {
|
||||||
|
|
||||||
if (queue.isEmpty())
|
if (queue.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
requests = new ArrayList<ProfileRequest>(queue);
|
requests = new ArrayList<ProfileRequest>(queue);
|
||||||
|
|
||||||
queue.clear();
|
queue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
profileFetcher.fetchRequests(requests);
|
profileFetcher.fetchRequests(requests);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sendResult(final ProfileFetchHandler handler,
|
private static void addHandler(final ProfileRequest request, final ProfileFetchHandler handler) {
|
||||||
final ProfileRequest request) {
|
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||||
|
@Override
|
||||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
|
public void run() {
|
||||||
new Runnable() {
|
request.addHandler(handler);
|
||||||
|
}
|
||||||
@Override
|
}, 1);
|
||||||
public void run() {
|
|
||||||
|
|
||||||
handler.onResult(request);
|
|
||||||
}
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addHandler(final ProfileRequest request,
|
private static void sendResult(final ProfileFetchHandler handler, final ProfileRequest request) {
|
||||||
final ProfileFetchHandler handler) {
|
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||||
|
@Override
|
||||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
|
public void run() {
|
||||||
new Runnable() {
|
handler.onResult(request);
|
||||||
|
}
|
||||||
@Override
|
}, 1);
|
||||||
public void run() {
|
|
||||||
|
|
||||||
request.addHandler(handler);
|
|
||||||
}
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package net.citizensnpcs.npc.profile;
|
package net.citizensnpcs.npc.profile;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
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;
|
||||||
@ -13,39 +16,20 @@ 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 org.bukkit.Bukkit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches game profiles that include skin data from Mojang servers.
|
* Fetches game profiles that include skin data from Mojang servers.
|
||||||
*
|
*
|
||||||
* @see ProfileFetchThread
|
* @see ProfileFetchThread
|
||||||
*/
|
*/
|
||||||
public class ProfileFetcher {
|
public class ProfileFetcher {
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
void fetchRequests(final Collection<ProfileRequest> requests) {
|
void fetchRequests(final Collection<ProfileRequest> requests) {
|
||||||
Preconditions.checkNotNull(requests);
|
Preconditions.checkNotNull(requests);
|
||||||
@ -54,70 +38,85 @@ public class ProfileFetcher {
|
|||||||
|
|
||||||
String[] playerNames = new String[requests.size()];
|
String[] playerNames = new String[requests.size()];
|
||||||
|
|
||||||
int i=0;
|
int i = 0;
|
||||||
for (ProfileRequest request : requests) {
|
for (ProfileRequest request : requests) {
|
||||||
playerNames[i] = request.getPlayerName();
|
playerNames[i] = request.getPlayerName();
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.findProfilesByNames(playerNames, Agent.MINECRAFT,
|
repo.findProfilesByNames(playerNames, Agent.MINECRAFT, new ProfileLookupCallback() {
|
||||||
new ProfileLookupCallback() {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
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 player '" +
|
Messaging.debug(
|
||||||
profile.getName() + "' failed: " + getExceptionMsg(e));
|
"Profile lookup for player '" + profile.getName() + "' failed: " + getExceptionMsg(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileRequest request = findRequest(profile.getName(), requests);
|
ProfileRequest request = findRequest(profile.getName(), requests);
|
||||||
if (request == null)
|
if (request == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (isProfileNotFound(e)) {
|
if (isProfileNotFound(e)) {
|
||||||
request.setResult(null, ProfileFetchResult.NOT_FOUND);
|
request.setResult(null, ProfileFetchResult.NOT_FOUND);
|
||||||
} else if (isTooManyRequests(e)) {
|
} else if (isTooManyRequests(e)) {
|
||||||
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
||||||
} else {
|
} else {
|
||||||
request.setResult(null, ProfileFetchResult.FAILED);
|
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
|
if (isTooManyRequests(e)) {
|
||||||
public void onProfileLookupSucceeded(final GameProfile profile) {
|
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
||||||
|
} else {
|
||||||
if (Messaging.isDebugging()) {
|
request.setResult(null, ProfileFetchResult.FAILED);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
@Nullable
|
||||||
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
|
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
|
||||||
|
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
|
|
||||||
for (ProfileRequest request : requests) {
|
for (ProfileRequest request : requests) {
|
||||||
@ -127,6 +126,12 @@ public class ProfileFetcher {
|
|||||||
return null;
|
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) {
|
private static boolean isProfileNotFound(Exception e) {
|
||||||
String message = e.getMessage();
|
String message = e.getMessage();
|
||||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||||
@ -135,12 +140,6 @@ public class ProfileFetcher {
|
|||||||
|| (cause != null && cause.contains("did not find"));
|
|| (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) {
|
private static boolean isTooManyRequests(Exception e) {
|
||||||
|
|
||||||
String message = e.getMessage();
|
String message = e.getMessage();
|
||||||
|
@ -2,75 +2,56 @@ package net.citizensnpcs.npc.profile;
|
|||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
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 net.citizensnpcs.api.CitizensAPI;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores basic information about a single profile used to request
|
* Stores basic information about a single profile used to request profiles from the Mojang servers.
|
||||||
* 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 {
|
public class ProfileRequest {
|
||||||
|
|
||||||
private final String playerName;
|
|
||||||
private Deque<ProfileFetchHandler> handlers;
|
private Deque<ProfileFetchHandler> handlers;
|
||||||
|
private final String playerName;
|
||||||
private GameProfile profile;
|
private GameProfile profile;
|
||||||
private volatile 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
|
||||||
* @param handler Optional handler to handle the result for the profile.
|
* The name of the player whose profile is being requested.
|
||||||
* Handler always invoked from the main thread.
|
* @param handler
|
||||||
|
* Optional handler to handle the result for the profile. Handler always invoked from the main thread.
|
||||||
*/
|
*/
|
||||||
ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) {
|
ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) {
|
||||||
Preconditions.checkNotNull(playerName);
|
Preconditions.checkNotNull(playerName);
|
||||||
|
|
||||||
this.playerName = playerName;
|
this.playerName = playerName;
|
||||||
|
|
||||||
if (handler != null)
|
if (handler != null) {
|
||||||
addHandler(handler);
|
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.
|
* 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) {
|
public void addHandler(ProfileFetchHandler handler) {
|
||||||
Preconditions.checkNotNull(handler);
|
Preconditions.checkNotNull(handler);
|
||||||
@ -86,20 +67,47 @@ public class ProfileRequest {
|
|||||||
handlers.addLast(handler);
|
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.
|
* 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 profile
|
||||||
* @param result The result of the request.
|
* The profile. Null if there was an error.
|
||||||
|
* @param result
|
||||||
|
* The result of the request.
|
||||||
*/
|
*/
|
||||||
void setResult(final @Nullable GameProfile profile, final ProfileFetchResult result) {
|
void setResult(final @Nullable GameProfile profile, final ProfileFetchResult result) {
|
||||||
|
|
||||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
ProfileRequest.this.profile = profile;
|
ProfileRequest.this.profile = profile;
|
||||||
ProfileRequest.this.result = result;
|
ProfileRequest.this.result = result;
|
||||||
|
|
||||||
|
@ -9,50 +9,34 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
import net.citizensnpcs.Settings;
|
import net.citizensnpcs.Settings;
|
||||||
import net.citizensnpcs.api.CitizensAPI;
|
import net.citizensnpcs.api.CitizensAPI;
|
||||||
import net.citizensnpcs.util.NMS;
|
import net.citizensnpcs.util.NMS;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends remove packets in batch per player.
|
* Sends remove packets in batch per player.
|
||||||
*
|
*
|
||||||
* <p>Collects entities to remove and sends them all to the
|
* <p>
|
||||||
* player in a single packet.</p>
|
* Collects entities to remove and sends them all to the player in a single packet.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
public class PlayerListRemover {
|
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() {
|
PlayerListRemover() {
|
||||||
Bukkit.getScheduler().runTaskTimer(CitizensAPI.getPlugin(), new Sender(), 2, 2);
|
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.
|
* Cancel packets pending to be sent to the specified player.
|
||||||
*
|
*
|
||||||
* @param player The player.
|
* @param player
|
||||||
|
* The player.
|
||||||
*/
|
*/
|
||||||
public void cancelPackets(Player player) {
|
public void cancelPackets(Player player) {
|
||||||
Preconditions.checkNotNull(player);
|
Preconditions.checkNotNull(player);
|
||||||
@ -67,11 +51,12 @@ 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 entity.
|
||||||
* for the specified skinnable entity.
|
|
||||||
*
|
*
|
||||||
* @param player The player.
|
* @param player
|
||||||
* @param skinnable The skinnable entity.
|
* The player.
|
||||||
|
* @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);
|
||||||
@ -100,6 +85,23 @@ public class PlayerListRemover {
|
|||||||
return entry;
|
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 {
|
private class PlayerEntry {
|
||||||
Player player;
|
Player player;
|
||||||
Set<SkinnableEntity> toRemove = new HashSet<SkinnableEntity>(25);
|
Set<SkinnableEntity> toRemove = new HashSet<SkinnableEntity>(25);
|
||||||
@ -110,7 +112,6 @@ public class PlayerListRemover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class Sender implements Runnable {
|
private class Sender implements Runnable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
@ -127,7 +128,7 @@ public class PlayerListRemover {
|
|||||||
|
|
||||||
List<SkinnableEntity> skinnableList = new ArrayList<SkinnableEntity>(listSize);
|
List<SkinnableEntity> skinnableList = new ArrayList<SkinnableEntity>(listSize);
|
||||||
|
|
||||||
int i =0;
|
int i = 0;
|
||||||
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
|
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
|
||||||
while (skinIterator.hasNext()) {
|
while (skinIterator.hasNext()) {
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
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.GameProfile;
|
||||||
import com.mojang.authlib.properties.Property;
|
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.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.ProfileFetchHandler;
|
import net.citizensnpcs.npc.profile.ProfileFetchHandler;
|
||||||
|
import net.citizensnpcs.npc.profile.ProfileFetchResult;
|
||||||
import net.citizensnpcs.npc.profile.ProfileFetcher;
|
import net.citizensnpcs.npc.profile.ProfileFetcher;
|
||||||
import net.citizensnpcs.npc.profile.ProfileRequest;
|
import net.citizensnpcs.npc.profile.ProfileRequest;
|
||||||
|
|
||||||
@ -23,20 +24,176 @@ import net.citizensnpcs.npc.profile.ProfileRequest;
|
|||||||
* Stores data for a single skin.
|
* Stores data for a single skin.
|
||||||
*/
|
*/
|
||||||
public class Skin {
|
public class Skin {
|
||||||
|
|
||||||
private final String skinName;
|
|
||||||
private volatile Property skinData;
|
|
||||||
private volatile UUID skinId;
|
|
||||||
private volatile boolean isValid = true;
|
private volatile boolean isValid = true;
|
||||||
private final Map<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(15);
|
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.
|
* Get a skin for a skinnable entity.
|
||||||
*
|
*
|
||||||
* <p>If a Skin instance does not exist, a new one is created and the
|
* <p>
|
||||||
* skin data is automatically fetched.</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) {
|
public static Skin get(SkinnableEntity entity) {
|
||||||
Preconditions.checkNotNull(entity);
|
Preconditions.checkNotNull(entity);
|
||||||
@ -55,187 +212,26 @@ public class Skin {
|
|||||||
return skin;
|
return skin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static void setNPCSkinData(SkinnableEntity entity, String skinName, UUID skinId, Property skinProperty) {
|
||||||
* 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) {
|
|
||||||
|
|
||||||
NPC npc = entity.getNPC();
|
NPC npc = entity.getNPC();
|
||||||
|
|
||||||
// cache skins for faster initial skin availability
|
// cache skins for faster initial skin availability
|
||||||
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName);
|
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName);
|
||||||
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString());
|
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString());
|
||||||
npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES, skinProperty.getValue());
|
if (skinProperty.getValue() != null) {
|
||||||
npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN, skinProperty.getSignature());
|
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();
|
GameProfile profile = entity.getProfile();
|
||||||
profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties.
|
profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties.
|
||||||
profile.getProperties().put("textures", skinProperty);
|
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";
|
private static final Map<String, Skin> CACHE = new HashMap<String, Skin>(20);
|
||||||
public static final String PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN = "player-skin-signature";
|
|
||||||
public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
|
public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
|
||||||
public static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
|
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);
|
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,6 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
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.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@ -19,25 +13,31 @@ import org.bukkit.event.Listener;
|
|||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
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
|
* Handles and synchronizes add and remove packets for Player type NPC's in order to properly apply the NPC skin.
|
||||||
* 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 {
|
public class SkinPacketTracker {
|
||||||
|
|
||||||
private final SkinnableEntity entity;
|
private final SkinnableEntity entity;
|
||||||
private final Map<UUID, PlayerEntry> inProgress =
|
private final Map<UUID, PlayerEntry> inProgress = new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
|
||||||
new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
|
|
||||||
|
|
||||||
private Skin skin;
|
|
||||||
private boolean isRemoved;
|
private boolean isRemoved;
|
||||||
|
private Skin skin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
*
|
*
|
||||||
* @param entity The skinnable 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);
|
||||||
@ -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) {
|
void notifyRemovePacketCancelled(UUID playerId) {
|
||||||
Preconditions.checkNotNull(player);
|
inProgress.remove(playerId);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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();
|
entry.removeCount -= 1;
|
||||||
Player from = entity.getBukkitEntity();
|
if (entry.removeCount == 0) {
|
||||||
Location location = from.getLocation();
|
inProgress.remove(playerId);
|
||||||
|
} else {
|
||||||
for (Player player : Bukkit.getServer().getOnlinePlayers()) {
|
scheduleRemovePacket(entry);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* 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() {
|
public void onRemoveNPC() {
|
||||||
|
|
||||||
isRemoved = true;
|
isRemoved = true;
|
||||||
|
|
||||||
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
|
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
|
||||||
@ -136,50 +121,21 @@ public class SkinPacketTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void scheduleRemovePacket(final PlayerEntry entry) {
|
||||||
* Notify that the NPC skin has been changed.
|
if (isRemoved)
|
||||||
*/
|
|
||||||
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;
|
return;
|
||||||
|
|
||||||
if (entry.removeCount == 0)
|
entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() {
|
||||||
return;
|
@Override
|
||||||
|
public void run() {
|
||||||
entry.removeCount -= 1;
|
if (shouldRemoveFromPlayerList()) {
|
||||||
if (entry.removeCount == 0) {
|
PLAYER_LIST_REMOVER.sendPacket(entry.player, entity);
|
||||||
inProgress.remove(playerId);
|
}
|
||||||
}
|
}
|
||||||
else {
|
}, PACKET_DELAY_REMOVE);
|
||||||
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())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -187,26 +143,7 @@ public class SkinPacketTracker {
|
|||||||
scheduleRemovePacket(entry);
|
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() {
|
private boolean shouldRemoveFromPlayerList() {
|
||||||
|
|
||||||
boolean isTablistDisabled = Settings.Setting.DISABLE_TABLIST.asBoolean();
|
boolean isTablistDisabled = Settings.Setting.DISABLE_TABLIST.asBoolean();
|
||||||
boolean isNpcRemoved = entity.getNPC().data().get("removefromplayerlist",
|
boolean isNpcRemoved = entity.getNPC().data().get("removefromplayerlist",
|
||||||
Settings.Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean());
|
Settings.Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean());
|
||||||
@ -214,12 +151,67 @@ public class SkinPacketTracker {
|
|||||||
return isNpcRemoved && isTablistDisabled;
|
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 {
|
private class PlayerEntry {
|
||||||
Player player;
|
Player player;
|
||||||
int removeCount;
|
int removeCount;
|
||||||
BukkitTask removeTask;
|
BukkitTask removeTask;
|
||||||
|
|
||||||
PlayerEntry (Player player) {
|
PlayerEntry(Player player) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,10 +225,8 @@ public class SkinPacketTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class PlayerListener implements Listener {
|
private static class PlayerListener implements Listener {
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
private void onPlayerQuit(PlayerQuitEvent event) {
|
private void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
|
||||||
// this also causes any entries in the "inProgress" field to
|
// this also causes any entries in the "inProgress" field to
|
||||||
// be removed.
|
// be removed.
|
||||||
PLAYER_LIST_REMOVER.cancelPackets(event.getPlayer());
|
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 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 int PACKET_DELAY_REMOVE = 1;
|
||||||
private static final PlayerListRemover PLAYER_LIST_REMOVER = new PlayerListRemover();
|
private static final PlayerListRemover PLAYER_LIST_REMOVER = new PlayerListRemover();
|
||||||
private static PlayerListener LISTENER;
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user