mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2025-01-03 06:57:41 +01:00
Add skin fetching for bedrock players using skin prefix or -b flag in /npc skin. Hide the floodgate prefix for /npc mirror. Add NPCShopPurchaseEvent
This commit is contained in:
parent
7a9a4db787
commit
2c0c7bf8a0
@ -73,8 +73,8 @@ import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.npc.CitizensNPCRegistry;
|
||||
import net.citizensnpcs.npc.CitizensTraitFactory;
|
||||
import net.citizensnpcs.npc.NPCSelector;
|
||||
import net.citizensnpcs.npc.profile.ProfileFetcher;
|
||||
import net.citizensnpcs.npc.skin.Skin;
|
||||
import net.citizensnpcs.npc.skin.profile.ProfileFetcher;
|
||||
import net.citizensnpcs.trait.ShopTrait;
|
||||
import net.citizensnpcs.trait.shop.StoredShops;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
@ -48,6 +48,7 @@ import net.citizensnpcs.trait.RotationTrait;
|
||||
import net.citizensnpcs.trait.RotationTrait.PacketRotationSession;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.citizensnpcs.util.SkinProperty;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
public class ProtocolLibListener implements Listener {
|
||||
private ProtocolManager manager;
|
||||
@ -168,7 +169,8 @@ public class ProtocolLibListener implements Listener {
|
||||
if (playerProfile == null) {
|
||||
playerProfile = NMS.getProfile(event.getPlayer());
|
||||
wgp = WrappedGameProfile.fromPlayer(event.getPlayer());
|
||||
playerName = WrappedChatComponent.fromText(event.getPlayer().getDisplayName());
|
||||
playerName = WrappedChatComponent.fromText(
|
||||
Util.possiblyStripBedrockPrefix(event.getPlayer().getDisplayName(), wgp.getUUID()));
|
||||
}
|
||||
if (trait.mirrorName()) {
|
||||
list.set(i, new PlayerInfoData(wgp.withId(npcInfo.getProfile().getId()), npcInfo.getLatency(),
|
||||
|
@ -2937,12 +2937,12 @@ public class NPCCommands {
|
||||
|
||||
@Command(
|
||||
aliases = { "npc" },
|
||||
usage = "skin (-e(xport) -c(lear) -l(atest) -s(kull)) [name] (or --url [url] --file [file] (-s(lim)) or -t [uuid/name] [data] [signature])",
|
||||
usage = "skin (-e(xport) -c(lear) -l(atest) -s(kull) -b(edrock)) [name] (or --url [url] --file [file] (-s(lim)) or -t [uuid/name] [data] [signature])",
|
||||
desc = "",
|
||||
modifiers = { "skin" },
|
||||
min = 1,
|
||||
max = 4,
|
||||
flags = "ectls",
|
||||
flags = "bectls",
|
||||
permission = "citizens.npc.skin")
|
||||
@Requirements(types = EntityType.PLAYER, selected = true, ownership = true)
|
||||
public void skin(CommandContext args, CommandSender sender, NPC npc, @Flag("url") String url,
|
||||
@ -3053,6 +3053,9 @@ public class NPCCommands {
|
||||
}
|
||||
skinName = args.getString(1);
|
||||
}
|
||||
if (args.hasFlag('b')) {
|
||||
skinName = Util.possiblyConvertToBedrockName(skinName);
|
||||
}
|
||||
Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName);
|
||||
trait.setSkinName(skinName, true);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.citizensnpcs.npc.skin;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@ -22,7 +23,7 @@ import net.citizensnpcs.api.event.DespawnReason;
|
||||
import net.citizensnpcs.api.event.SpawnReason;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.npc.profile.ProfileFetcher;
|
||||
import net.citizensnpcs.npc.skin.profile.ProfileFetcher;
|
||||
import net.citizensnpcs.trait.SkinTrait;
|
||||
import net.citizensnpcs.util.SkinProperty;
|
||||
|
||||
@ -48,7 +49,7 @@ public class Skin {
|
||||
* @param forceUpdate
|
||||
*/
|
||||
Skin(String skinName) {
|
||||
this.skinName = skinName.toLowerCase();
|
||||
this.skinName = skinName.toLowerCase(Locale.ROOT);
|
||||
|
||||
synchronized (CACHE) {
|
||||
if (CACHE.containsKey(this.skinName))
|
||||
@ -88,26 +89,24 @@ public class Skin {
|
||||
if (entity.getNPC().data().has("player-skin-use-latest")) {
|
||||
entity.getNPC().data().remove("player-skin-use-latest");
|
||||
}
|
||||
if (!skinTrait.shouldUpdateSkins())
|
||||
// cache preferred
|
||||
if (!skinTrait.shouldUpdateSkins()) // cache preferred
|
||||
return true;
|
||||
}
|
||||
if (!hasSkinData()) {
|
||||
String defaultSkinName = ChatColor.stripColor(npc.getName()).toLowerCase();
|
||||
String defaultSkinName = ChatColor.stripColor(npc.getName()).toLowerCase(Locale.ROOT);
|
||||
|
||||
if (npc.hasTrait(SkinTrait.class) && skinName.equals(defaultSkinName)
|
||||
&& !npc.getOrAddTrait(SkinTrait.class).fetchDefaultSkin())
|
||||
return false;
|
||||
|
||||
if (hasFetched) {
|
||||
if (hasFetched)
|
||||
return true;
|
||||
} else {
|
||||
if (!fetching) {
|
||||
fetch();
|
||||
}
|
||||
pending.put(entity, null);
|
||||
return false;
|
||||
|
||||
if (!fetching) {
|
||||
fetch();
|
||||
}
|
||||
pending.put(entity, null);
|
||||
return false;
|
||||
}
|
||||
setNPCSkinData(entity, skinName, skinId, skinData);
|
||||
|
||||
@ -151,7 +150,7 @@ public class Skin {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (skinName.toLowerCase().startsWith("cit-"))
|
||||
if (skinName.toLowerCase(Locale.ROOT).startsWith("cit-"))
|
||||
return;
|
||||
|
||||
fetching = true;
|
||||
@ -164,9 +163,9 @@ public class Skin {
|
||||
isValid = false;
|
||||
break;
|
||||
case TOO_MANY_REQUESTS:
|
||||
if (maxRetries == 0) {
|
||||
if (maxRetries == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
fetchRetries++;
|
||||
long delay = Setting.NPC_SKIN_RETRY_DELAY.asTicks();
|
||||
retryTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), (Runnable) this::fetch,
|
||||
@ -194,7 +193,7 @@ public class Skin {
|
||||
Messaging.idebug(() -> "Skin name invalid length '" + skinName + "'");
|
||||
return;
|
||||
}
|
||||
if (skinName.toLowerCase().startsWith("cit-"))
|
||||
if (skinName.toLowerCase(Locale.ROOT).startsWith("cit-"))
|
||||
return;
|
||||
|
||||
fetching = true;
|
||||
@ -263,7 +262,7 @@ public class Skin {
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
if (!profile.getName().toLowerCase().equals(skinName)) {
|
||||
if (!profile.getName().toLowerCase(Locale.ROOT).equals(skinName)) {
|
||||
Messaging.debug("GameProfile name (" + profile.getName() + ") and " + "skin name (" + skinName
|
||||
+ ") do not match. Has the user renamed recently?");
|
||||
}
|
||||
@ -321,8 +320,7 @@ public class Skin {
|
||||
public static Skin get(SkinnableEntity entity, boolean forceUpdate) {
|
||||
Objects.requireNonNull(entity);
|
||||
|
||||
String skinName = entity.getSkinName().toLowerCase();
|
||||
return get(skinName, forceUpdate);
|
||||
return get(entity.getSkinName(), forceUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -338,7 +336,7 @@ public class Skin {
|
||||
public static Skin get(String skinName, boolean forceUpdate) {
|
||||
Objects.requireNonNull(skinName);
|
||||
|
||||
skinName = skinName.toLowerCase();
|
||||
skinName = skinName.toLowerCase(Locale.ROOT);
|
||||
|
||||
Skin skin;
|
||||
synchronized (CACHE) {
|
||||
@ -382,7 +380,7 @@ public class Skin {
|
||||
skinProperty.apply(profile);
|
||||
}
|
||||
|
||||
private static Map<String, Skin> CACHE = new HashMap<>(20);
|
||||
public static String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
|
||||
public static String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
|
||||
private static final Map<String, Skin> CACHE = new HashMap<>(20);
|
||||
public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
|
||||
public static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
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.
|
||||
*/
|
||||
void onResult(ProfileRequest request);
|
||||
}
|
||||
package net.citizensnpcs.npc.skin.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.
|
||||
*/
|
||||
void onResult(ProfileRequest request);
|
||||
}
|
@ -1,27 +1,27 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
/**
|
||||
* The result status of a profile fetch.
|
||||
*/
|
||||
public enum ProfileFetchResult {
|
||||
/**
|
||||
* The profile request failed for unknown reasons.
|
||||
*/
|
||||
FAILED,
|
||||
/**
|
||||
* The profile request failed because the profile was not found.
|
||||
*/
|
||||
NOT_FOUND,
|
||||
/**
|
||||
* The profile has not been fetched yet.
|
||||
*/
|
||||
PENDING,
|
||||
/**
|
||||
* The profile was successfully fetched.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* The profile request failed because too many requests were sent.
|
||||
*/
|
||||
TOO_MANY_REQUESTS
|
||||
}
|
||||
package net.citizensnpcs.npc.skin.profile;
|
||||
|
||||
/**
|
||||
* The result status of a profile fetch.
|
||||
*/
|
||||
public enum ProfileFetchResult {
|
||||
/**
|
||||
* The profile request failed for unknown reasons.
|
||||
*/
|
||||
FAILED,
|
||||
/**
|
||||
* The profile request failed because the profile was not found.
|
||||
*/
|
||||
NOT_FOUND,
|
||||
/**
|
||||
* The profile has not been fetched yet.
|
||||
*/
|
||||
PENDING,
|
||||
/**
|
||||
* The profile was successfully fetched.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* The profile request failed because too many requests were sent.
|
||||
*/
|
||||
TOO_MANY_REQUESTS
|
||||
}
|
@ -1,236 +1,258 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.ProfileLookupCallback;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* @see ProfileFetcher
|
||||
*/
|
||||
class ProfileFetchThread implements Runnable {
|
||||
private final Deque<ProfileRequest> queue = new ArrayDeque<>();
|
||||
private final Map<String, ProfileRequest> requested = new HashMap<>(40);
|
||||
private final Object sync = new Object(); // sync for queue & requested fields
|
||||
|
||||
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.
|
||||
*
|
||||
* @see ProfileFetcher#fetch
|
||||
*/
|
||||
void fetch(String name, @Nullable ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
name = name.toLowerCase();
|
||||
ProfileRequest request;
|
||||
|
||||
synchronized (sync) {
|
||||
request = requested.get(name);
|
||||
if (request == null) {
|
||||
request = new ProfileRequest(name, handler);
|
||||
queue.add(request);
|
||||
requested.put(name, request);
|
||||
return;
|
||||
} else if (request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
queue.add(request);
|
||||
}
|
||||
}
|
||||
if (handler != null) {
|
||||
if (request.getResult() == ProfileFetchResult.PENDING
|
||||
|| request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
addHandler(request, handler);
|
||||
} else {
|
||||
sendResult(handler, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchForced(String name, ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
name = name.toLowerCase();
|
||||
ProfileRequest request;
|
||||
|
||||
synchronized (sync) {
|
||||
request = requested.get(name);
|
||||
if (request != null) {
|
||||
if (request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
queue.add(request);
|
||||
} else {
|
||||
requested.remove(name);
|
||||
queue.remove(request);
|
||||
request = null;
|
||||
}
|
||||
}
|
||||
if (request == null) {
|
||||
request = new ProfileRequest(name, handler);
|
||||
queue.add(request);
|
||||
requested.put(name, request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (handler != null) {
|
||||
if (request.getResult() == ProfileFetchResult.PENDING
|
||||
|| request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
addHandler(request, handler);
|
||||
} else {
|
||||
sendResult(handler, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more profiles.
|
||||
*
|
||||
* @param requests
|
||||
* The profile requests.
|
||||
*/
|
||||
private void fetchRequests(Collection<ProfileRequest> requests) {
|
||||
Objects.requireNonNull(requests);
|
||||
|
||||
String[] playerNames = new String[requests.size()];
|
||||
|
||||
int i = 0;
|
||||
for (ProfileRequest request : requests) {
|
||||
playerNames[i++] = request.getPlayerName();
|
||||
}
|
||||
NMS.findProfilesByNames(playerNames, new ProfileLookupCallback() {
|
||||
@SuppressWarnings("unused")
|
||||
public void onProfileLookupFailed(GameProfile profile, Exception e) {
|
||||
onProfileLookupFailed(profile.getName(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileLookupFailed(String profileName, Exception e) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Profile lookup for player '" + profileName + "' failed: " + getExceptionMsg(e));
|
||||
Messaging.debug(Throwables.getStackTraceAsString(e));
|
||||
}
|
||||
ProfileRequest request = findRequest(profileName, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileLookupSucceeded(GameProfile profile) {
|
||||
Messaging.idebug(() -> "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 (Throwable e) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Filling profile lookup for player '" + profile.getName() + "' failed: "
|
||||
+ getExceptionMsg(e) + " " + isTooManyRequests(e));
|
||||
Messaging.debug(Throwables.getStackTraceAsString(e));
|
||||
}
|
||||
if (isTooManyRequests(e)) {
|
||||
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
||||
} else {
|
||||
request.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
List<ProfileRequest> requests;
|
||||
|
||||
synchronized (sync) {
|
||||
if (queue.isEmpty())
|
||||
return;
|
||||
|
||||
requests = new ArrayList<>(queue);
|
||||
queue.clear();
|
||||
}
|
||||
try {
|
||||
fetchRequests(requests);
|
||||
} catch (Exception ex) {
|
||||
Messaging.severe("Error fetching skins: " + ex.getMessage());
|
||||
for (ProfileRequest req : requests) {
|
||||
req.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addHandler(ProfileRequest request, ProfileFetchHandler handler) {
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> request.addHandler(handler), 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
for (ProfileRequest request : requests) {
|
||||
if (request.getPlayerName().equals(name))
|
||||
return request;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getExceptionMsg(Throwable e) {
|
||||
return Throwables.getRootCause(e).getMessage();
|
||||
}
|
||||
|
||||
private static boolean isProfileNotFound(Exception e) {
|
||||
String message = e.getMessage();
|
||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||
|
||||
return message != null && message.contains("did not find") || cause != null && cause.contains("did not find");
|
||||
}
|
||||
|
||||
private static boolean isTooManyRequests(Throwable e) {
|
||||
String message = e.getMessage();
|
||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||
|
||||
return message != null && message.contains("too many requests")
|
||||
|| cause != null && cause.contains("too many requests");
|
||||
}
|
||||
|
||||
private static void sendResult(ProfileFetchHandler handler, ProfileRequest request) {
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> handler.onResult(request), 1);
|
||||
}
|
||||
}
|
||||
package net.citizensnpcs.npc.skin.profile;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.ProfileLookupCallback;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.MojangSkinGenerator;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* @see ProfileFetcher
|
||||
*/
|
||||
class ProfileFetchThread implements Runnable {
|
||||
private final Deque<ProfileRequest> queue = new ArrayDeque<>();
|
||||
private final Map<String, ProfileRequest> requested = new HashMap<>(40);
|
||||
private final Object sync = new Object(); // sync for queue & requested fields
|
||||
|
||||
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.
|
||||
*
|
||||
* @see ProfileFetcher#fetch
|
||||
*/
|
||||
void fetch(String name, @Nullable ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
ProfileRequest request;
|
||||
|
||||
synchronized (sync) {
|
||||
request = requested.get(name);
|
||||
if (request == null) {
|
||||
request = new ProfileRequest(name, handler);
|
||||
queue.add(request);
|
||||
requested.put(name, request);
|
||||
return;
|
||||
} else if (request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
queue.add(request);
|
||||
}
|
||||
}
|
||||
if (handler != null) {
|
||||
if (request.getResult() == ProfileFetchResult.PENDING
|
||||
|| request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
addHandler(request, handler);
|
||||
} else {
|
||||
sendResult(handler, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchForced(String name, ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
ProfileRequest request;
|
||||
|
||||
synchronized (sync) {
|
||||
request = requested.get(name);
|
||||
if (request != null) {
|
||||
if (request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
queue.add(request);
|
||||
} else {
|
||||
requested.remove(name);
|
||||
queue.remove(request);
|
||||
request = null;
|
||||
}
|
||||
}
|
||||
if (request == null) {
|
||||
request = new ProfileRequest(name, handler);
|
||||
queue.add(request);
|
||||
requested.put(name, request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (handler != null) {
|
||||
if (request.getResult() == ProfileFetchResult.PENDING
|
||||
|| request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
addHandler(request, handler);
|
||||
} else {
|
||||
sendResult(handler, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more profiles.
|
||||
*
|
||||
* @param requests
|
||||
* The profile requests.
|
||||
*/
|
||||
private void fetchRequests(Collection<ProfileRequest> requests) {
|
||||
Objects.requireNonNull(requests);
|
||||
|
||||
List<String> javaNames = new ArrayList<String>(requests.size());
|
||||
List<String> bedrockNames = new ArrayList<String>(0);
|
||||
|
||||
for (ProfileRequest request : requests) {
|
||||
if (Util.isBedrockName(request.getPlayerName())) {
|
||||
bedrockNames.add(request.getPlayerName());
|
||||
} else {
|
||||
javaNames.add(request.getPlayerName());
|
||||
}
|
||||
}
|
||||
NMS.findProfilesByNames(javaNames.toArray(new String[javaNames.size()]), new ProfileLookupCallback() {
|
||||
@SuppressWarnings("unused")
|
||||
public void onProfileLookupFailed(GameProfile profile, Exception e) {
|
||||
onProfileLookupFailed(profile.getName(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileLookupFailed(String profileName, Exception e) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Profile lookup for player '" + profileName + "' failed: " + getExceptionMsg(e));
|
||||
Messaging.debug(Throwables.getStackTraceAsString(e));
|
||||
}
|
||||
ProfileRequest request = findRequest(profileName, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileLookupSucceeded(GameProfile profile) {
|
||||
Messaging.idebug(() -> "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 (Throwable e) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Filling profile lookup for player '" + profile.getName() + "' failed: "
|
||||
+ getExceptionMsg(e) + " " + isTooManyRequests(e));
|
||||
Messaging.debug(Throwables.getStackTraceAsString(e));
|
||||
}
|
||||
if (isTooManyRequests(e)) {
|
||||
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
||||
} else {
|
||||
request.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
for (String name : bedrockNames) {
|
||||
String strippedName = Util.stripBedrockPrefix(name);
|
||||
ProfileRequest request = findRequest(name, requests);
|
||||
try {
|
||||
Long xuid = MojangSkinGenerator.getXUIDFromName(strippedName);
|
||||
if (xuid == null) {
|
||||
request.setResult(null, ProfileFetchResult.NOT_FOUND);
|
||||
continue;
|
||||
}
|
||||
request.setResult(MojangSkinGenerator.getFilledGameProfileByXUID(name, xuid),
|
||||
ProfileFetchResult.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
request.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
List<ProfileRequest> requests;
|
||||
|
||||
synchronized (sync) {
|
||||
if (queue.isEmpty())
|
||||
return;
|
||||
|
||||
requests = new ArrayList<>(queue);
|
||||
queue.clear();
|
||||
}
|
||||
try {
|
||||
fetchRequests(requests);
|
||||
} catch (Exception ex) {
|
||||
Messaging.severe("Error fetching skins: " + ex.getMessage());
|
||||
for (ProfileRequest req : requests) {
|
||||
req.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addHandler(ProfileRequest request, ProfileFetchHandler handler) {
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> request.addHandler(handler), 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
|
||||
for (ProfileRequest request : requests) {
|
||||
if (request.getPlayerName().equals(name))
|
||||
return request;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getExceptionMsg(Throwable e) {
|
||||
return Throwables.getRootCause(e).getMessage();
|
||||
}
|
||||
|
||||
private static boolean isProfileNotFound(Exception e) {
|
||||
String message = e.getMessage();
|
||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||
|
||||
return message != null && message.contains("did not find") || cause != null && cause.contains("did not find");
|
||||
}
|
||||
|
||||
private static boolean isTooManyRequests(Throwable e) {
|
||||
String message = e.getMessage();
|
||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||
|
||||
return message != null && message.contains("too many requests")
|
||||
|| cause != null && cause.contains("too many requests");
|
||||
}
|
||||
|
||||
private static void sendResult(ProfileFetchHandler handler, ProfileRequest request) {
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> handler.onResult(request), 1);
|
||||
}
|
||||
}
|
@ -1,71 +1,71 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
|
||||
/**
|
||||
* Fetches game profiles that include skin data from Mojang servers.
|
||||
*
|
||||
* @see ProfileFetchThread
|
||||
*/
|
||||
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) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
if (PROFILE_THREAD == null) {
|
||||
initThread();
|
||||
}
|
||||
PROFILE_THREAD.fetch(name, handler);
|
||||
}
|
||||
|
||||
public static void fetchForced(String name, ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
if (PROFILE_THREAD == null) {
|
||||
initThread();
|
||||
}
|
||||
PROFILE_THREAD.fetchForced(name, handler);
|
||||
}
|
||||
|
||||
private static void initThread() {
|
||||
if (THREAD_TASK != null) {
|
||||
THREAD_TASK.cancel();
|
||||
}
|
||||
PROFILE_THREAD = new ProfileFetchThread();
|
||||
THREAD_TASK = Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD, 21, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all queued and cached requests.
|
||||
*/
|
||||
public static void reset() {
|
||||
initThread();
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (THREAD_TASK != null) {
|
||||
THREAD_TASK.cancel();
|
||||
THREAD_TASK = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ProfileFetchThread PROFILE_THREAD;
|
||||
private static BukkitTask THREAD_TASK;
|
||||
}
|
||||
package net.citizensnpcs.npc.skin.profile;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
|
||||
/**
|
||||
* Fetches game profiles that include skin data from Mojang servers.
|
||||
*
|
||||
* @see ProfileFetchThread
|
||||
*/
|
||||
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) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
if (PROFILE_THREAD == null) {
|
||||
initThread();
|
||||
}
|
||||
PROFILE_THREAD.fetch(name, handler);
|
||||
}
|
||||
|
||||
public static void fetchForced(String name, ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(name);
|
||||
|
||||
if (PROFILE_THREAD == null) {
|
||||
initThread();
|
||||
}
|
||||
PROFILE_THREAD.fetchForced(name, handler);
|
||||
}
|
||||
|
||||
private static void initThread() {
|
||||
if (THREAD_TASK != null) {
|
||||
THREAD_TASK.cancel();
|
||||
}
|
||||
PROFILE_THREAD = new ProfileFetchThread();
|
||||
THREAD_TASK = Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD, 21, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all queued and cached requests.
|
||||
*/
|
||||
public static void reset() {
|
||||
initThread();
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (THREAD_TASK != null) {
|
||||
THREAD_TASK.cancel();
|
||||
THREAD_TASK = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ProfileFetchThread PROFILE_THREAD;
|
||||
private static BukkitTask THREAD_TASK;
|
||||
}
|
@ -1,123 +1,123 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
|
||||
/**
|
||||
* Stores basic information about a single profile used to request profiles from the Mojang servers.
|
||||
*
|
||||
* <p>
|
||||
* Also stores the result of the request.
|
||||
* </p>
|
||||
*/
|
||||
public class ProfileRequest {
|
||||
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.
|
||||
*/
|
||||
|
||||
public ProfileRequest(String playerName, ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(playerName);
|
||||
|
||||
this.playerName = playerName;
|
||||
|
||||
if (handler != null) {
|
||||
addHandler(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one time result handler.
|
||||
*
|
||||
* <p>
|
||||
* Handler is always invoked from the main thread.
|
||||
* </p>
|
||||
*
|
||||
* @param handler
|
||||
* The result handler.
|
||||
*/
|
||||
public void addHandler(ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(handler);
|
||||
|
||||
if (result != ProfileFetchResult.PENDING) {
|
||||
handler.onResult(this);
|
||||
return;
|
||||
}
|
||||
if (handlers == null) {
|
||||
handlers = new ArrayDeque<>();
|
||||
}
|
||||
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>
|
||||
*
|
||||
* @param profile
|
||||
* The profile. Null if there was an error.
|
||||
* @param result
|
||||
* The result of the request.
|
||||
*/
|
||||
void setResult(@Nullable GameProfile profile, ProfileFetchResult result) {
|
||||
if (!CitizensAPI.hasImplementation())
|
||||
return;
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> {
|
||||
ProfileRequest.this.profile = profile;
|
||||
ProfileRequest.this.result = result;
|
||||
|
||||
if (handlers == null)
|
||||
return;
|
||||
|
||||
while (!handlers.isEmpty()) {
|
||||
handlers.removeFirst().onResult(ProfileRequest.this);
|
||||
}
|
||||
handlers = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
package net.citizensnpcs.npc.skin.profile;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
|
||||
/**
|
||||
* Stores basic information about a single profile used to request profiles from the Mojang servers.
|
||||
*
|
||||
* <p>
|
||||
* Also stores the result of the request.
|
||||
* </p>
|
||||
*/
|
||||
public class ProfileRequest {
|
||||
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.
|
||||
*/
|
||||
|
||||
public ProfileRequest(String playerName, ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(playerName);
|
||||
|
||||
this.playerName = playerName;
|
||||
|
||||
if (handler != null) {
|
||||
addHandler(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one time result handler.
|
||||
*
|
||||
* <p>
|
||||
* Handler is always invoked from the main thread.
|
||||
* </p>
|
||||
*
|
||||
* @param handler
|
||||
* The result handler.
|
||||
*/
|
||||
public void addHandler(ProfileFetchHandler handler) {
|
||||
Objects.requireNonNull(handler);
|
||||
|
||||
if (result != ProfileFetchResult.PENDING) {
|
||||
handler.onResult(this);
|
||||
return;
|
||||
}
|
||||
if (handlers == null) {
|
||||
handlers = new ArrayDeque<>();
|
||||
}
|
||||
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>
|
||||
*
|
||||
* @param profile
|
||||
* The profile. Null if there was an error.
|
||||
* @param result
|
||||
* The result of the request.
|
||||
*/
|
||||
void setResult(@Nullable GameProfile profile, ProfileFetchResult result) {
|
||||
if (!CitizensAPI.hasImplementation())
|
||||
return;
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> {
|
||||
ProfileRequest.this.profile = profile;
|
||||
ProfileRequest.this.result = result;
|
||||
|
||||
if (handlers == null)
|
||||
return;
|
||||
|
||||
while (!handlers.isEmpty()) {
|
||||
handlers.removeFirst().onResult(ProfileRequest.this);
|
||||
}
|
||||
handlers = null;
|
||||
});
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.HumanEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
@ -482,6 +483,9 @@ public class ShopTrait extends Trait {
|
||||
if (timesPurchasable > 0) {
|
||||
purchases.put(player.getUniqueId(), purchases.getOrDefault(player.getUniqueId(), 0) + 1);
|
||||
}
|
||||
if (NPCShopPurchaseEvent.HANDLERS.getRegisteredListeners().length > 0) {
|
||||
Bukkit.getPluginManager().callEvent(new NPCShopPurchaseEvent(player, shop, this));
|
||||
}
|
||||
}
|
||||
|
||||
private String placeholders(String string, Player player) {
|
||||
@ -503,7 +507,7 @@ public class ShopTrait extends Trait {
|
||||
public void save(DataKey key) {
|
||||
}
|
||||
|
||||
private static Pattern PLACEHOLDER_REGEX = Pattern.compile("<(cost|result)>", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern PLACEHOLDER_REGEX = Pattern.compile("<(cost|result)>", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
@Menu(title = "NPC Shop Item Editor", type = InventoryType.CHEST, dimensions = { 6, 9 })
|
||||
@ -748,6 +752,37 @@ public class ShopTrait extends Trait {
|
||||
}
|
||||
}
|
||||
|
||||
public static class NPCShopPurchaseEvent extends Event {
|
||||
private final NPCShopItem item;
|
||||
private final Player player;
|
||||
private final NPCShop shop;
|
||||
|
||||
public NPCShopPurchaseEvent(Player player, NPCShop shop, NPCShopItem item) {
|
||||
this.player = player;
|
||||
this.shop = shop;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public NPCShopItem getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public NPCShop getShop() {
|
||||
return shop;
|
||||
}
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
}
|
||||
|
||||
@Menu(title = "NPC Shop Editor", type = InventoryType.CHEST, dimensions = { 1, 9 })
|
||||
public static class NPCShopSettings extends InventoryMenuPage {
|
||||
private MenuContext ctx;
|
||||
@ -849,7 +884,7 @@ public class ShopTrait extends Trait {
|
||||
ctx.getSlot(i).setClickHandler(evt -> {
|
||||
evt.setCancelled(true);
|
||||
item.onClick(shop, (Player) evt.getWhoClicked(),
|
||||
new InventoryMultiplexer(((Player) evt.getWhoClicked()).getInventory()), evt.isShiftClick(),
|
||||
new InventoryMultiplexer(evt.getWhoClicked().getInventory()), evt.isShiftClick(),
|
||||
lastClickedItem == item);
|
||||
lastClickedItem = item;
|
||||
});
|
||||
|
@ -36,17 +36,16 @@ public class SkinTrait extends Trait {
|
||||
super("skintrait");
|
||||
}
|
||||
|
||||
private void checkPlaceholder(boolean update) {
|
||||
private boolean checkPlaceholder() {
|
||||
if (skinName == null)
|
||||
return;
|
||||
return false;
|
||||
String filled = ChatColor.stripColor(Placeholders.replace(skinName, null, npc).toLowerCase());
|
||||
if (!filled.equalsIgnoreCase(skinName) && !filled.equalsIgnoreCase(filledPlaceholder)) {
|
||||
filledPlaceholder = filled;
|
||||
Messaging.debug("Filled skin placeholder", filled, "from", skinName);
|
||||
if (update) {
|
||||
onSkinChange(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,7 +88,7 @@ public class SkinTrait extends Trait {
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) {
|
||||
checkPlaceholder(false);
|
||||
checkPlaceholder();
|
||||
}
|
||||
|
||||
private void onSkinChange(boolean forceUpdate) {
|
||||
@ -103,7 +102,9 @@ public class SkinTrait extends Trait {
|
||||
if (timer-- > 0)
|
||||
return;
|
||||
timer = Setting.PLACEHOLDER_SKIN_UPDATE_FREQUENCY.asTicks();
|
||||
checkPlaceholder(true);
|
||||
if (checkPlaceholder()) {
|
||||
onSkinChange(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,7 +148,6 @@ public class SkinTrait extends Trait {
|
||||
|
||||
private void setSkinNameInternal(String name) {
|
||||
skinName = ChatColor.stripColor(name);
|
||||
checkPlaceholder(false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,9 @@ import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@ -13,6 +15,7 @@ import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
|
||||
@ -23,7 +26,7 @@ public class MojangSkinGenerator {
|
||||
DataOutputStream out = null;
|
||||
InputStreamReader reader = null;
|
||||
try {
|
||||
URL target = new URL("https://api.mineskin.org/generate/upload" + (slim ? "?model=slim" : ""));
|
||||
URL target = new URI("https://api.mineskin.org/generate/upload" + (slim ? "?model=slim" : "")).toURL();
|
||||
HttpURLConnection con = (HttpURLConnection) target.openConnection();
|
||||
con.setRequestMethod("POST");
|
||||
con.setDoOutput(true);
|
||||
@ -83,7 +86,7 @@ public class MojangSkinGenerator {
|
||||
DataOutputStream out = null;
|
||||
InputStreamReader reader = null;
|
||||
try {
|
||||
URL target = new URL("https://api.mineskin.org/generate/url");
|
||||
URL target = new URI("https://api.mineskin.org/generate/url").toURL();
|
||||
HttpURLConnection con = (HttpURLConnection) target.openConnection();
|
||||
con.setRequestMethod("POST");
|
||||
con.setDoOutput(true);
|
||||
@ -131,5 +134,80 @@ public class MojangSkinGenerator {
|
||||
}).get();
|
||||
}
|
||||
|
||||
public static GameProfile getFilledGameProfileByXUID(String name, long xuid)
|
||||
throws InterruptedException, ExecutionException {
|
||||
return EXECUTOR.submit(() -> {
|
||||
InputStreamReader reader = null;
|
||||
try {
|
||||
URL target = new URI("https://api.geysermc.org/v2/skin/" + xuid).toURL();
|
||||
HttpURLConnection con = (HttpURLConnection) target.openConnection();
|
||||
con.setRequestMethod("GET");
|
||||
con.setRequestProperty("User-Agent", "Citizens/2.0");
|
||||
con.setRequestProperty("Accept", "application/json");
|
||||
con.setConnectTimeout(2000);
|
||||
con.setReadTimeout(20000);
|
||||
reader = new InputStreamReader(con.getInputStream());
|
||||
String str = CharStreams.toString(reader);
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug(str);
|
||||
}
|
||||
if (con.getResponseCode() != 200)
|
||||
return null;
|
||||
|
||||
JSONObject output = (JSONObject) new JSONParser().parse(str);
|
||||
con.disconnect();
|
||||
String hex = Long.toHexString(xuid);
|
||||
GameProfile profile = new GameProfile(
|
||||
UUID.fromString("00000000-0000-0000-" + hex.substring(0, 4) + "-" + hex.substring(4)), name);
|
||||
new SkinProperty((String) output.get("texture_id"), (String) output.get("value"),
|
||||
(String) output.get("signature")).apply(profile);
|
||||
return profile;
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}).get();
|
||||
}
|
||||
|
||||
public static Long getXUIDFromName(String name) throws InterruptedException, ExecutionException {
|
||||
return EXECUTOR.submit(() -> {
|
||||
InputStreamReader reader = null;
|
||||
try {
|
||||
URL target = new URI("https://api.geysermc.org/v2/xbox/xuid/" + name).toURL();
|
||||
HttpURLConnection con = (HttpURLConnection) target.openConnection();
|
||||
con.setRequestMethod("GET");
|
||||
con.setRequestProperty("User-Agent", "Citizens/2.0");
|
||||
con.setRequestProperty("Accept", "application/json");
|
||||
con.setConnectTimeout(2000);
|
||||
con.setReadTimeout(10000);
|
||||
reader = new InputStreamReader(con.getInputStream());
|
||||
String str = CharStreams.toString(reader);
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug(str);
|
||||
}
|
||||
if (con.getResponseCode() != 200)
|
||||
return null;
|
||||
|
||||
JSONObject output = (JSONObject) new JSONParser().parse(str);
|
||||
con.disconnect();
|
||||
if (!output.containsKey("xuid"))
|
||||
return null;
|
||||
|
||||
return ((Number) output.get("xuid")).longValue();
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}).get();
|
||||
}
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
@ -198,13 +199,9 @@ public class Util {
|
||||
}
|
||||
|
||||
public static Entity getEntity(UUID uuid) {
|
||||
if (SUPPORTS_BUKKIT_GETENTITY) {
|
||||
try {
|
||||
return Bukkit.getEntity(uuid);
|
||||
} catch (Throwable t) {
|
||||
SUPPORTS_BUKKIT_GETENTITY = false;
|
||||
}
|
||||
}
|
||||
if (SUPPORTS_BUKKIT_GETENTITY)
|
||||
return Bukkit.getEntity(uuid);
|
||||
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
for (Entity entity : world.getEntities()) {
|
||||
if (entity.getUniqueId().equals(uuid))
|
||||
@ -298,6 +295,10 @@ public class Util {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isBedrockName(String name) {
|
||||
return BEDROCK_NAME_PREFIX != null ? name.startsWith(BEDROCK_NAME_PREFIX) : false;
|
||||
}
|
||||
|
||||
public static boolean isHorse(EntityType type) {
|
||||
String name = type.name();
|
||||
return type == EntityType.HORSE || name.contains("_HORSE") || name.equals("DONKEY") || name.equals("MULE")
|
||||
@ -354,10 +355,9 @@ public class Util {
|
||||
}
|
||||
|
||||
public static boolean matchesItemInHand(Player player, String setting) {
|
||||
String parts = setting;
|
||||
if (parts.contains("*") || parts.isEmpty())
|
||||
if (setting.contains("*") || setting.isEmpty())
|
||||
return true;
|
||||
for (String part : Splitter.on(',').split(parts)) {
|
||||
for (String part : Splitter.on(',').split(setting)) {
|
||||
Material matchMaterial = SpigotUtil.isUsing1_13API() ? Material.matchMaterial(part, false)
|
||||
: Material.matchMaterial(part);
|
||||
if (matchMaterial == player.getInventory().getItemInHand().getType())
|
||||
@ -421,6 +421,17 @@ public class Util {
|
||||
return duration == null ? -1 : toTicks(duration);
|
||||
}
|
||||
|
||||
public static String possiblyConvertToBedrockName(String name) {
|
||||
return name.startsWith(BEDROCK_NAME_PREFIX) ? name : BEDROCK_NAME_PREFIX + name;
|
||||
}
|
||||
|
||||
public static String possiblyStripBedrockPrefix(String name, UUID uuid) {
|
||||
if (uuid.getMostSignificantBits() == 0) {
|
||||
return stripBedrockPrefix(name);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public static String prettyEnum(Enum<?> e) {
|
||||
return e.name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
}
|
||||
@ -490,6 +501,10 @@ public class Util {
|
||||
}
|
||||
}
|
||||
|
||||
public static String stripBedrockPrefix(String name) {
|
||||
return name.replaceFirst(Pattern.quote(BEDROCK_NAME_PREFIX), "");
|
||||
}
|
||||
|
||||
public static void talk(SpeechContext context) {
|
||||
if (context.getTalker() == null)
|
||||
return;
|
||||
@ -599,11 +614,26 @@ public class Util {
|
||||
+ TimeUnit.MILLISECONDS.convert(delay.getNano(), TimeUnit.NANOSECONDS)) / 50;
|
||||
}
|
||||
|
||||
private static String BEDROCK_NAME_PREFIX = ".";
|
||||
private static final Scoreboard DUMMY_SCOREBOARD = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||
private static boolean SUPPORTS_BUKKIT_GETENTITY = true;
|
||||
private static final DecimalFormat TWO_DIGIT_DECIMAL = new DecimalFormat();
|
||||
|
||||
static {
|
||||
TWO_DIGIT_DECIMAL.setMaximumFractionDigits(2);
|
||||
try {
|
||||
Bukkit.class.getMethod("getEntity", UUID.class);
|
||||
} catch (Exception e) {
|
||||
SUPPORTS_BUKKIT_GETENTITY = false;
|
||||
}
|
||||
Class<?> floodgateApiHolderClass;
|
||||
try {
|
||||
floodgateApiHolderClass = Class.forName("org.geysermc.floodgate.api.InstanceHolder");
|
||||
Object api = floodgateApiHolderClass.getMethod("getApi").invoke(null);
|
||||
BEDROCK_NAME_PREFIX = (String) api.getClass().getMethod("getPlayerPrefix").invoke(api);
|
||||
} catch (ClassNotFoundException e) {
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1201,13 +1201,13 @@ public class NMSImpl implements NMSBridge {
|
||||
GameProfile playerProfile = null;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
ClientboundPlayerInfoUpdatePacket.Entry npcInfo = list.get(i);
|
||||
if (npcInfo == null) {
|
||||
if (npcInfo == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
MirrorTrait trait = mirrorTraits.apply(npcInfo.profileId());
|
||||
if (trait == null || !trait.isMirroring(player)) {
|
||||
if (trait == null || !trait.isMirroring(player))
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean disableTablist = trait.getNPC().shouldRemoveFromTabList();
|
||||
|
||||
if (disableTablist != npcInfo.listed()) {
|
||||
@ -1223,7 +1223,8 @@ public class NMSImpl implements NMSBridge {
|
||||
if (trait.mirrorName()) {
|
||||
list.set(i,
|
||||
new ClientboundPlayerInfoUpdatePacket.Entry(npcInfo.profileId(), playerProfile, !disableTablist,
|
||||
npcInfo.latency(), npcInfo.gameMode(), Component.literal(playerProfile.getName()),
|
||||
npcInfo.latency(), npcInfo.gameMode(), Component.literal(Util
|
||||
.possiblyStripBedrockPrefix(playerProfile.getName(), playerProfile.getId())),
|
||||
npcInfo.chatSession()));
|
||||
changed = true;
|
||||
continue;
|
||||
@ -1413,8 +1414,8 @@ public class NMSImpl implements NMSBridge {
|
||||
public void sendTabListRemove(Player recipient, Collection<Player> skinnableNPCs) {
|
||||
Preconditions.checkNotNull(recipient);
|
||||
Preconditions.checkNotNull(skinnableNPCs);
|
||||
sendPacket(recipient, new ClientboundPlayerInfoRemovePacket(
|
||||
skinnableNPCs.stream().map((Function<? super Player, ? extends UUID>) Player::getUniqueId).collect(Collectors.toList())));
|
||||
sendPacket(recipient, new ClientboundPlayerInfoRemovePacket(skinnableNPCs.stream()
|
||||
.map((Function<? super Player, ? extends UUID>) Player::getUniqueId).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1210,7 +1210,8 @@ public class NMSImpl implements NMSBridge {
|
||||
if (trait.mirrorName()) {
|
||||
list.set(i,
|
||||
new ClientboundPlayerInfoUpdatePacket.Entry(npcInfo.profileId(), playerProfile, !disableTablist,
|
||||
npcInfo.latency(), npcInfo.gameMode(), Component.literal(playerProfile.getName()),
|
||||
npcInfo.latency(), npcInfo.gameMode(), Component.literal(Util
|
||||
.possiblyStripBedrockPrefix(playerProfile.getName(), playerProfile.getId())),
|
||||
npcInfo.chatSession()));
|
||||
changed = true;
|
||||
continue;
|
||||
@ -1400,8 +1401,8 @@ public class NMSImpl implements NMSBridge {
|
||||
public void sendTabListRemove(Player recipient, Collection<Player> players) {
|
||||
Preconditions.checkNotNull(recipient);
|
||||
Preconditions.checkNotNull(players);
|
||||
sendPacket(recipient, new ClientboundPlayerInfoRemovePacket(
|
||||
players.stream().map((Function<? super Player, ? extends UUID>) Player::getUniqueId).collect(Collectors.toList())));
|
||||
sendPacket(recipient, new ClientboundPlayerInfoRemovePacket(players.stream()
|
||||
.map((Function<? super Player, ? extends UUID>) Player::getUniqueId).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -111,6 +111,7 @@ public class TraderLlamaController extends MobEntityController {
|
||||
super.customServerAiStep();
|
||||
}
|
||||
setDespawnDelay(10);
|
||||
NMS.setStepHeight(getBukkitEntity(), 1);
|
||||
npc.update();
|
||||
}
|
||||
}
|
||||
|
@ -1188,7 +1188,8 @@ public class NMSImpl implements NMSBridge {
|
||||
if (trait.mirrorName()) {
|
||||
list.set(i,
|
||||
new ClientboundPlayerInfoUpdatePacket.Entry(npcInfo.profileId(), playerProfile, !disableTablist,
|
||||
npcInfo.latency(), npcInfo.gameMode(), Component.literal(playerProfile.getName()),
|
||||
npcInfo.latency(), npcInfo.gameMode(), Component.literal(Util
|
||||
.possiblyStripBedrockPrefix(playerProfile.getName(), playerProfile.getId())),
|
||||
npcInfo.chatSession()));
|
||||
changed = true;
|
||||
continue;
|
||||
|
Loading…
Reference in New Issue
Block a user