diff --git a/main/src/main/java/net/citizensnpcs/Citizens.java b/main/src/main/java/net/citizensnpcs/Citizens.java index ec11e2d61..3f762fb60 100644 --- a/main/src/main/java/net/citizensnpcs/Citizens.java +++ b/main/src/main/java/net/citizensnpcs/Citizens.java @@ -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; diff --git a/main/src/main/java/net/citizensnpcs/ProtocolLibListener.java b/main/src/main/java/net/citizensnpcs/ProtocolLibListener.java index fc606f79f..0599b51d3 100644 --- a/main/src/main/java/net/citizensnpcs/ProtocolLibListener.java +++ b/main/src/main/java/net/citizensnpcs/ProtocolLibListener.java @@ -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(), diff --git a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java index 208f8da81..e1242d048 100644 --- a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java +++ b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java @@ -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); } diff --git a/main/src/main/java/net/citizensnpcs/npc/skin/Skin.java b/main/src/main/java/net/citizensnpcs/npc/skin/Skin.java index 44112c51b..8cc551989 100644 --- a/main/src/main/java/net/citizensnpcs/npc/skin/Skin.java +++ b/main/src/main/java/net/citizensnpcs/npc/skin/Skin.java @@ -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 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 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"; } diff --git a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchHandler.java similarity index 85% rename from main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java rename to main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchHandler.java index fd2d3ee2f..8f2055cdd 100644 --- a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchHandler.java +++ b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchHandler.java @@ -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); +} diff --git a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchResult.java b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchResult.java similarity index 88% rename from main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchResult.java rename to main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchResult.java index 1e563c44d..c8e451d3f 100644 --- a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchResult.java +++ b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchResult.java @@ -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 +} diff --git a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchThread.java similarity index 84% rename from main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java rename to main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchThread.java index 98e78074a..284cc88de 100644 --- a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java +++ b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetchThread.java @@ -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. - * - *

- * Maintains a cache of profiles so that no profile is ever requested more than once during a single server session. - *

- * - * @see ProfileFetcher - */ -class ProfileFetchThread implements Runnable { - private final Deque queue = new ArrayDeque<>(); - private final Map 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 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 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 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. + * + *

+ * Maintains a cache of profiles so that no profile is ever requested more than once during a single server session. + *

+ * + * @see ProfileFetcher + */ +class ProfileFetchThread implements Runnable { + private final Deque queue = new ArrayDeque<>(); + private final Map 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 requests) { + Objects.requireNonNull(requests); + + List javaNames = new ArrayList(requests.size()); + List bedrockNames = new ArrayList(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 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 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); + } +} diff --git a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetcher.java similarity index 94% rename from main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java rename to main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetcher.java index 7aa6341b9..62f2260f7 100644 --- a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java +++ b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileFetcher.java @@ -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; +} diff --git a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileRequest.java similarity index 95% rename from main/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java rename to main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileRequest.java index d83b79119..3dc364f2f 100644 --- a/main/src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java +++ b/main/src/main/java/net/citizensnpcs/npc/skin/profile/ProfileRequest.java @@ -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. - * - *

- * Also stores the result of the request. - *

- */ -public class ProfileRequest { - private Deque 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. - * - *

- * Handler is always invoked from the main thread. - *

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

- * Can be invoked from any thread, always executes on the main thread. - *

- * - * @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. + * + *

+ * Also stores the result of the request. + *

+ */ +public class ProfileRequest { + private Deque 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. + * + *

+ * Handler is always invoked from the main thread. + *

+ * + * @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. + * + *

+ * Can be invoked from any thread, always executes on the main thread. + *

+ * + * @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; + }); + } +} diff --git a/main/src/main/java/net/citizensnpcs/trait/ShopTrait.java b/main/src/main/java/net/citizensnpcs/trait/ShopTrait.java index aeaa9cc2e..79b3afa72 100644 --- a/main/src/main/java/net/citizensnpcs/trait/ShopTrait.java +++ b/main/src/main/java/net/citizensnpcs/trait/ShopTrait.java @@ -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; }); diff --git a/main/src/main/java/net/citizensnpcs/trait/SkinTrait.java b/main/src/main/java/net/citizensnpcs/trait/SkinTrait.java index 99f9a7851..d0488436a 100644 --- a/main/src/main/java/net/citizensnpcs/trait/SkinTrait.java +++ b/main/src/main/java/net/citizensnpcs/trait/SkinTrait.java @@ -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); } /** diff --git a/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java b/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java index a4dc45cb6..2547a81b6 100644 --- a/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java +++ b/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java @@ -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(); } diff --git a/main/src/main/java/net/citizensnpcs/util/Util.java b/main/src/main/java/net/citizensnpcs/util/Util.java index 60823a459..d1ce348ef 100644 --- a/main/src/main/java/net/citizensnpcs/util/Util.java +++ b/main/src/main/java/net/citizensnpcs/util/Util.java @@ -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(); + } } } diff --git a/v1_19_R3/src/main/java/net/citizensnpcs/nms/v1_19_R3/util/NMSImpl.java b/v1_19_R3/src/main/java/net/citizensnpcs/nms/v1_19_R3/util/NMSImpl.java index 31fce2040..ecce9a085 100644 --- a/v1_19_R3/src/main/java/net/citizensnpcs/nms/v1_19_R3/util/NMSImpl.java +++ b/v1_19_R3/src/main/java/net/citizensnpcs/nms/v1_19_R3/util/NMSImpl.java @@ -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 skinnableNPCs) { Preconditions.checkNotNull(recipient); Preconditions.checkNotNull(skinnableNPCs); - sendPacket(recipient, new ClientboundPlayerInfoRemovePacket( - skinnableNPCs.stream().map((Function) Player::getUniqueId).collect(Collectors.toList()))); + sendPacket(recipient, new ClientboundPlayerInfoRemovePacket(skinnableNPCs.stream() + .map((Function) Player::getUniqueId).collect(Collectors.toList()))); } @Override diff --git a/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java b/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java index d5305eff7..ddd2033f3 100644 --- a/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java +++ b/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java @@ -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 players) { Preconditions.checkNotNull(recipient); Preconditions.checkNotNull(players); - sendPacket(recipient, new ClientboundPlayerInfoRemovePacket( - players.stream().map((Function) Player::getUniqueId).collect(Collectors.toList()))); + sendPacket(recipient, new ClientboundPlayerInfoRemovePacket(players.stream() + .map((Function) Player::getUniqueId).collect(Collectors.toList()))); } @Override diff --git a/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/entity/TraderLlamaController.java b/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/entity/TraderLlamaController.java index a874ac4ed..1159835f1 100644 --- a/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/entity/TraderLlamaController.java +++ b/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/entity/TraderLlamaController.java @@ -111,6 +111,7 @@ public class TraderLlamaController extends MobEntityController { super.customServerAiStep(); } setDespawnDelay(10); + NMS.setStepHeight(getBukkitEntity(), 1); npc.update(); } } diff --git a/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java b/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java index 60a8923c1..4714dbad6 100644 --- a/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java +++ b/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java @@ -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;