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:
fullwall 2024-07-19 11:19:32 +08:00
parent 7a9a4db787
commit 2c0c7bf8a0
17 changed files with 703 additions and 531 deletions

View File

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

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -111,6 +111,7 @@ public class TraderLlamaController extends MobEntityController {
super.customServerAiStep();
}
setDespawnDelay(10);
NMS.setStepHeight(getBukkitEntity(), 1);
npc.update();
}
}

View File

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