From 9a601d9e6e1d372dbae2c4a5d47bfbc275e85fc7 Mon Sep 17 00:00:00 2001 From: Brendan Callanan Date: Thu, 6 Nov 2025 00:34:34 +0000 Subject: [PATCH 1/4] feat: adding --static param for npc skin command --- .../fancynpcs/commands/npc/SkinCMD.java | 119 +++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java index 6b0e2298..0a0530d2 100644 --- a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java +++ b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java @@ -20,10 +20,23 @@ import org.incendo.cloud.context.CommandInput; import org.jetbrains.annotations.NotNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import de.oliver.fancynpcs.skins.SkinManagerImpl; +import de.oliver.fancynpcs.skins.mojang.MojangAPI; +import de.oliver.fancynpcs.skins.mineskin.RatelimitException; + import java.io.File; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Base64; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; public enum SkinCMD { INSTANCE; // SINGLETON @@ -38,7 +51,8 @@ public void onSkin( final @NotNull CommandSender sender, final @NotNull Npc npc, final @NotNull @Argument(suggestions = "SkinCMD/skin") String skin, - final @Flag("slim") boolean slim + final @Flag("slim") boolean slim, + final @Flag("static") boolean static ) { if (npc.getData().getType() != EntityType.PLAYER) { translator.translate("command_unsupported_npc_type").send(sender); @@ -69,6 +83,11 @@ public void onSkin( translator.translate("command_npc_modification_cancelled").send(sender); } } else try { + if (static && SkinUtils.isUsername(skin)) { + downloadStaticSkin(skin, sender, npc, slim); + return; + } + SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(skin, variant); skinData.setIdentifier(skin); @@ -102,6 +121,104 @@ public void onSkin( /* UTILITY METHODS */ + private void downloadStaticSkin(String username, CommandSender sender, Npc npc, boolean slim) { + SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; + + try { + SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByUsername(username, variant); + if (skinData.hasTexture()) { + processStaticSkin(skinData.getTextureValue(), username, sender, npc, slim); + return; + } + } catch (SkinLoadException e) { + translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); + return; + } + + UUID uuid = FancyNpcs.getInstance().getSkinManagerImpl().getUuidCache().getUUID(username); + if (uuid == null) { + translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); + return; + } + + CompletableFuture.supplyAsync(() -> { + try { + return new MojangAPI(SkinManagerImpl.EXECUTOR).fetchSkin(uuid.toString(), variant); + } catch (RatelimitException e) { + return null; + } + }).thenAccept(fetchedSkinData -> { + if (fetchedSkinData == null || !fetchedSkinData.hasTexture()) { + FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); + }); + return; + } + processStaticSkin(fetchedSkinData.getTextureValue(), username, sender, npc, slim); + }); + + translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); + } + + private void processStaticSkin(String textureValue, String username, CommandSender sender, Npc npc, boolean slim) { + try { + String decodedJson = new String(Base64.getDecoder().decode(textureValue)); + JsonObject jsonObject = JsonParser.parseString(decodedJson).getAsJsonObject(); + String skinUrl = jsonObject.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString(); + + String fileName = username + ".png"; + File skinFile = new File(new File(FancyNpcs.getInstance().getDataFolder(), "skins"), fileName); + skinFile.getParentFile().mkdirs(); + + HttpClient.newHttpClient().sendAsync( + HttpRequest.newBuilder().uri(URI.create(skinUrl)).GET().build(), + HttpResponse.BodyHandlers.ofFile(skinFile.toPath()) + ).thenAccept(response -> { + if (response.statusCode() >= 200 && response.statusCode() < 300) { + FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + try { + SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; + SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(fileName, variant); + skinData.setIdentifier(fileName); + + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinData, sender).callEvent()) { + translator.translate("npc_skin_set") + .replace("npc", npc.getData().getName()) + .replace("name", skinData.getIdentifier()) + .send(sender); + if (!skinData.hasTexture()) { + translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); + } + npc.getData().setMirrorSkin(false); + npc.getData().setSkinData(skinData); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + } catch (SkinLoadException e) { + translator.translate("npc_skin_failure_invalid_file").replace("npc", npc.getData().getName()).send(sender); + } + }); + } else { + FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); + }); + } + }).exceptionally(throwable -> { + FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); + }); + return null; + }); + } catch (Exception e) { + FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); + }); + } + } + @Suggestions("SkinCMD/skin") public List suggestSkin(final CommandContext context, final CommandInput input) { return new ArrayList<>() {{ From cdfabcd46f3c6c87c8c5b6b72ff69fc59f3b5658 Mon Sep 17 00:00:00 2001 From: Brendan Callanan Date: Thu, 6 Nov 2025 00:45:52 +0000 Subject: [PATCH 2/4] refactor: improve static skin download code quality --- .../fancynpcs/commands/npc/SkinCMD.java | 105 ++++++++---------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java index 0a0530d2..8349a09e 100644 --- a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java +++ b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java @@ -42,6 +42,7 @@ public enum SkinCMD { INSTANCE; // SINGLETON private final Translator translator = FancyNpcs.getInstance().getTranslator(); + private final HttpClient httpClient = HttpClient.newHttpClient(); /* PARSERS AND SUGGESTIONS */ @@ -52,7 +53,7 @@ public void onSkin( final @NotNull Npc npc, final @NotNull @Argument(suggestions = "SkinCMD/skin") String skin, final @Flag("slim") boolean slim, - final @Flag("static") boolean static + final @Flag("static") boolean isStatic ) { if (npc.getData().getType() != EntityType.PLAYER) { translator.translate("command_unsupported_npc_type").send(sender); @@ -83,32 +84,16 @@ public void onSkin( translator.translate("command_npc_modification_cancelled").send(sender); } } else try { - if (static && SkinUtils.isUsername(skin)) { + if (isStatic && SkinUtils.isUsername(skin)) { downloadStaticSkin(skin, sender, npc, slim); return; } - SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; - SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(skin, variant); + final SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; + final SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(skin, variant); skinData.setIdentifier(skin); - if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinData, sender).callEvent()) { - translator.translate("npc_skin_set") - .replace("npc", npc.getData().getName()) - .replace("name", skinData.getIdentifier()) - .send(sender); - if (!skinData.hasTexture()) { - translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); - } - npc.getData().setMirrorSkin(false); - npc.getData().setSkinData(skinData); - npc.removeForAll(); - npc.create(); - npc.spawnForAll(); - - } else { - translator.translate("command_npc_modification_cancelled").send(sender); - } + applySkin(npc, skinData, sender); } catch (final SkinLoadException e) { switch (e.getReason()) { case INVALID_URL -> translator.translate("npc_skin_failure_invalid_url").replace("npc", npc.getData().getName()).send(sender); @@ -121,33 +106,35 @@ public void onSkin( /* UTILITY METHODS */ - private void downloadStaticSkin(String username, CommandSender sender, Npc npc, boolean slim) { - SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; - + private void downloadStaticSkin(final String username, final CommandSender sender, final Npc npc, final boolean slim) { + final SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; + try { - SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByUsername(username, variant); + final SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByUsername(username, variant); if (skinData.hasTexture()) { processStaticSkin(skinData.getTextureValue(), username, sender, npc, slim); return; } - } catch (SkinLoadException e) { + } catch (final SkinLoadException e) { translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); return; } - UUID uuid = FancyNpcs.getInstance().getSkinManagerImpl().getUuidCache().getUUID(username); + final UUID uuid = FancyNpcs.getInstance().getSkinManagerImpl().getUuidCache().getUUID(username); if (uuid == null) { translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); return; } + translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); + CompletableFuture.supplyAsync(() -> { try { return new MojangAPI(SkinManagerImpl.EXECUTOR).fetchSkin(uuid.toString(), variant); - } catch (RatelimitException e) { + } catch (final RatelimitException e) { return null; } - }).thenAccept(fetchedSkinData -> { + }, SkinManagerImpl.EXECUTOR).thenAccept(fetchedSkinData -> { if (fetchedSkinData == null || !fetchedSkinData.hasTexture()) { FancyNpcs.getInstance().getScheduler().runTask(null, () -> { translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); @@ -156,48 +143,31 @@ private void downloadStaticSkin(String username, CommandSender sender, Npc npc, } processStaticSkin(fetchedSkinData.getTextureValue(), username, sender, npc, slim); }); - - translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); } - private void processStaticSkin(String textureValue, String username, CommandSender sender, Npc npc, boolean slim) { + private void processStaticSkin(final String textureValue, final String username, final CommandSender sender, final Npc npc, final boolean slim) { try { - String decodedJson = new String(Base64.getDecoder().decode(textureValue)); - JsonObject jsonObject = JsonParser.parseString(decodedJson).getAsJsonObject(); - String skinUrl = jsonObject.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString(); + final String decodedJson = new String(Base64.getDecoder().decode(textureValue)); + final JsonObject jsonObject = JsonParser.parseString(decodedJson).getAsJsonObject(); + final String skinUrl = jsonObject.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString(); - String fileName = username + ".png"; - File skinFile = new File(new File(FancyNpcs.getInstance().getDataFolder(), "skins"), fileName); + final String fileName = username + ".png"; + final File skinFile = new File(new File(FancyNpcs.getInstance().getDataFolder(), "skins"), fileName); skinFile.getParentFile().mkdirs(); - HttpClient.newHttpClient().sendAsync( + httpClient.sendAsync( HttpRequest.newBuilder().uri(URI.create(skinUrl)).GET().build(), HttpResponse.BodyHandlers.ofFile(skinFile.toPath()) ).thenAccept(response -> { if (response.statusCode() >= 200 && response.statusCode() < 300) { FancyNpcs.getInstance().getScheduler().runTask(null, () -> { try { - SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; - SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(fileName, variant); + final SkinData.SkinVariant variant = slim ? SkinData.SkinVariant.SLIM : SkinData.SkinVariant.AUTO; + final SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(fileName, variant); skinData.setIdentifier(fileName); - if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinData, sender).callEvent()) { - translator.translate("npc_skin_set") - .replace("npc", npc.getData().getName()) - .replace("name", skinData.getIdentifier()) - .send(sender); - if (!skinData.hasTexture()) { - translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); - } - npc.getData().setMirrorSkin(false); - npc.getData().setSkinData(skinData); - npc.removeForAll(); - npc.create(); - npc.spawnForAll(); - } else { - translator.translate("command_npc_modification_cancelled").send(sender); - } - } catch (SkinLoadException e) { + applySkin(npc, skinData, sender); + } catch (final SkinLoadException e) { translator.translate("npc_skin_failure_invalid_file").replace("npc", npc.getData().getName()).send(sender); } }); @@ -212,13 +182,32 @@ private void processStaticSkin(String textureValue, String username, CommandSend }); return null; }); - } catch (Exception e) { + } catch (final Exception e) { FancyNpcs.getInstance().getScheduler().runTask(null, () -> { translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); }); } } + private void applySkin(final Npc npc, final SkinData skinData, final CommandSender sender) { + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinData, sender).callEvent()) { + translator.translate("npc_skin_set") + .replace("npc", npc.getData().getName()) + .replace("name", skinData.getIdentifier()) + .send(sender); + if (!skinData.hasTexture()) { + translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); + } + npc.getData().setMirrorSkin(false); + npc.getData().setSkinData(skinData); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + } + @Suggestions("SkinCMD/skin") public List suggestSkin(final CommandContext context, final CommandInput input) { return new ArrayList<>() {{ From f0733830d4ffba988fafe1d4922e64b6e1fe981a Mon Sep 17 00:00:00 2001 From: Brendan Callanan Date: Thu, 6 Nov 2025 01:14:35 +0000 Subject: [PATCH 3/4] fix: resolved bad order causing duplicate messages when loading skins --- .../java/de/oliver/fancynpcs/commands/npc/SkinCMD.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java index 8349a09e..1b9ee017 100644 --- a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java +++ b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java @@ -126,8 +126,6 @@ private void downloadStaticSkin(final String username, final CommandSender sende return; } - translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); - CompletableFuture.supplyAsync(() -> { try { return new MojangAPI(SkinManagerImpl.EXECUTOR).fetchSkin(uuid.toString(), variant); @@ -166,6 +164,9 @@ private void processStaticSkin(final String textureValue, final String username, final SkinData skinData = FancyNpcs.getInstance().getSkinManagerImpl().getByIdentifier(fileName, variant); skinData.setIdentifier(fileName); + if (!skinData.hasTexture()) { + translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); + } applySkin(npc, skinData, sender); } catch (final SkinLoadException e) { translator.translate("npc_skin_failure_invalid_file").replace("npc", npc.getData().getName()).send(sender); @@ -195,7 +196,7 @@ private void applySkin(final Npc npc, final SkinData skinData, final CommandSend .replace("npc", npc.getData().getName()) .replace("name", skinData.getIdentifier()) .send(sender); - if (!skinData.hasTexture()) { + if (!skinData.hasTexture() && !SkinUtils.isFile(skinData.getIdentifier())) { translator.translate("npc_skin_set_later").replace("npc", npc.getData().getName()).send(sender); } npc.getData().setMirrorSkin(false); From 8df1a499d7d28cfc2d2cc3e106fa4d21af096adb Mon Sep 17 00:00:00 2001 From: Brendan Callanan Date: Thu, 6 Nov 2025 10:41:51 +0000 Subject: [PATCH 4/4] refactor: clean up downloadStaticSkin impl --- .../fancynpcs/commands/npc/SkinCMD.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java index 1b9ee017..e4cf5aaf 100644 --- a/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java +++ b/plugins/fancynpcs/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java @@ -120,26 +120,24 @@ private void downloadStaticSkin(final String username, final CommandSender sende return; } - final UUID uuid = FancyNpcs.getInstance().getSkinManagerImpl().getUuidCache().getUUID(username); - if (uuid == null) { - translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); - return; - } - CompletableFuture.supplyAsync(() -> { try { + final UUID uuid = FancyNpcs.getInstance().getSkinManagerImpl().getUuidCache().getUUID(username); + if (uuid == null) { + return null; + } return new MojangAPI(SkinManagerImpl.EXECUTOR).fetchSkin(uuid.toString(), variant); } catch (final RatelimitException e) { return null; } }, SkinManagerImpl.EXECUTOR).thenAccept(fetchedSkinData -> { - if (fetchedSkinData == null || !fetchedSkinData.hasTexture()) { - FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + FancyNpcs.getInstance().getScheduler().runTask(null, () -> { + if (fetchedSkinData == null || !fetchedSkinData.hasTexture()) { translator.translate("npc_skin_failure_invalid_username").replace("npc", npc.getData().getName()).send(sender); - }); - return; - } - processStaticSkin(fetchedSkinData.getTextureValue(), username, sender, npc, slim); + return; + } + processStaticSkin(fetchedSkinData.getTextureValue(), username, sender, npc, slim); + }); }); }