diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java index c4d1f9a..42d7624 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java @@ -93,7 +93,7 @@ public class ZNpcsPlus { packetEvents.load(); configManager = new ConfigManager(getDataFolder()); - skinCache = new MojangSkinCache(configManager); + skinCache = new MojangSkinCache(configManager, new File(getDataFolder(), "skins")); propertyRegistry = new EntityPropertyRegistryImpl(skinCache, configManager); NpcPropertyRegistryProvider.register(propertyRegistry); 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 dc9832f..485b866 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java @@ -17,10 +17,14 @@ import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import java.io.File; +import java.io.FileNotFoundException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public class SkinCommand implements CommandHandler { private final MojangSkinCache skinCache; @@ -90,6 +94,30 @@ public class SkinCommand implements CommandHandler { context.send(Component.text("Invalid url!", NamedTextColor.RED)); } return; + } else if (type.equalsIgnoreCase("file")) { + context.ensureArgsNotEmpty(); + String path = context.dumpAllArgs(); + context.send(Component.text("Fetching skin from file \"" + path + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.fromFile(skinCache, path).exceptionally(e -> { + if (e instanceof FileNotFoundException || e.getCause() instanceof FileNotFoundException) { + context.send(Component.text("A file at the specified path could not be found!", NamedTextColor.RED)); + } else { + context.send(Component.text("An error occurred while fetching the skin from file! Check the console for more details.", NamedTextColor.RED)); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + return null; + }).thenAccept(skin -> { + if (skin == null) return; + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the file path 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)); + }); + return; } context.send(Component.text("Unknown skin type! Please use one of the following: mirror, static, dynamic, url")); } @@ -97,11 +125,19 @@ public class SkinCommand implements CommandHandler { @Override public List suggest(CommandContext context) throws CommandExecutionException { if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); - if (context.argSize() == 2) return context.suggestLiteral("mirror", "static", "dynamic", "url"); + if (context.argSize() == 2) return context.suggestLiteral("mirror", "static", "dynamic", "url", "file"); if (context.matchSuggestion("*", "static")) return context.suggestPlayers(); if (context.argSize() == 3 && context.matchSuggestion("*", "url")) { return context.suggestLiteral("slim", "classic"); } + if (context.argSize() == 3 && context.matchSuggestion("*", "file")) { + if (skinCache.getSkinsFolder().exists()) { + File[] files = skinCache.getSkinsFolder().listFiles(); + if (files != null) { + return Arrays.stream(files).map(File::getName).collect(Collectors.toList()); + } + } + } return Collections.emptyList(); } } 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 2a54509..faef8f5 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 @@ -14,6 +14,7 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -26,9 +27,12 @@ public class MojangSkinCache { private final Map cache = new ConcurrentHashMap<>(); private final Map idCache = new ConcurrentHashMap<>(); + private final File skinsFolder; - public MojangSkinCache(ConfigManager configManager) { + public MojangSkinCache(ConfigManager configManager, File skinsFolder) { this.configManager = configManager; + this.skinsFolder = skinsFolder; + if (!skinsFolder.exists()) skinsFolder.mkdirs(); } public void cleanCache() { @@ -142,6 +146,58 @@ public class MojangSkinCache { }); } + public CompletableFuture fetchFromFile(String path) throws FileNotFoundException { + File file = new File(skinsFolder, path); + if (!file.exists()) throw new FileNotFoundException("File not found: " + path); + return CompletableFuture.supplyAsync(() -> { + URL apiUrl = parseUrl("https://api.mineskin.org/generate/upload"); + HttpURLConnection connection = null; + try { + String boundary = "*****"; + String CRLF = "\r\n"; + + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + connection.setUseCaches(false); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); + connection.setDoInput(true); + connection.setDoOutput(true); + OutputStream outputStream = connection.getOutputStream(); + DataOutputStream out = new DataOutputStream(outputStream); + out.writeBytes("--" + boundary + CRLF); + out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"" + CRLF); + out.writeBytes("Content-Type: image/png" + CRLF); + out.writeBytes(CRLF); + out.write(Files.readAllBytes(file.toPath())); + out.writeBytes(CRLF); + out.writeBytes("--" + boundary + "--" + CRLF); + out.flush(); + out.close(); + outputStream.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 SkinImpl(texture.get("value").getAsString(), texture.get("signature").getAsString()); + } + + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get skin from file:"); + 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; @@ -213,4 +269,8 @@ public class MojangSkinCache { throw new RuntimeException(exception); } } + + public File getSkinsFolder() { + return skinsFolder; + } } 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 c852080..24cd80c 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.SkinImpl; import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; import org.bukkit.entity.Player; +import java.io.FileNotFoundException; import java.net.URL; import java.util.concurrent.CompletableFuture; @@ -25,6 +26,16 @@ public class PrefetchedDescriptor implements BaseSkinDescriptor, SkinDescriptor return CompletableFuture.supplyAsync(() -> new PrefetchedDescriptor(cache.fetchByUrl(url, variant).join())); } + public static CompletableFuture fromFile(MojangSkinCache cache, String path) { + return CompletableFuture.supplyAsync(() -> { + try { + return new PrefetchedDescriptor(cache.fetchFromFile(path).join()); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + }); + } + @Override public CompletableFuture fetch(Player player) { return CompletableFuture.completedFuture(skin);