diff --git a/java/com/autcraft/aac/AAC.java b/java/com/autcraft/aac/AAC.java deleted file mode 100644 index fd47172..0000000 --- a/java/com/autcraft/aac/AAC.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.autcraft.aac; - -import com.autcraft.aac.commands.MainCommand; -import com.autcraft.aac.events.Click; -import com.autcraft.aac.objects.InventoryGUI; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.TextColor; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -public final class AAC extends JavaPlugin { - private Map stringMap = new HashMap<>(); - private InventoryGUI inventoryGUI; - private Cache cooldown; - - @Override - public void onEnable() { - // Plugin startup logic - saveDefaultConfig(); - - // Set cooldown timer based on config setting in config.yml - int cooldown_timer = getConfig().getInt("settings.cooldown_in_seconds",5); - cooldown = CacheBuilder.newBuilder().expireAfterWrite(cooldown_timer, TimeUnit.SECONDS).build(); - - // Initialize our Inventory GUI - inventoryGUI = new InventoryGUI(this, "AAC"); - - // Set the strings - initializeStringMap(); - - // Set commands - getCommand("aac").setExecutor(new MainCommand(this)); - - // Register events - getServer().getPluginManager().registerEvents(new Click(this), this); - - getLogger().info("AAC - Augmentative and Alternative Communication initialized"); - } - - @Override - public void onDisable() { - // Plugin shutdown logic - getLogger().info("AAC is no longer available for communicating."); - } - - /** - * Reload information from config.yml - */ - public void reload(){ - reloadConfig(); - initializeStringMap(); - inventoryGUI.reload(); - saveConfig(); - } - - - /** - * Reference to the inventory GUI - * - * @return - */ - public InventoryGUI getInventoryGUI(){ - return this.inventoryGUI; - } - - /** - * Retrieve and store strings from config.yml - * - */ - public void initializeStringMap(){ - // Loop over strings section of config.yml - for( String path : getConfig().getConfigurationSection("strings").getKeys(false) ){ - stringMap.put(path, getConfig().getString("strings." + path, "String not found: " + path)); - } - debug("Strings initialized from config."); - } - - /** - * Return the string from config.yml corresponding to "key" - * - * @return - */ - public String getString(String key){ - return stringMap.get(key); - } - - /** - * Clear the string map for config reload purpoases - * - */ - public void clearStringMap(){ - stringMap.clear(); - } - - /** - * Return true or false if player is still within the cooldown cache - * - * @param player - * @return - */ - public boolean isInCooldown(Player player){ - return cooldown.asMap().containsKey(player.getUniqueId()); - } - - /** - * Get the amount of time remaining before next message can be sent - * - * @param player - * @return - */ - public long getCooldownRemaining(Player player){ - return TimeUnit.MILLISECONDS.toSeconds(cooldown.asMap().get(player.getUniqueId()) - System.currentTimeMillis()); - } - - /** - * Add player to the cooldown cache - * - * @param player - */ - public void addPlayerCooldown(Player player){ - int cooldown_in_seconds = getConfig().getInt("settings.cooldown_in_seconds") * 1000; - cooldown.put(player.getUniqueId(), System.currentTimeMillis() + cooldown_in_seconds); - } - - public void debug(String string){ - if( getConfig().getBoolean("settings.debug") ) - toConsole(string); - } - - /** - * Send output directly to console regardless of debugging settings - * - * @param string - */ - public void toConsole(String string){ - getLogger().info(string); - } - - /** - * Returns a text Component with the given string from config.yml - * - * @param errorString - * @return - */ - public Component errorMessage(String errorString){ - return Component.text(getString(errorString)).color(TextColor.color(190, 0, 0)); - } - - /** - * Returns a text Component with the given string but also replaces some text, based on replacements hashmap - * - * @param errorString - * @param replacements - * @return - */ - public Component errorMessage(String errorString, HashMap replacements){ - String returnMessage = getString(errorString); - for( Map.Entry set : replacements.entrySet() ){ - returnMessage = returnMessage.replace(set.getKey(), set.getValue()); - } - - return Component.text(returnMessage).color(TextColor.color(190, 0, 0)); - } -} diff --git a/java/com/autcraft/aac/CreatePlayerHead.java b/java/com/autcraft/aac/CreatePlayerHead.java deleted file mode 100644 index 76680ed..0000000 --- a/java/com/autcraft/aac/CreatePlayerHead.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.autcraft.aac; - -import net.kyori.adventure.text.Component; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.profile.PlayerProfile; -import org.bukkit.profile.PlayerTextures; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.ParseException; -import java.util.Base64; -import java.util.List; -import java.util.Scanner; -import java.util.UUID; - -public class CreatePlayerHead { - - JSONParser PARSER = new JSONParser(); - - /** - * Retrieve a player head just from a player's name - * - * @param playerName - * @return - */ - public ItemStack getSkull(String playerName, List lore) { - String uuid = null; - String texture = null; - try { - // Retrieve the player's UUID if at all possible - // If it fails, that player probably doesn't exist - uuid = getUUIDFromMojangByName(playerName); - } catch (IOException | ParseException e) { - return null; - } - - // Somehow mojang returned a blank uuid? - if (uuid == null) { - Bukkit.getLogger().info("Error: Could not retrieve UUID for player " + playerName + ". Using a Player Head for now but try \"/aac reload\" and see if it fixes it."); - return null; - } - - // Try to retrieve the texture from Mojang's Session server - // If it fails, it means that Mojang's servers are down. - try { - texture = getSkinTextureByUUID(UUID.fromString(uuid)); - } catch (IOException | org.json.simple.parser.ParseException e) { - return null; - } - - // Now run the main function to return the item stack - return getSkull(UUID.randomUUID(), texture, Component.text(playerName), lore); - } - - public ItemStack getSkull(UUID uuid, String texture, Component customName, List lore) { - // Create the item stack in advance - ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1); - SkullMeta meta = (SkullMeta) skull.getItemMeta(); - String url = null; - - // Depending on the length of the texture string, use the appropriate method to extra the URL - if (texture.length() > 200) { - try { - url = getSkinURLFromMojang(texture); - } catch (UnsupportedEncodingException | org.json.simple.parser.ParseException e) { - Bukkit.getLogger().info("Unable to retrieve URL from " + texture); - } - } else { - url = getSkinURLFromString(texture); - } - - PlayerProfile profile = Bukkit.createPlayerProfile(uuid); - PlayerTextures textures = profile.getTextures(); - URL urlObject; - try { - urlObject = new URL(url); // The URL to the skin, for example: https://textures.minecraft.net/texture/18813764b2abc94ec3c3bc67b9147c21be850cdf996679703157f4555997ea63a - } catch (MalformedURLException exception) { - throw new RuntimeException("Invalid URL", exception); - } - textures.setSkin(urlObject); // Set the skin of the player profile to the URL - profile.setTextures(textures); // Set the textures back to the profile - - // Set all that data to the itemstack metadata - meta.setOwnerProfile(profile); - - // Display name - if (customName != null) { - meta.displayName(customName); - } - - // Lore - if (!lore.isEmpty()) { - meta.lore(lore); - } - - skull.setItemMeta(meta); - - return skull; - } - - /** - * Retrieve the player's UUID from just their name - * - * @param name - * @return - * @throws IOException - * @throws ParseException - */ - public String getUUIDFromMojangByName(String name) throws IOException, ParseException { - String uuid = null; - - // First obvious method is to just get it from the server itself. - Player player = Bukkit.getPlayer(name); - if (player != null) { - return player.getUniqueId().toString(); - } - - // If the server has no record of the player, get it from Mojang's API - URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name); - - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.connect(); - - int responsecode = conn.getResponseCode(); - - if (responsecode != 200) { - //throw new RuntimeException("HttpResponseCode: " + responsecode); - return null; - } else { - - String inline = ""; - Scanner scanner = new Scanner(url.openStream()); - - //Write all the JSON data into a string using a scanner - while (scanner.hasNext()) { - inline += scanner.nextLine(); - } - - //Close the scanner - scanner.close(); - - //Using the JSON simple library parse the string into a json object - JSONParser parse = new JSONParser(); - JSONObject data_obj = null; - try { - data_obj = (JSONObject) parse.parse(inline); - } catch (org.json.simple.parser.ParseException e) { - e.printStackTrace(); - } - - //Get the required object from the above created object - if (data_obj != null) { - uuid = data_obj.get("id").toString(); - } - } - if (uuid != null) { - String result = uuid; - result = uuid.substring(0, 8) + "-" + uuid.substring(8, 12) + "-" + uuid.substring(12, 16) + "-" + uuid.substring(16, 20) + "-" + uuid.substring(20, 32); - uuid = result; - } - - return uuid; - } - - - /** - * If getting a head for a player, we must retrieve the texture from Mojang's Session servers. - * - * @param uuid - * @return - * @throws IOException - * @throws org.json.simple.parser.ParseException - */ - public String getSkinTextureByUUID(UUID uuid) throws IOException, org.json.simple.parser.ParseException { - String texture = null; - String apiURL = "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString(); - - URL url = new URL(apiURL); - - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.connect(); - - int responsecode = conn.getResponseCode(); - - if (responsecode != 200) { - throw new RuntimeException("HttpResponseCode: " + responsecode); - } else { - - String inline = ""; - Scanner scanner = new Scanner(url.openStream()); - - //Write all the JSON data into a string using a scanner - while (scanner.hasNext()) { - inline += scanner.nextLine(); - } - - //Close the scanner - scanner.close(); - - //Using the JSON simple library parse the string into a json object - JSONParser parse = new JSONParser(); - JSONObject data_obj = (JSONObject) parse.parse(inline); - JSONArray propertiesArray = (JSONArray) data_obj.get("properties"); - for (JSONObject property : (List) propertiesArray) { - String name = (String) property.get("name"); - if (name.equals("textures")) { - return (String) property.get("value"); - } - } - - //Get the required object from the above created object - System.out.println(data_obj.values()); - texture = data_obj.get("properties").toString(); - } - - return texture; - } - - /** - * Get Skin URL from string entered into config file - * - * @param base64 - * @return - */ - private String getSkinURLFromString(String base64) { - //String url = Base64.getEncoder().withoutPadding().encodeToString(texture.getBytes()); - Base64.Decoder dec = Base64.getDecoder(); - String decoded = new String(dec.decode(base64)); - - // Should be something like this: - // {"textures":{"SKIN":{"url":"http://textures.minecraft.net/texture/9631597dce4e4051e8d5a543641966ab54fbf25a0ed6047f11e6140d88bf48f"}}} - // System.out.println("URL = " + decoded.substring(28, decoded.length() - 4)); - return decoded.substring(28, decoded.length() - 4); - } - - /** - * Get Skin URL from long string returned by Mojang's API - * - * @param base64 - * @return - * @throws UnsupportedEncodingException - * @throws org.json.simple.parser.ParseException - */ - private String getSkinURLFromMojang(String base64) throws UnsupportedEncodingException, org.json.simple.parser.ParseException { - String texture = null; - String decodedBase64 = new String(Base64.getDecoder().decode(base64), "UTF-8"); - JSONObject base64json = (JSONObject) PARSER.parse(decodedBase64); - JSONObject textures = (JSONObject) base64json.get("textures"); - if (textures.containsKey("SKIN")) { - JSONObject skinObject = (JSONObject) textures.get("SKIN"); - texture = (String) skinObject.get("url"); - } - return texture; - } -} diff --git a/java/com/autcraft/aac/events/Click.java b/java/com/autcraft/aac/events/Click.java deleted file mode 100644 index a4fab08..0000000 --- a/java/com/autcraft/aac/events/Click.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.autcraft.aac.events; - -import com.autcraft.aac.AAC; -import com.autcraft.aac.objects.InventoryGUI; -import net.kyori.adventure.text.Component; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.event.inventory.InventoryAction; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.HashMap; - -public class Click implements Listener { - AAC plugin; - - public Click(AAC plugin) { - this.plugin = plugin; - } - - /** - * Tool item (book) to open GUI - * - * @param e - */ - @EventHandler - public void onClick(PlayerInteractEvent e) { - Player player = (Player) e.getPlayer(); - InventoryGUI inventoryGUI = plugin.getInventoryGUI(); - ItemStack item = player.getInventory().getItemInMainHand(); - ItemStack itemOffHand = player.getInventory().getItemInOffHand(); - - // The AAC tool has to be in either the main hand or offhand to work - boolean validItem = inventoryGUI.isItemPanelTool(item) || inventoryGUI.isItemPanelTool(itemOffHand); - - // Check to see if the item in the player's hand is the AAC Tool - // Make sure it's not a physical action, such as stepping onto a pressure plate - if (validItem && e.getAction() != Action.PHYSICAL) { - // If, for whatever reason, the player doesn't have permission to open the gui - if (!player.hasPermission("aac.open")) { - player.sendMessage(plugin.getString("error_no_permission")); - return; - } - - player.openInventory(inventoryGUI.getGUI(player, 1)); - - e.setCancelled(true); - } - } - - /** - * Clicking an item in the GUI inventory screen - * - * @param e - */ - @EventHandler - public void inventoryClick(InventoryClickEvent e) { - - Component inventoryTitle = e.getView().title(); - Component GUITitle = Component.text(plugin.getConfig().getString("settings.title")); - - // If the inventory clicked on is the same as what was created by AAC - if (inventoryTitle.equals(GUITitle)) { - // Disable all number key interactions when this menu is open. NO SWITCHING ITEMS! - if (e.getClick() == ClickType.NUMBER_KEY) { - e.setCancelled(true); - return; - } - - // If the item clicked is not null and not air - if (e.getCurrentItem() != null && e.getCurrentItem().getType() != Material.AIR ) { - - // Object vars - Player player = (Player) e.getWhoClicked(); - ItemStack clickedItem = e.getCurrentItem(); - InventoryGUI inventoryGUI = plugin.getInventoryGUI(); - - // If the next button is clicked - if (inventoryGUI.isNextButton(clickedItem)) { - // Open GUI for the next page - player.openInventory(inventoryGUI.getGUI(player, inventoryGUI.getNextPage(clickedItem))); - } - // If the previous button is clicked - else if (inventoryGUI.isPreviousButton(clickedItem)) { - // Open GUI for the previous page - player.openInventory(inventoryGUI.getGUI(player, inventoryGUI.getPreviousPage(clickedItem))); - } - // Otherwise, output to the chat - else { - - // If it has persistent data, then it has the string to output - String output = inventoryGUI.getPersistentDataContainer(clickedItem); - - // So long as the output isn't blank, send it to the chat - if (output != null) { - if (plugin.isInCooldown(player)) { - HashMap replacements = new HashMap<>(); - replacements.put("{SECONDS}", "" + plugin.getCooldownRemaining(player)); - - player.sendMessage(plugin.errorMessage("error_player_in_cooldown", replacements)); - } else { - plugin.toConsole(player.getName() + " is using AAC to generate the following text in chat:"); - player.chat(output); - - // Message sent successfully, now apply cooldown - plugin.addPlayerCooldown(player); - } - } else { - plugin.toConsole("Error: No output was stored because the plugin could not set the persistent data for the panel option."); - } - } - - e.setCancelled(true); - player.updateInventory(); - } - } - } - - public boolean compareMaterials(Material material, String icon) { - Material panelIcon = Material.getMaterial(icon.toUpperCase()); - if (panelIcon == null) - return false; - - return material == panelIcon; - } -} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..875298e --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + + com.autcraft + aac + + UNKNOWN + AAC + + + 1.21.1-R0.1-SNAPSHOT + UTF-8 + + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + + + io.papermc.paper + paper-api + ${paper.version} + provided + + + + + + + + + + src/main/resources + true + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + + + + + diff --git a/java/com/autcraft/aac/commands/MainCommand.java b/src/main/java/com/autcraft/aac/AACCommandHandler.java similarity index 59% rename from java/com/autcraft/aac/commands/MainCommand.java rename to src/main/java/com/autcraft/aac/AACCommandHandler.java index c2e406b..18bdf50 100644 --- a/java/com/autcraft/aac/commands/MainCommand.java +++ b/src/main/java/com/autcraft/aac/AACCommandHandler.java @@ -1,9 +1,8 @@ -package com.autcraft.aac.commands; +package com.autcraft.aac; -import com.autcraft.aac.AAC; -import com.autcraft.aac.objects.InventoryGUI; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -12,14 +11,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.TreeSet; -public class MainCommand implements CommandExecutor, TabCompleter { - AAC plugin; +public class AACCommandHandler implements CommandExecutor, TabCompleter { + private final AACPlugin plugin; - public MainCommand(AAC plugin){ + public AACCommandHandler(AACPlugin plugin) { this.plugin = plugin; } @@ -27,22 +26,21 @@ public MainCommand(AAC plugin){ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { // No arguments provided - get help text - if( args.length == 0 ){ + if (args.length == 0) { // Error: Invalid permission - if( !commandSender.hasPermission("aac.help") ) { + if (!commandSender.hasPermission("aac.help")) { commandSender.sendMessage(plugin.errorMessage("error_no_permission")); return true; } - commandSender.sendMessage(Component.text(plugin.getConfig().getString("settings.helptext")).color(TextColor.color(60, 180 ,180))); + commandSender.sendMessage(Component.text(plugin.getConfig().getString("settings.helptext")).color(TextColor.color(60, 180, 180))); return true; } - - // Reload the config and re-initalize the panel items - if( args[0].equalsIgnoreCase("reload") ){ + // Reload the config and re-initialize the panel items + if (args[0].equalsIgnoreCase("reload")) { // Error: Invalid permission - if( !commandSender.hasPermission("aac.reload") ) { + if (!commandSender.hasPermission("aac.reload")) { commandSender.sendMessage(plugin.errorMessage("error_no_permission")); return true; } @@ -54,73 +52,90 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - - // Get the knowledge book! - if( args[0].equalsIgnoreCase("get") ){ + // Get the AAC tool + if (args[0].equalsIgnoreCase("get")) { // Error: Invalid permission - if( !commandSender.hasPermission("aac.get") ){ + if (!commandSender.hasPermission("aac.get")) { commandSender.sendMessage(plugin.errorMessage("error_no_permission")); return true; } // If player - if( commandSender instanceof Player) { - Player player = (Player) commandSender; + if (commandSender instanceof Player player) { InventoryGUI inventoryGUI = plugin.getInventoryGUI(); // Put the item into the player's inventory that will trigger the AAC GUI player.getInventory().addItem(inventoryGUI.getTool()); plugin.debug("Gave AAC tool to " + player.getName()); - } - else { + } else { commandSender.sendMessage(plugin.errorMessage("error_no_console")); return true; } } - // If the player is trying to give the book to another player - if( args[0].equalsIgnoreCase("give") ){ + // Give the tool to a player + if (args[0].equalsIgnoreCase("give")) { // Error: Invalid permission - if( !commandSender.hasPermission("aac.reload") ) { + if (!commandSender.hasPermission("aac.give")) { commandSender.sendMessage(plugin.errorMessage("error_no_permission")); return true; } // Error: /aac give command ran but no player provided. - if( args.length == 1 ){ + if (args.length == 1) { commandSender.sendMessage(plugin.errorMessage("error_player_not_provided")); - return true; + return false; } + // Get player based on args[1] given in command + Player player = Bukkit.getPlayerExact(args[1]); // Error: /aac give command ran but player is not online - if( plugin.getServer().getPlayer(args[1]) == null ){ + if (player == null) { commandSender.sendMessage(plugin.errorMessage("error_player_not_online")); return true; } - // Get player based on args[1] given in command - Player player = plugin.getServer().getPlayer(args[1]); InventoryGUI inventoryGUI = plugin.getInventoryGUI(); player.getInventory().addItem(inventoryGUI.getTool()); commandSender.sendMessage(Component.text(plugin.getString("success_tool_given_to_player")).color(TextColor.color(60, 180, 180))); + return true; } return false; } @Override public @Nullable List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { - List options = new ArrayList<>(); + return switch (args.length) { + case 0 -> onTabCompleteNoArgs(commandSender); + case 1 -> onTabCompleteArgs1(commandSender, args); + case 2 -> onTabCompleteArgs2(args); + default -> null; + }; + } + + private List onTabCompleteNoArgs(CommandSender sender) { + var r = new TreeSet(); + if (sender instanceof Player) r.add("get"); + r.add("give"); + r.add("reload"); + return r.stream().toList(); + } - if( args.length == 1 ){ - options.add("get"); - options.add("reload"); + private List onTabCompleteArgs1(CommandSender sender, String[] args) { + if ("give".equals(args[0])) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(); } - Collections.sort(options); - if(!options.isEmpty()) - return options; + var options = onTabCompleteNoArgs(sender); + return options.contains(args[0]) ? Collections.emptyList() : options; + } - return null; + private List onTabCompleteArgs2(String[] args) { + if (!"give".equals(args[0])) { + return Collections.emptyList(); + } + return Bukkit.getOnlinePlayers().stream().map(Player::getName).filter(s -> s.startsWith(args[1])).sorted().toList(); } + } diff --git a/src/main/java/com/autcraft/aac/AACListener.java b/src/main/java/com/autcraft/aac/AACListener.java new file mode 100644 index 0000000..cc01291 --- /dev/null +++ b/src/main/java/com/autcraft/aac/AACListener.java @@ -0,0 +1,143 @@ +package com.autcraft.aac; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class AACListener implements Listener { + private final AACPlugin plugin; + private final Map cooldown = new ConcurrentHashMap<>(); + + public AACListener(AACPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler(ignoreCancelled = true) + public void playerOpensInventoryGUI(PlayerInteractEvent e) { + if (e.getAction() == Action.PHYSICAL) return; + + Player player = e.getPlayer(); + InventoryGUI inventoryGUI = plugin.getInventoryGUI(); + ItemStack item = player.getInventory().getItemInMainHand(); + ItemStack itemOffHand = player.getInventory().getItemInOffHand(); + + // The AAC tool has to be in either the main hand or offhand to work + var valid = inventoryGUI.isItemPanelTool(item) || inventoryGUI.isItemPanelTool(itemOffHand); + if (!valid) return; + + // If, for whatever reason, the player doesn't have permission to open the gui + if (!player.hasPermission("aac.open")) { + player.sendMessage(plugin.getString("error_no_permission")); + return; + } + + player.openInventory(inventoryGUI.getGUI(player, 1)); + + e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void playerMakesAACSelection(InventoryClickEvent e) { + + // Disable all number key interactions when this menu is open. NO SWITCHING ITEMS! + if (e.getClick() == ClickType.NUMBER_KEY + || e.getCurrentItem() == null + || e.getCurrentItem().getType() == Material.AIR + ) { + e.setCancelled(true); + return; + } + + Component inventoryTitle = e.getView().title(); + Component GUITitle = Component.text(Objects.requireNonNull(plugin.getConfig().getString("settings.title"))); + + if (!inventoryTitle.equals(GUITitle)) return; + + // Object vars + Player player = (Player) e.getWhoClicked(); + ItemStack clickedItem = e.getCurrentItem(); + InventoryGUI inventoryGUI = plugin.getInventoryGUI(); + + // If the next button is clicked + if (inventoryGUI.isNextButton(clickedItem)) { + // Open GUI for the next page + player.openInventory(inventoryGUI.getGUI(player, inventoryGUI.getNextPage(clickedItem))); + } + // If the previous button is clicked + else if (inventoryGUI.isPreviousButton(clickedItem)) { + // Open GUI for the previous page + player.openInventory(inventoryGUI.getGUI(player, inventoryGUI.getPreviousPage(clickedItem))); + } + // Otherwise, output to the chat + else { + + // If it has persistent data, then it has the string to output + String output = inventoryGUI.getPersistentDataContainer(clickedItem); + + // So long as the output isn't blank, send it to the chat + if (output != null) { + if (isInCooldown(player)) { + HashMap replacements = new HashMap<>(); + replacements.put("{SECONDS}", "" + getCooldownRemaining(player)); + + player.sendMessage(plugin.errorMessage("error_player_in_cooldown", replacements)); + } else { + plugin.toConsole(player.getName() + " is using AAC to generate the following text in chat:"); + player.chat(output); + + // Message sent successfully, now apply cooldown + addPlayerCooldown(player); + } + } + } + + e.setCancelled(true); + player.updateInventory(); + + } + + /** + * Return true or false if player is still within the cooldown cache + */ + private boolean isInCooldown(Player player) { + var pid = player.getUniqueId(); + if (cooldown.containsKey(pid)) { + if (cooldown.get(pid) - System.currentTimeMillis() > 0L) { + return true; + } else { + cooldown.remove(pid); + } + } + return false; + } + + /** + * Get the amount of time remaining before next message can be sent + */ + private long getCooldownRemaining(Player player) { + return TimeUnit.MILLISECONDS.toSeconds(cooldown.get(player.getUniqueId()) - System.currentTimeMillis()); + } + + /** + * Add player to the cooldown cache + */ + private void addPlayerCooldown(Player player) { + int cooldown_in_seconds = plugin.getConfig().getInt("settings.cooldown_in_seconds") * 1000; + cooldown.put(player.getUniqueId(), System.currentTimeMillis() + cooldown_in_seconds); + } + +} diff --git a/src/main/java/com/autcraft/aac/AACPlugin.java b/src/main/java/com/autcraft/aac/AACPlugin.java new file mode 100644 index 0000000..1e1e074 --- /dev/null +++ b/src/main/java/com/autcraft/aac/AACPlugin.java @@ -0,0 +1,108 @@ +package com.autcraft.aac; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class AACPlugin extends JavaPlugin { + private final Map stringMap = new HashMap<>(); + private InventoryGUI inventoryGUI; + + @Override + public void onEnable() { + // Plugin startup logic + saveDefaultConfig(); + + // Initialize our Inventory GUI + inventoryGUI = new InventoryGUI(this, "AAC"); + + // Set the strings + initializeStringMap(); + + // Set commands + Objects.requireNonNull(getCommand("aac")).setExecutor(new AACCommandHandler(this)); + + // Register events + getServer().getPluginManager().registerEvents(new AACListener(this), this); + + getLogger().info("AAC - Augmentative and Alternative Communication initialized"); + } + + /** + * Reload information from config.yml + */ + public void reload() { + reloadConfig(); + initializeStringMap(); + inventoryGUI.reload(); + saveConfig(); + } + + /** + * Reference to the inventory GUI + */ + public InventoryGUI getInventoryGUI() { + return this.inventoryGUI; + } + + /** + * Retrieve and store strings from config.yml + */ + public void initializeStringMap() { + var section = getConfig().getConfigurationSection("strings"); + if (section == null) { + getLogger().warning("missing configuration section: strings"); + return; + } + // Loop over strings section of config.yml + var loadedStrings = new HashMap(); + for (String path : section.getKeys(false)) { + loadedStrings.put(path, getConfig().getString("strings." + path, "String not found: " + path)); + } + this.stringMap.clear(); + this.stringMap.putAll(loadedStrings); + debug("Strings initialized from config."); + } + + /** + * Return the string from config.yml corresponding to "key" + */ + public String getString(String key) { + return stringMap.get(key); + } + + public void debug(String string) { + if (getConfig().getBoolean("settings.debug")) + toConsole(string); + } + + /** + * Send output directly to console regardless of debugging settings + */ + public void toConsole(String string) { + getLogger().info(string); + } + + /** + * Returns a text Component with the given string from config.yml + */ + public Component errorMessage(String errorString) { + return Component.text(getString(errorString)).color(TextColor.color(190, 0, 0)); + } + + /** + * Returns a text Component with the given string but also replaces some text, based on replacements hashmap + */ + public Component errorMessage(String errorString, HashMap replacements) { + String returnMessage = getString(errorString); + for (Map.Entry set : replacements.entrySet()) { + returnMessage = returnMessage.replace(set.getKey(), set.getValue()); + } + + return Component.text(returnMessage).color(TextColor.color(190, 0, 0)); + } +} diff --git a/java/com/autcraft/aac/objects/InventoryGUI.java b/src/main/java/com/autcraft/aac/InventoryGUI.java similarity index 81% rename from java/com/autcraft/aac/objects/InventoryGUI.java rename to src/main/java/com/autcraft/aac/InventoryGUI.java index c068684..06f8b31 100644 --- a/java/com/autcraft/aac/objects/InventoryGUI.java +++ b/src/main/java/com/autcraft/aac/InventoryGUI.java @@ -1,9 +1,8 @@ -package com.autcraft.aac.objects; +package com.autcraft.aac; -import com.autcraft.aac.AAC; -import com.autcraft.aac.CreatePlayerHead; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; @@ -17,17 +16,16 @@ import java.util.*; public class InventoryGUI { - AAC plugin; + private final AACPlugin plugin; - private Map panelOptions = new HashMap<>(); - private Map panelTool = new HashMap<>(); - private NamespacedKey namespacedKeyAACTool; - private NamespacedKey namespacedKey; - private NamespacedKey namespacedKeyNext; - private NamespacedKey namespacedKeyPrevious; + private final Map panelOptions = new HashMap<>(); + private final Map panelTool = new HashMap<>(); + private final NamespacedKey namespacedKeyAACTool; + private final NamespacedKey namespacedKey; + private final NamespacedKey namespacedKeyNext; + private final NamespacedKey namespacedKeyPrevious; - - public InventoryGUI(AAC plugin, String namespaceKey) { + public InventoryGUI(AACPlugin plugin, String namespaceKey) { this.plugin = plugin; this.namespacedKeyAACTool = new NamespacedKey(plugin, "AAC_Tool"); this.namespacedKey = new NamespacedKey(plugin, namespaceKey); @@ -61,9 +59,6 @@ public String getPanelToolDescription() { /** * Return true or false if the item being checked has the persistent data container with AAC_Tool value stored. - * - * @param itemStack - * @return */ public boolean isItemPanelTool(ItemStack itemStack) { if (itemStack == null) { @@ -79,15 +74,12 @@ public boolean isItemPanelTool(ItemStack itemStack) { return false; } - return meta.getPersistentDataContainer().get(this.namespacedKeyAACTool, PersistentDataType.STRING).equalsIgnoreCase("AAC_Tool"); + return Objects.requireNonNull(meta.getPersistentDataContainer().get(this.namespacedKeyAACTool, PersistentDataType.STRING)).equalsIgnoreCase("AAC_Tool"); } /** - * Returns the String value of the persistent data container for itemstack specified - * - * @param itemStack - * @return + * Returns the String value of the persistent data container for item stack specified */ public String getPersistentDataContainer(ItemStack itemStack) { ItemMeta meta = itemStack.getItemMeta(); @@ -102,9 +94,6 @@ public String getPersistentDataContainer(ItemStack itemStack) { /** * Set the persistent data container for the item - * - * @param itemStack - * @param output */ private void SetPersistentDataContainer(ItemStack itemStack, String output) { if (itemStack == null) { @@ -112,28 +101,27 @@ private void SetPersistentDataContainer(ItemStack itemStack, String output) { return; } ItemMeta meta = itemStack.getItemMeta(); - PersistentDataContainer dataContainer = meta.getPersistentDataContainer(); meta.getPersistentDataContainer().set(this.namespacedKey, PersistentDataType.STRING, output); itemStack.setItemMeta(meta); } /** - * Iinitialize the panel items by putting the itemstack data into a map + * Initialize the panel items by putting the item stack data into a map */ public void initializePanelOptions() { - plugin.debug("Initirializing Panel from config."); + plugin.debug("Initializing Panel from config."); panelOptions.clear(); // Loop over the panel options in the config - for (String path : plugin.getConfig().getConfigurationSection("panel").getKeys(false)) { - ItemStack itemStack = null; + for (String path : Objects.requireNonNull(plugin.getConfig().getConfigurationSection("panel")).getKeys(false)) { + ItemStack itemStack; String panelItem = "panel." + path; String icon = plugin.getConfig().getString(panelItem + ".icon", ""); String name = plugin.getConfig().getString(panelItem + ".name", ""); String playerName = plugin.getConfig().getString(panelItem + ".player", ""); String texture = plugin.getConfig().getString(panelItem + ".texture", ""); - List lore = new ArrayList(); + List lore = new ArrayList<>(); lore.add(Component.text(plugin.getConfig().getString(panelItem + ".lore", ""))); String output = plugin.getConfig().getString(panelItem + ".output", ""); @@ -145,34 +133,27 @@ public void initializePanelOptions() { } // If material is set to player_head - if (icon.equalsIgnoreCase("PLAYER_HEAD") && (!texture.equals("") || !playerName.equals(""))) { + if (icon.equalsIgnoreCase("PLAYER_HEAD") && (!texture.isEmpty() || !playerName.isEmpty())) { // Prioritize texture. If they entered one, they probably want it. if (!texture.isEmpty()) { // Create the player head with texture and other info - CreatePlayerHead playerHead = new CreatePlayerHead(); - itemStack = playerHead.getSkull(UUID.randomUUID(), texture, Component.text(playerName), lore); - - // If something failed in retrieving the skull, rather than just break completely, give the panel a blank player head - if (itemStack == null) { + try { + itemStack = PlayerHeadUtil.getSkull(UUID.randomUUID(), texture, Component.text(playerName), lore); + } catch (Exception e) { itemStack = new ItemStack(Material.PLAYER_HEAD, 1); } } // Second is player name. If one is set, get the player's currect skin file - else if (!playerName.isEmpty()) { + else { // Create the player head with texture and other info - CreatePlayerHead playerHead = new CreatePlayerHead(); - itemStack = playerHead.getSkull(playerName, lore); + itemStack = PlayerHeadUtil.getSkull(playerName, lore); // If something failed in retrieving the skull, rather than just break completely, give the panel a blank player head if (itemStack == null) { itemStack = new ItemStack(Material.PLAYER_HEAD, 1); } } - // If neither is set, just use the generic player head - else { - itemStack = new ItemStack(Material.PLAYER_HEAD, 1); - } // set Display and lore info SkullMeta meta = (SkullMeta) itemStack.getItemMeta(); @@ -217,9 +198,6 @@ public void initializePanelTool() { /** * Returns the inventory/GUI for the clickable items - * - * @param player - * @return */ public Inventory getGUI(Player player, int page) { int inventorySize = 54; @@ -230,17 +208,15 @@ public Inventory getGUI(Player player, int page) { } int lastIndex = startIndex + endIndex; - ArrayList sortedKeys = new ArrayList(panelOptions.keySet()); + ArrayList sortedKeys = new ArrayList<>(panelOptions.keySet()); Collections.sort(sortedKeys); String title = plugin.getConfig().getString("settings.title"); // Create the inventory - Inventory inventory = plugin.getServer().createInventory(player, inventorySize, title); + Inventory inventory = plugin.getServer().createInventory(player, inventorySize, Component.text(title == null ? "AAC - Communications Panel" : title)); - /* - Loop over panel options to populate inventory - */ + // Loop over panel options to populate inventory int slot = 0; int counter = 0; @@ -268,11 +244,8 @@ public Inventory getGUI(Player player, int page) { return inventory; } - /** * Retrieve the panel tool item to put into the player's inventory when they run the command /aac get - * - * @return */ public ItemStack getTool() { String displayName = panelTool.get("name"); @@ -280,9 +253,9 @@ public ItemStack getTool() { lore.add(Component.text(panelTool.get("lore"))); // Get the material, if there is one. If not, default it to knowledge book. - Material material = Material.getMaterial(panelTool.get("icon")); + Material material = Material.getMaterial(panelTool.get("icon").toUpperCase()); if (material == null) - material = Material.KNOWLEDGE_BOOK; + material = Material.FEATHER; // Get the item stack and set data to it. ItemStack item = new ItemStack(material, 1); @@ -291,6 +264,7 @@ public ItemStack getTool() { // Set display and lore data meta.displayName(Component.text(displayName)); meta.lore(lore); + meta.setEnchantmentGlintOverride(true); // Store persistent data as an identifier to know when this tool is being clicked meta.getPersistentDataContainer().set(this.namespacedKeyAACTool, PersistentDataType.STRING, "AAC_Tool"); @@ -302,8 +276,6 @@ public ItemStack getTool() { /** * Generate and return the Next Button item stack - * - * @return */ public ItemStack getNextButton(int page) { ItemStack itemStack; @@ -315,20 +287,23 @@ public ItemStack getNextButton(int page) { // Default check. We can't have a player head without a texture. // If it is blank, reset the material to a beacon. - if (materialName.equalsIgnoreCase("player_head") && texture.equals("")) { + if (materialName.equalsIgnoreCase("player_head") && (texture == null || texture.isEmpty())) { plugin.debug("Material is set to player head but texture is blank."); - materialName = "beacon"; + return errorItem("unknown player head"); } // Get material, if there is one Material material = Material.getMaterial(materialName.toUpperCase()); + if (material == null) { + return errorItem(materialName); + } + // If material is set to player_head if (materialName.equalsIgnoreCase("PLAYER_HEAD")) { // Create the player head with texture and other info - CreatePlayerHead playerHead = new CreatePlayerHead(); - itemStack = playerHead.getSkull(UUID.randomUUID(), texture, displayName, lore); + itemStack = PlayerHeadUtil.getSkull(UUID.randomUUID(), texture, displayName, lore); } // Any other material @@ -353,8 +328,6 @@ public ItemStack getNextButton(int page) { /** * Generate and return the Previous Button item stack - * - * @return */ public ItemStack getPreviousButton(int page) { ItemStack itemStack; @@ -366,20 +339,23 @@ public ItemStack getPreviousButton(int page) { // Default check. We can't have a player head without a texture. // If it is blank, reset the material to a beacon. - if (materialName.equalsIgnoreCase("player_head") && texture.equals("")) { + if (materialName.equalsIgnoreCase("player_head") && (texture == null || texture.isEmpty())) { plugin.debug(plugin.getString("error_player_head_no_material")); - materialName = "beacon"; + return errorItem("unknown player head"); } // Get material, if there is one Material material = Material.getMaterial(materialName.toUpperCase()); + if (material == null) { + return errorItem(materialName); + } + // If material is set to player_head if (materialName.equalsIgnoreCase("PLAYER_HEAD")) { // Create the player head with texture and other info - CreatePlayerHead playerHead = new CreatePlayerHead(); - itemStack = playerHead.getSkull(UUID.randomUUID(), texture, displayName, lore); + itemStack = PlayerHeadUtil.getSkull(UUID.randomUUID(), texture, displayName, lore); } // Any other material else { @@ -404,15 +380,12 @@ public ItemStack getPreviousButton(int page) { /** * Returns true or false if the persistent data exists within the item stack - * - * @param itemStack - * @return */ public boolean isNextButton(ItemStack itemStack) { return itemStack.getItemMeta().getPersistentDataContainer().has(this.namespacedKeyNext); } - public int getNextPage(ItemStack itemStack) { + public Integer getNextPage(ItemStack itemStack) { return itemStack.getItemMeta().getPersistentDataContainer().get(this.namespacedKeyNext, PersistentDataType.INTEGER); } @@ -420,7 +393,17 @@ public boolean isPreviousButton(ItemStack itemStack) { return itemStack.getItemMeta().getPersistentDataContainer().has(this.namespacedKeyPrevious); } - public int getPreviousPage(ItemStack itemStack) { + public Integer getPreviousPage(ItemStack itemStack) { return itemStack.getItemMeta().getPersistentDataContainer().get(this.namespacedKeyPrevious, PersistentDataType.INTEGER); } + + private ItemStack errorItem(String materialName) { + var item = new ItemStack(Material.BARRIER); + item.lore(List.of( + Component.text("Unknown material: ", TextColor.color(0, 0, 0), TextDecoration.BOLD) + .append(Component.text(materialName)) + )); + return item; + } + } diff --git a/src/main/java/com/autcraft/aac/PlayerHeadUtil.java b/src/main/java/com/autcraft/aac/PlayerHeadUtil.java new file mode 100644 index 0000000..a04f5db --- /dev/null +++ b/src/main/java/com/autcraft/aac/PlayerHeadUtil.java @@ -0,0 +1,278 @@ +package com.autcraft.aac; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * Utility for acquiring player head items with skins loaded from official API. + */ +public class PlayerHeadUtil { + + private static final Logger logger = LoggerFactory.getLogger(PlayerHeadUtil.class); + + private static final JSONParser PARSER = new JSONParser(); + + /** + * Retrieve a player head just from a player's name. + * + * @param playerName the registered name of the player + * @param lore the lore of the returned item + * @return an {@code ItemStack} of the type {@code Material.PLAYER_HEAD} with the skin of the provided player name + * and the provided lore attached or {@code null} if the skin could not be loaded + */ + public static @Nullable ItemStack getSkull(@NotNull String playerName, @NotNull List lore) { + String uuid; + String texture; + try { + // Retrieve the player's UUID if at all possible + // If it fails, that player probably doesn't exist + uuid = getUUIDFromMojangByName(playerName); + } catch (Exception e) { + return null; + } + + // Try to retrieve the texture from Mojang's Session server + // If it fails, it means that Mojang's servers are down. + try { + texture = getSkinTextureByUUID(UUID.fromString(uuid)); + } catch (Exception e) { + logger.warn("Unable to get skin for {}", uuid); + return null; + } + + // Now run the main function to return the item stack + return getSkull(UUID.randomUUID(), texture, Component.text(playerName), lore); + } + + /** + * Retrieve a player head. + * + * @param uuid the registered UUID of the player + * @param texture base64 encoded JSON containing the skin's URL, e.g. + * {@code {"textures":{"SKIN":{"url":"http://textures.minecraft.net/texture/{id}"}}}} + * @param displayName the display name of the returned item + * @param lore the lore of the returned item + * @return an {@code ItemStack} of the type {@code Material.PLAYER_HEAD} with the skin of the provided player and + * the provided options + * @throws RuntimeException if the provided texture is invalid or the skin failed to download + */ + public static @NotNull ItemStack getSkull(@NotNull UUID uuid, @NotNull String texture, @Nullable Component displayName, @NotNull List lore) throws RuntimeException { + // Create the item stack in advance + ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + String url; + + // Depending on the length of the texture string, use the appropriate method to extra the URL + if (texture.length() > 200) { + try { + url = getSkinURLFromMojang(texture); + } catch (UnsupportedEncodingException | ParseException e) { + logger.error("Unable to retrieve URL from {}", texture); + throw new RuntimeException(e); + } + } else { + url = getSkinURLFromString(texture); + } + + PlayerProfile profile = Bukkit.createProfile(uuid); + PlayerTextures textures = profile.getTextures(); + URL urlObject; + try { + urlObject = URI.create(url).toURL(); // The URL to the skin, for example: https://textures.minecraft.net/texture/18813764b2abc94ec3c3bc67b9147c21be850cdf996679703157f4555997ea63a + } catch (MalformedURLException e) { + logger.error("Invalid URL {}", url); + throw new RuntimeException(e); + } + textures.setSkin(urlObject); // Set the skin of the player profile to the URL + profile.setTextures(textures); // Set the textures back to the profile + + // Set all that data to the itemstack metadata + meta.setOwningPlayer(Bukkit.getOfflinePlayer(Objects.requireNonNull(uuid))); + + // Display name + if (displayName != null) { + meta.displayName(displayName); + } + + // Lore + if (!lore.isEmpty()) { + meta.lore(lore); + } + + skull.setItemMeta(meta); + + return skull; + } + + /** + * Retrieve the player's UUID from just their name. + * + * @param name the registered name of the player + * @return the String value of the player's unique id + * @throws IOException if communication with the Mojang API fails + */ + public static @NotNull String getUUIDFromMojangByName(@NotNull String name) throws IOException { + String uuid = null; + + // First obvious method is to just get it from the server itself. + Player player = Bukkit.getPlayer(name); + if (player != null) { + return player.getUniqueId().toString(); + } + + // If the server has no record of the player, get it from Mojang's API + URL url = URI.create("https://api.mojang.com/users/profiles/minecraft/" + name).toURL(); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.connect(); + + validateResponse(conn.getResponseCode()); + + StringBuilder inline = new StringBuilder(); + Scanner scanner = new Scanner(url.openStream()); + + //Write all the JSON data into a string using a scanner + while (scanner.hasNext()) { + inline.append(scanner.nextLine()); + } + + //Close the scanner + scanner.close(); + + //Using the JSON simple library parse the string into a json object + JSONObject data_obj; + try { + data_obj = (JSONObject) PARSER.parse(String.valueOf(inline)); + } catch (ParseException e) { + logger.error("Could not parse API response"); + throw new RuntimeException(e); + } + + //Get the required object from the above created object + if (data_obj != null) { + uuid = data_obj.get("id").toString(); + } + + if (uuid != null) { + String result; + result = uuid.substring(0, 8) + "-" + uuid.substring(8, 12) + "-" + uuid.substring(12, 16) + "-" + uuid.substring(16, 20) + "-" + uuid.substring(20, 32); + uuid = result; + } else { + logger.error("could not determine id of {}", name); + throw new RuntimeException("uuid unresolved"); + } + + return uuid; + } + + + /** + * Retrieve the skin of the player with the provided id from Mojang's Session servers. + * + * @param uuid the registered id of the player + * @return the encoded skin data + */ + public static @NotNull String getSkinTextureByUUID(@NotNull UUID uuid) throws IOException, ParseException { + String texture; + String apiURL = "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid; + + URL url = URI.create(apiURL).toURL(); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.connect(); + + validateResponse(conn.getResponseCode()); + + StringBuilder inline = new StringBuilder(); + Scanner scanner = new Scanner(url.openStream()); + + //Write all the JSON data into a string using a scanner + while (scanner.hasNext()) { + inline.append(scanner.nextLine()); + } + + //Close the scanner + scanner.close(); + + //Using the JSON simple library parse the string into a json object + JSONParser parse = new JSONParser(); + JSONObject data_obj = (JSONObject) parse.parse(inline.toString()); + JSONArray propertiesArray = (JSONArray) data_obj.get("properties"); + for (JSONObject property : (List) propertiesArray) { + String name = (String) property.get("name"); + if (name.equals("textures")) { + return (String) property.get("value"); + } + } + + //Get the required object from the above created object + texture = data_obj.get("properties").toString(); + + return texture; + } + + /** + * Get Skin URL from string entered into config file + */ + private static @NotNull String getSkinURLFromString(@NotNull String base64) { + //String url = Base64.getEncoder().withoutPadding().encodeToString(texture.getBytes()); + Base64.Decoder dec = Base64.getDecoder(); + String decoded = new String(dec.decode(base64)); + + // Should be something like this: + // {"textures":{"SKIN":{"url":"http://textures.minecraft.net/texture/9631597dce4e4051e8d5a543641966ab54fbf25a0ed6047f11e6140d88bf48f"}}} + // System.out.println("URL = " + decoded.substring(28, decoded.length() - 4)); + return decoded.substring(28, decoded.length() - 4); + } + + /** + * Get Skin URL from long string returned by Mojang's API + */ + private static @NotNull String getSkinURLFromMojang(@NotNull String base64) throws UnsupportedEncodingException, ParseException { + String texture = null; + String decodedBase64 = new String(Base64.getDecoder().decode(base64), StandardCharsets.UTF_8); + JSONObject base64json = (JSONObject) PARSER.parse(decodedBase64); + JSONObject textures = (JSONObject) base64json.get("textures"); + if (textures.containsKey("SKIN")) { + JSONObject skinObject = (JSONObject) textures.get("SKIN"); + texture = (String) skinObject.get("url"); + } + assert texture != null; // it is assumed that the API response guarantees the field's presence + return texture; + } + + private static void validateResponse(int response) { + if (response != 200) { + logger.error("API connection not OK with response {}", response); + throw new RuntimeException(); + } + } + + private PlayerHeadUtil() {} + +} diff --git a/resources/config.yml b/src/main/resources/config.yml similarity index 99% rename from resources/config.yml rename to src/main/resources/config.yml index 282c05a..5c82a97 100644 --- a/resources/config.yml +++ b/src/main/resources/config.yml @@ -17,7 +17,7 @@ settings: # Tool to use to get the player started # This will be an item that they can hold in their hand to click and open the GUI tool: - icon: knowledge_book + icon: feather name: Open AAC Interface lore: Click to open Augmentative and Alternative Communication Graphical Interface diff --git a/resources/plugin.yml b/src/main/resources/plugin.yml similarity index 88% rename from resources/plugin.yml rename to src/main/resources/plugin.yml index 523cf6d..ca68157 100644 --- a/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,8 +1,7 @@ name: AAC version: '${version}' -main: com.autcraft.aac.AAC -api-version: '1.20' -load: STARTUP +main: com.autcraft.aac.AACPlugin +api-version: '1.21' commands: aac: description: Main command