diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index 91c555f3d..581f0e93f 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -56,6 +56,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.*; import java.util.concurrent.*; +import java.util.function.Predicate; public class SkinProvider { public static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes(); @@ -83,6 +84,12 @@ public class SkinProvider { private static final Map cachedGeometry = new ConcurrentHashMap<>(); + /** + * Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and + * offline mode players use version 3. + */ + public static final Predicate IS_NPC = uuid -> uuid.version() == 2; + public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars(); public static final String EARS_GEOMETRY; public static final String EARS_GEOMETRY_SLIM; @@ -293,6 +300,10 @@ public class SkinProvider { String username, boolean newThread) { if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) { for (CapeProvider provider : CapeProvider.VALUES) { + if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) { + continue; + } + Cape cape1 = getOrDefault( requestCape(provider.getUrlFor(playerId, username), provider, newThread), EMPTY_CAPE, 4 @@ -330,6 +341,10 @@ public class SkinProvider { */ public static CompletableFuture requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) { for (EarsProvider provider : EarsProvider.VALUES) { + if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) { + continue; + } + Skin skin1 = getOrDefault( requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin), officialSkin, 4