diff --git a/main/src/main/java/net/citizensnpcs/Citizens.java b/main/src/main/java/net/citizensnpcs/Citizens.java index 419ac2571..2a9541a6d 100644 --- a/main/src/main/java/net/citizensnpcs/Citizens.java +++ b/main/src/main/java/net/citizensnpcs/Citizens.java @@ -415,6 +415,7 @@ public class Citizens extends JavaPlugin implements CitizensPlugin { return new ShopTrait(shops); })); selector = new NPCSelector(this); + Bukkit.getPluginManager().registerEvents(new EventListen(storedRegistries), this); Bukkit.getPluginManager().registerEvents(new Placeholders(), this); Placeholders.registerNPCPlaceholder(Pattern.compile("command_[a-zA-Z_0-9]+"), (npc, sender, input) -> { diff --git a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java index b0b660b5b..c6ebb73d7 100644 --- a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java +++ b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java @@ -1,12 +1,7 @@ package net.citizensnpcs.commands; -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; +import java.io.File; +import java.nio.file.Files; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -43,7 +38,6 @@ import org.bukkit.entity.Zombie; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.ItemStack; import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -148,6 +142,7 @@ import net.citizensnpcs.trait.WolfModifiers; import net.citizensnpcs.trait.waypoint.Waypoints; import net.citizensnpcs.util.Anchor; import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.MojangSkinGenerator; import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.PlayerAnimation; import net.citizensnpcs.util.StringHelper; @@ -2609,7 +2604,7 @@ public class NPCCommands { @Command( aliases = { "npc" }, - usage = "skin (-c(lear) -l(atest)) [name] (or --url [url] or -t [uuid/name] [data] [signature])", + usage = "skin (-c(lear) -l(atest)) [name] (or --url [url] --file [file] or -t [uuid/name] [data] [signature])", desc = "Sets an NPC's skin name. Use -l to set the skin to always update to the latest", modifiers = { "skin" }, min = 1, @@ -2617,73 +2612,48 @@ public class NPCCommands { flags = "ctl", permission = "citizens.npc.skin") @Requirements(types = EntityType.PLAYER, selected = true, ownership = true) - public void skin(final CommandContext args, final CommandSender sender, final NPC npc, @Flag("url") String url) - throws CommandException { + public void skin(final CommandContext args, final CommandSender sender, final NPC npc, @Flag("url") String url, + @Flag("file") String file) throws CommandException { String skinName = npc.getName(); final SkinTrait trait = npc.getOrAddTrait(SkinTrait.class); if (args.hasFlag('c')) { trait.clearTexture(); - } else if (url != null) { - Bukkit.getScheduler().runTaskAsynchronously(CitizensAPI.getPlugin(), new Runnable() { - @Override - public void run() { - DataOutputStream out = null; - BufferedReader reader = null; - try { - URL target = new URL("https://api.mineskin.org/generate/url"); - HttpURLConnection con = (HttpURLConnection) target.openConnection(); - con.setRequestMethod("POST"); - con.setDoOutput(true); - con.setConnectTimeout(1000); - con.setReadTimeout(30000); - out = new DataOutputStream(con.getOutputStream()); - out.writeBytes("url=" + URLEncoder.encode(url, "UTF-8")); - out.close(); - reader = new BufferedReader(new InputStreamReader(con.getInputStream())); - JSONObject output = (JSONObject) new JSONParser().parse(reader); - JSONObject data = (JSONObject) output.get("data"); - String uuid = (String) data.get("uuid"); - JSONObject texture = (JSONObject) data.get("texture"); - String textureEncoded = (String) texture.get("value"); - String signature = (String) texture.get("signature"); - con.disconnect(); - Bukkit.getScheduler().runTask(CitizensAPI.getPlugin(), new Runnable() { - @Override - public void run() { - try { - trait.setSkinPersistent(uuid, signature, textureEncoded); - Messaging.sendTr(sender, Messages.SKIN_URL_SET, npc.getName(), url); - } catch (IllegalArgumentException e) { - Messaging.sendErrorTr(sender, Messages.ERROR_SETTING_SKIN_URL, url); - } - } - }); - } catch (Throwable t) { - if (Messaging.isDebugging()) { - t.printStackTrace(); - } - Bukkit.getScheduler().runTask(CitizensAPI.getPlugin(), new Runnable() { - @Override - public void run() { - Messaging.sendErrorTr(sender, Messages.ERROR_SETTING_SKIN_URL, url); - } - }); - } finally { - if (out != null) { - try { - out.close(); - } catch (IOException e) { - } - } - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - } + } else if (url != null || file != null) { + Messaging.sendErrorTr(sender, Messages.FETCHING_SKIN, file); + Bukkit.getScheduler().runTaskAsynchronously(CitizensAPI.getPlugin(), () -> { + try { + JSONObject data = null; + if (file != null) { + File skin = new File(new File(CitizensAPI.getDataFolder(), "skins"), file); + if (!skin.exists() + || !skin.getParentFile().equals(new File(CitizensAPI.getDataFolder(), "skins"))) { + Bukkit.getScheduler().runTask(CitizensAPI.getPlugin(), + () -> Messaging.sendErrorTr(sender, Messages.INVALID_SKIN_FILE, file)); + return; } + data = MojangSkinGenerator.generateFromPNG(Files.readAllBytes(skin.toPath())); + } else { + MojangSkinGenerator.generateFromURL(url); } + String uuid = (String) data.get("uuid"); + JSONObject texture = (JSONObject) data.get("texture"); + String textureEncoded = (String) texture.get("value"); + String signature = (String) texture.get("signature"); + Bukkit.getScheduler().runTask(CitizensAPI.getPlugin(), () -> { + try { + trait.setSkinPersistent(uuid, signature, textureEncoded); + Messaging.sendTr(sender, Messages.SKIN_URL_SET, npc.getName(), url); + } catch (IllegalArgumentException e) { + Messaging.sendErrorTr(sender, Messages.ERROR_SETTING_SKIN_URL, url); + } + }); + } catch (Throwable t) { + if (Messaging.isDebugging()) { + t.printStackTrace(); + } + Bukkit.getScheduler().runTask(CitizensAPI.getPlugin(), + () -> Messaging.sendErrorTr(sender, Messages.ERROR_SETTING_SKIN_URL, url)); } - }); return; } else if (args.hasFlag('t')) { diff --git a/main/src/main/java/net/citizensnpcs/util/Messages.java b/main/src/main/java/net/citizensnpcs/util/Messages.java index dc55ecc6c..7c0a7c502 100644 --- a/main/src/main/java/net/citizensnpcs/util/Messages.java +++ b/main/src/main/java/net/citizensnpcs/util/Messages.java @@ -114,6 +114,7 @@ public class Messages { public static final String FAILED_LOAD_SAVES = "citizens.saves.load-failed"; public static final String FAILED_TO_MOUNT_NPC = "citizens.commands.npc.mount.failed"; public static final String FAILED_TO_REMOVE = "citizens.commands.trait.failed-to-remove"; + public static final String FETCHING_SKIN = "citizens.commands.npc.skin.fetching"; public static final String FLYABLE_SET = "citizens.commands.npc.flyable.set"; public static final String FLYABLE_UNSET = "citizens.commands.npc.flyable.unset"; public static final String FOLLOW_PLAYER_NOT_INGAME = "citizens.commands.npc.follow.player-not-ingame"; @@ -189,6 +190,7 @@ public class Messages { public static final String INVALID_SHEEP_COLOR = "citizens.commands.npc.sheep.invalid-color"; public static final String INVALID_SHULKER_COLOR = "citizens.commands.npc.shulker.invalid-color"; public static final String INVALID_SKELETON_TYPE = "citizens.commands.npc.skeletontype.invalid-type"; + public static final String INVALID_SKIN_FILE = "citizens.commands.npc.skin.invalid-file"; public static final String INVALID_SOUND = "citizens.commands.npc.sound.invalid-sound"; public static final String INVALID_SPAWN_LOCATION = "citizens.commands.npc.create.invalid-location"; public static final String INVALID_TRIGGER_TELEPORT_FORMAT = "citizens.editors.waypoints.triggers.teleport.invalid-format"; diff --git a/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java b/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java new file mode 100644 index 000000000..deccbbfe0 --- /dev/null +++ b/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java @@ -0,0 +1,98 @@ +package net.citizensnpcs.util; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +public class MojangSkinGenerator { + public static JSONObject generateFromPNG(final byte[] png) throws InterruptedException, ExecutionException { + return EXECUTOR.submit(() -> { + DataOutputStream out = null; + BufferedReader reader = null; + try { + URL target = new URL("https://api.mineskin.org/generate/upload"); + HttpURLConnection con = (HttpURLConnection) target.openConnection(); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.setRequestProperty("Cache-Control", "no-cache"); + con.setRequestProperty("Content-Type", "multipart/form-data;boundary=*****"); + con.setConnectTimeout(1000); + con.setReadTimeout(30000); + out = new DataOutputStream(con.getOutputStream()); + out.writeBytes("--*****\r\n"); + out.writeBytes("Content-Disposition: form-data; name=\"skin.png\";filename=\"skin.png\"\r\n\r\n"); + out.write(png); + out.writeBytes("\r\n"); + out.writeBytes("--*****--\r\n"); + out.flush(); + out.close(); + reader = new BufferedReader(new InputStreamReader(con.getInputStream())); + JSONObject output = (JSONObject) new JSONParser().parse(reader); + JSONObject data = (JSONObject) output.get("data"); + con.disconnect(); + return data; + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + }).get(); + } + + public static JSONObject generateFromURL(final String url) throws InterruptedException, ExecutionException { + return EXECUTOR.submit(() -> { + DataOutputStream out = null; + BufferedReader reader = null; + try { + URL target = new URL("https://api.mineskin.org/generate/url"); + HttpURLConnection con = (HttpURLConnection) target.openConnection(); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.setConnectTimeout(1000); + con.setReadTimeout(30000); + out = new DataOutputStream(con.getOutputStream()); + out.writeBytes("url=" + URLEncoder.encode(url, "UTF-8")); + out.close(); + reader = new BufferedReader(new InputStreamReader(con.getInputStream())); + JSONObject output = (JSONObject) new JSONParser().parse(reader); + JSONObject data = (JSONObject) output.get("data"); + con.disconnect(); + return data; + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + }).get(); + } + + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); +} diff --git a/main/src/main/resources/messages_en.properties b/main/src/main/resources/messages_en.properties index 9c1b79eaa..871ed2698 100644 --- a/main/src/main/resources/messages_en.properties +++ b/main/src/main/resources/messages_en.properties @@ -276,6 +276,8 @@ citizens.commands.npc.skin.error-setting-url=Error downloading skin texture from citizens.commands.npc.skin.skin-url-set=Downloaded [[{0}]]''s skin from [[{1}]]. citizens.commands.npc.skin.set=[[{0}]]''s skin name set to [[{1}]]. citizens.commands.npc.skin.missing-skin=A skin name is required. +citizens.commands.npc.skin.fetching=Attempting to generate skin using https://mineskin.org +citizens.commands.npc.skin.invalid-file=Skin file [[{0}]] not found. Must be a file under plugins/Citizens2/skins/ citizens.commands.npc.skin.cleared=[[{0}]]''s skin name was cleared. citizens.commands.npc.skin.layers-set=[[{0}]]''s skin layers: cape - [[{1}]], hat - [[{2}]], jacket - [[{3}]], sleeves - [[{4}]], pants - [[{5}]]. citizens.commands.npc.size.description=[[{0}]]''s size is [[{1}]].