From 902b90212204ef511a05b08a678fb4b5cdcae7b5 Mon Sep 17 00:00:00 2001
From: D3v1s0m <49519439+D3v1s0m@users.noreply.github.com>
Date: Fri, 21 Jul 2023 15:51:32 +0530
Subject: [PATCH] Added url skin type

---
 .../api/skin/SkinDescriptorFactory.java       |  5 +++
 .../pyr/znpcsplus/commands/SkinCommand.java   | 35 ++++++++++++----
 .../entity/EntityPropertyRegistryImpl.java    |  1 +
 .../entity/properties/SkinProperty.java       | 20 +++++++++
 .../skin/SkinDescriptorFactoryImpl.java       | 13 ++++++
 .../znpcsplus/skin/cache/MojangSkinCache.java | 42 +++++++++++++++++--
 .../skin/descriptor/PrefetchedDescriptor.java |  5 +++
 7 files changed, 109 insertions(+), 12 deletions(-)
 create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/SkinProperty.java

diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java
index ba67e88..bc6954a 100644
--- a/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java
+++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java
@@ -1,8 +1,13 @@
 package lol.pyr.znpcsplus.api.skin;
 
+import java.net.MalformedURLException;
+import java.net.URL;
+
 public interface SkinDescriptorFactory {
     SkinDescriptor createMirrorDescriptor();
     SkinDescriptor createRefreshingDescriptor(String playerName);
     SkinDescriptor createStaticDescriptor(String playerName);
     SkinDescriptor createStaticDescriptor(String texture, String signature);
+    SkinDescriptor createUrlDescriptor(String url) throws MalformedURLException;
+    SkinDescriptor createUrlDescriptor(URL url);
 }
diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java
index abe40fd..69a81e7 100644
--- a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java
+++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java
@@ -17,6 +17,8 @@ import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
 
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.Collections;
 import java.util.List;
 
@@ -44,9 +46,7 @@ public class SkinCommand implements CommandHandler {
             npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache));
             npc.respawn();
             context.halt(Component.text("The NPC's skin will now mirror the player that it's being displayed to", NamedTextColor.GREEN));
-        }
-
-        if (type.equalsIgnoreCase("static")) {
+        } else if (type.equalsIgnoreCase("static")) {
             context.ensureArgsNotEmpty();
             String name = context.dumpAllArgs();
             context.send(Component.text("Fetching skin \"" + name + "\"...", NamedTextColor.GREEN));
@@ -57,25 +57,42 @@ public class SkinCommand implements CommandHandler {
                 }
                 npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin);
                 npc.respawn();
-                context.send(Component.text("The NPC's skin has been set to \"" + name + "\""));
+                context.send(Component.text("The NPC's skin has been set to \"" + name + "\"", NamedTextColor.GREEN));
             });
             return;
-        }
-
-        if (type.equalsIgnoreCase("dynamic")) {
+        } else if (type.equalsIgnoreCase("dynamic")) {
             context.ensureArgsNotEmpty();
             String name = context.dumpAllArgs();
             npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new FetchingDescriptor(skinCache, name));
             npc.respawn();
             context.halt(Component.text("The NPC's skin will now be resolved per-player from \"" + name + "\""));
+        } else if (type.equalsIgnoreCase("url")) {
+            context.ensureArgsNotEmpty();
+            String urlString = context.dumpAllArgs();
+            try {
+                URL url = new URL(urlString);
+                context.send(Component.text("Fetching skin from url \"" + urlString + "\"...", NamedTextColor.GREEN));
+                PrefetchedDescriptor.fromUrl(skinCache, url).thenAccept(skin -> {
+                    if (skin.getSkin() == null) {
+                        context.send(Component.text("Failed to fetch skin, are you sure the url is valid?", NamedTextColor.RED));
+                        return;
+                    }
+                    npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin);
+                    npc.respawn();
+                    context.send(Component.text("The NPC's skin has been set.", NamedTextColor.GREEN));
+                });
+            } catch (MalformedURLException e) {
+                context.send(Component.text("Invalid url!", NamedTextColor.RED));
+            }
+            return;
         }
-        context.send(Component.text("Unknown skin type! Please use one of the following: mirror, static, dynamic"));
+        context.send(Component.text("Unknown skin type! Please use one of the following: mirror, static, dynamic, url"));
     }
 
     @Override
     public List<String> suggest(CommandContext context) throws CommandExecutionException {
         if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds());
-        if (context.argSize() == 2) return context.suggestLiteral("mirror", "static", "dynamic");
+        if (context.argSize() == 2) return context.suggestLiteral("mirror", "static", "dynamic", "url");
         if (context.matchSuggestion("*", "static")) return context.suggestPlayers();
         return Collections.emptyList();
     }
diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java
index f0b6baa..d6b5ada 100644
--- a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java
+++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java
@@ -254,6 +254,7 @@ public class EntityPropertyRegistryImpl implements EntityPropertyRegistry {
         register(new GlowProperty(packetFactory));
         register(new EffectsProperty("fire", 0x01));
         register(new EffectsProperty("invisible", 0x20));
+        register(new SkinProperty());
     }
 
     private void registerSerializer(PropertySerializer<?> serializer) {
diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/SkinProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/SkinProperty.java
new file mode 100644
index 0000000..b0d7579
--- /dev/null
+++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/SkinProperty.java
@@ -0,0 +1,20 @@
+package lol.pyr.znpcsplus.entity.properties;
+
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+import lol.pyr.znpcsplus.api.skin.SkinDescriptor;
+import lol.pyr.znpcsplus.entity.EntityPropertyImpl;
+import lol.pyr.znpcsplus.entity.PacketEntity;
+import org.bukkit.entity.Player;
+
+import java.util.Map;
+
+public class SkinProperty extends EntityPropertyImpl<SkinDescriptor> {
+    public SkinProperty() {
+        super("skin", null, SkinDescriptor.class);
+    }
+
+    @Override
+    public void apply(SkinDescriptor value, Player player, PacketEntity entity, boolean isSpawned, Map<Integer, EntityData> properties) {
+
+    }
+}
diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java
index ca52520..8fc2b73 100644
--- a/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java
+++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java
@@ -7,6 +7,9 @@ import lol.pyr.znpcsplus.skin.descriptor.FetchingDescriptor;
 import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor;
 import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor;
 
+import java.net.MalformedURLException;
+import java.net.URL;
+
 public class SkinDescriptorFactoryImpl implements SkinDescriptorFactory {
     private final MojangSkinCache skinCache;
     private final MirrorDescriptor mirrorDescriptor;
@@ -35,4 +38,14 @@ public class SkinDescriptorFactoryImpl implements SkinDescriptorFactory {
     public SkinDescriptor createStaticDescriptor(String texture, String signature) {
         return new PrefetchedDescriptor(new Skin(texture, signature));
     }
+
+    @Override
+    public SkinDescriptor createUrlDescriptor(String url) throws MalformedURLException {
+        return createUrlDescriptor(new URL(url));
+    }
+
+    @Override
+    public SkinDescriptor createUrlDescriptor(URL url) {
+        return PrefetchedDescriptor.fromUrl(skinCache, url).join();
+    }
 }
diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java
index 8df8bcc..31a3b6a 100644
--- a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java
+++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java
@@ -8,9 +8,7 @@ import lol.pyr.znpcsplus.skin.Skin;
 import org.bukkit.Bukkit;
 import org.bukkit.entity.Player;
 
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
+import java.io.*;
 import java.lang.reflect.InvocationTargetException;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
@@ -69,6 +67,44 @@ public class MojangSkinCache {
         });
     }
 
+    public CompletableFuture<Skin> fetchByUrl(URL url) {
+
+        return CompletableFuture.supplyAsync(() -> {
+            URL apiUrl = parseUrl("https://api.mineskin.org/generate/url");
+            HttpURLConnection connection = null;
+            try {
+                connection = (HttpURLConnection) apiUrl.openConnection();
+                connection.setRequestMethod("POST");
+                connection.setRequestProperty("accept", "application/json");
+                connection.setRequestProperty("Content-Type", "application/json");
+                connection.setDoOutput(true);
+                OutputStream outStream = connection.getOutputStream();
+                DataOutputStream out = new DataOutputStream(outStream);
+                out.writeBytes("{\"variant\":\"classic\",\"url\":\"" + url.toString() + "\"}"); // TODO: configurable variant (slim, classic) default: classic
+                out.flush();
+                out.close();
+                outStream.close();
+
+                try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) {
+                    JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject();
+                    if (obj.has("error")) return null;
+                    if (!obj.has("data")) return null;
+                    JsonObject texture = obj.get("data").getAsJsonObject().get("texture").getAsJsonObject();
+                    return new Skin(texture.get("value").getAsString(), texture.get("signature").getAsString());
+                }
+
+            } catch (IOException exception) {
+                if (!configManager.getConfig().disableSkinFetcherWarnings()) {
+                    logger.warning("Failed to get skin from url:");
+                    exception.printStackTrace();
+                }
+            } finally {
+                if (connection != null) connection.disconnect();
+            }
+            return null;
+        });
+    }
+
     public boolean isNameFullyCached(String s) {
         String name = s.toLowerCase();
         if (!idCache.containsKey(name)) return false;
diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java
index 767757d..f6cc66a 100644
--- a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java
+++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java
@@ -7,6 +7,7 @@ import lol.pyr.znpcsplus.skin.Skin;
 import lol.pyr.znpcsplus.skin.cache.MojangSkinCache;
 import org.bukkit.entity.Player;
 
+import java.net.URL;
 import java.util.concurrent.CompletableFuture;
 
 public class PrefetchedDescriptor implements BaseSkinDescriptor, SkinDescriptor {
@@ -20,6 +21,10 @@ public class PrefetchedDescriptor implements BaseSkinDescriptor, SkinDescriptor
         return CompletableFuture.supplyAsync(() -> new PrefetchedDescriptor(cache.fetchByName(name).join()));
     }
 
+    public static CompletableFuture<PrefetchedDescriptor> fromUrl(MojangSkinCache cache, URL url) {
+        return CompletableFuture.supplyAsync(() -> new PrefetchedDescriptor(cache.fetchByUrl(url).join()));
+    }
+
     @Override
     public CompletableFuture<Skin> fetch(Player player) {
         return CompletableFuture.completedFuture(skin);