diff --git a/pom.xml b/pom.xml index d18b9e99..2e77284f 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ org.spigotmc spigot - 1.14-pre5-2 + 1.14 diff --git a/src/main/java/com/songoda/update/Plugin.java b/src/main/java/com/songoda/update/Plugin.java index f93ed946..1d3a79a5 100644 --- a/src/main/java/com/songoda/update/Plugin.java +++ b/src/main/java/com/songoda/update/Plugin.java @@ -13,6 +13,8 @@ public class Plugin { private List modules = new ArrayList<>(); private String latestVersion; private String notification; + private String changeLog; + private String marketplaceLink; private JSONObject json; public Plugin(JavaPlugin javaPlugin, int songodaId) { @@ -36,6 +38,22 @@ public class Plugin { this.notification = notification; } + public String getChangeLog() { + return changeLog; + } + + public void setChangeLog(String changeLog) { + this.changeLog = changeLog; + } + + public String getMarketplaceLink() { + return marketplaceLink; + } + + public void setMarketplaceLink(String marketplaceLink) { + this.marketplaceLink = marketplaceLink; + } + public JSONObject getJson() { return json; } diff --git a/src/main/java/com/songoda/update/SongodaUpdate.java b/src/main/java/com/songoda/update/SongodaUpdate.java index 20b78af4..b85ac2ed 100644 --- a/src/main/java/com/songoda/update/SongodaUpdate.java +++ b/src/main/java/com/songoda/update/SongodaUpdate.java @@ -1,6 +1,9 @@ package com.songoda.update; +import com.songoda.update.command.CommandManager; import com.songoda.update.listeners.LoginListener; +import com.songoda.update.utils.ServerVersion; +import org.apache.commons.lang.ArrayUtils; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import org.json.simple.JSONObject; @@ -17,63 +20,85 @@ import java.util.List; public class SongodaUpdate { + private static String prefix = "[SongodaUpdate] "; + + private ServerVersion serverVersion = ServerVersion.fromPackageName(Bukkit.getServer().getClass().getPackage().getName()); + + private static int version = 1; private static List registeredPlugins = new ArrayList<>(); private static SongodaUpdate INSTANCE; + private static JavaPlugin hijackedPlugin; + public SongodaUpdate() { - JavaPlugin hijackedPlugin = registeredPlugins.get(0).getJavaPlugin(); + hijackedPlugin = registeredPlugins.get(0).getJavaPlugin(); Bukkit.getPluginManager().registerEvents(new LoginListener(this), hijackedPlugin); - Bukkit.getScheduler().scheduleSyncDelayedTask(hijackedPlugin, this::update, 20L); + + new CommandManager(this); } - private void update() { - for (Plugin plugin : registeredPlugins) { - try { - JavaPlugin javaPlugin = plugin.getJavaPlugin(); - System.out.println("Establishing connection with the Songoda update server."); - URL url = new URL("http://update.songoda.com/index.php?plugin=" + javaPlugin.getName() + - "&version=" + javaPlugin.getDescription().getVersion()); - URLConnection urlConnection = url.openConnection(); - InputStream is = urlConnection.getInputStream(); - InputStreamReader isr = new InputStreamReader(is); + private void update(Plugin plugin) { + try { + URL url = new URL("http://update.songoda.com/index.php?plugin=" + plugin.getSongodaId() + + "&version=" + plugin.getJavaPlugin().getDescription().getVersion()); + URLConnection urlConnection = url.openConnection(); + InputStream is = urlConnection.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); - int numCharsRead; - char[] charArray = new char[1024]; - StringBuffer sb = new StringBuffer(); - while ((numCharsRead = isr.read(charArray)) > 0) { - sb.append(charArray, 0, numCharsRead); - } - String jsonString = sb.toString(); - JSONObject json = (JSONObject) new JSONParser().parse(jsonString); - - plugin.setLatestVersion((String) json.get("latestVersion")); - plugin.setNotification((String) json.get("notification")); - - plugin.setJson(json); - - for (Module module : plugin.getModules()) { - module.run(plugin); - } - } catch (IOException e) { - System.out.println("Connection failed..."); - e.printStackTrace(); //ToDo: This cannot be here in final. - } catch (ParseException e) { - System.out.println("Failed to parse json."); - e.printStackTrace(); //ToDo: This cannot be here in final. + int numCharsRead; + char[] charArray = new char[1024]; + StringBuffer sb = new StringBuffer(); + while ((numCharsRead = isr.read(charArray)) > 0) { + sb.append(charArray, 0, numCharsRead); } + String jsonString = sb.toString(); + JSONObject json = (JSONObject) new JSONParser().parse(jsonString); + + plugin.setLatestVersion((String) json.get("latestVersion")); + plugin.setMarketplaceLink((String) json.get("link")); + plugin.setNotification((String) json.get("notification")); + plugin.setChangeLog((String) json.get("changeLog")); + + plugin.setJson(json); + + for (Module module : plugin.getModules()) { + module.run(plugin); + } + } catch (IOException e) { + System.out.println("Connection with Songoda servers failed..."); + e.printStackTrace(); //ToDo: This cannot be here in final. + } catch (ParseException e) { + System.out.println("Failed to parse json."); + e.printStackTrace(); //ToDo: This cannot be here in final. } } public static Plugin load(Plugin plugin) { registeredPlugins.add(plugin); - System.out.println("Hooked " + plugin.getJavaPlugin().getName() + "."); + System.out.println(prefix + "Hooked " + plugin.getJavaPlugin().getName() + "."); if (INSTANCE == null) INSTANCE = new SongodaUpdate(); + getInstance().update(plugin); return plugin; } + public ServerVersion getServerVersion() { + return serverVersion; + } + + public boolean isServerVersion(ServerVersion version) { + return serverVersion == version; + } + public boolean isServerVersion(ServerVersion... versions) { + return ArrayUtils.contains(versions, serverVersion); + } + + public boolean isServerVersionAtLeast(ServerVersion version) { + return serverVersion.ordinal() >= version.ordinal(); + } + public List getPlugins() { return new ArrayList<>(registeredPlugins); } @@ -82,6 +107,14 @@ public class SongodaUpdate { return version; } + public String getPrefix() { + return prefix; + } + + public static JavaPlugin getHijackedPlugin() { + return hijackedPlugin; + } + public static SongodaUpdate getInstance() { return INSTANCE; } diff --git a/src/main/java/com/songoda/update/command/AbstractCommand.java b/src/main/java/com/songoda/update/command/AbstractCommand.java new file mode 100644 index 00000000..2146b9ab --- /dev/null +++ b/src/main/java/com/songoda/update/command/AbstractCommand.java @@ -0,0 +1,71 @@ +package com.songoda.update.command; + +import com.songoda.update.SongodaUpdate; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public abstract class AbstractCommand { + + private final boolean noConsole; + private AbstractCommand parent = null; + private boolean hasArgs = false; + private String command; + + private List subCommand = new ArrayList<>(); + + protected AbstractCommand(AbstractCommand parent, boolean noConsole, String... command) { + if (parent != null) { + this.subCommand = Arrays.asList(command); + } else { + this.command = Arrays.asList(command).get(0); + } + this.parent = parent; + this.noConsole = noConsole; + } + + protected AbstractCommand(boolean noConsole, boolean hasArgs, String... command) { + this.command = Arrays.asList(command).get(0); + + this.hasArgs = hasArgs; + this.noConsole = noConsole; + } + + public AbstractCommand getParent() { + return parent; + } + + public String getCommand() { + return command; + } + + public List getSubCommand() { + return subCommand; + } + + public void addSubCommand(String command) { + subCommand.add(command); + } + + protected abstract ReturnType runCommand(SongodaUpdate instance, CommandSender sender, String... args); + + protected abstract List onTab(SongodaUpdate instance, CommandSender sender, String... args); + + public abstract String getPermissionNode(); + + public abstract String getSyntax(); + + public abstract String getDescription(); + + public boolean hasArgs() { + return hasArgs; + } + + public boolean isNoConsole() { + return noConsole; + } + + public enum ReturnType {SUCCESS, FAILURE, SYNTAX_ERROR} +} diff --git a/src/main/java/com/songoda/update/command/CommandManager.java b/src/main/java/com/songoda/update/command/CommandManager.java new file mode 100644 index 00000000..441b4c66 --- /dev/null +++ b/src/main/java/com/songoda/update/command/CommandManager.java @@ -0,0 +1,116 @@ +package com.songoda.update.command; + +import com.songoda.update.SongodaUpdate; +import com.songoda.update.command.commands.CommandDiag; +import com.songoda.update.command.commands.CommandSongoda; +import com.songoda.update.utils.Methods; +import org.bukkit.Bukkit; +import org.bukkit.command.*; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class CommandManager implements CommandExecutor { + + private SongodaUpdate instance; + private TabManager tabManager; + + private List commands = new ArrayList<>(); + + public CommandManager(SongodaUpdate instance) { + this.instance = instance; + this.tabManager = new TabManager(this); + + registerCommandDynamically("songoda", this); + + AbstractCommand commandSongoda = addCommand(new CommandSongoda()); + addCommand(new CommandDiag(commandSongoda)); + + for (AbstractCommand abstractCommand : commands) { + if (abstractCommand.getParent() != null) continue; + //instance.getCommand(abstractCommand.getCommand()).setTabCompleter(tabManager); + } + } + + private AbstractCommand addCommand(AbstractCommand abstractCommand) { + commands.add(abstractCommand); + return abstractCommand; + } + + @Override + public boolean onCommand(CommandSender commandSender, Command command, String s, String[] strings) { + for (AbstractCommand abstractCommand : commands) { + if (abstractCommand.getCommand() != null && abstractCommand.getCommand().equalsIgnoreCase(command.getName().toLowerCase())) { + if (strings.length == 0 || abstractCommand.hasArgs()) { + processRequirements(abstractCommand, commandSender, strings); + return true; + } + } else if (strings.length != 0 && abstractCommand.getParent() != null && abstractCommand.getParent().getCommand().equalsIgnoreCase(command.getName())) { + String cmd = strings[0]; + String cmd2 = strings.length >= 2 ? String.join(" ", strings[0], strings[1]) : null; + for (String cmds : abstractCommand.getSubCommand()) { + if (cmd.equalsIgnoreCase(cmds) || (cmd2 != null && cmd2.equalsIgnoreCase(cmds))) { + processRequirements(abstractCommand, commandSender, strings); + return true; + } + } + } + } + commandSender.sendMessage(instance.getPrefix() + Methods.formatText("&7The command you entered does not exist or is spelt incorrectly.")); + return true; + } + + private void processRequirements(AbstractCommand command, CommandSender sender, String[] strings) { + if (!(sender instanceof Player) && command.isNoConsole()) { + sender.sendMessage("You must be a player to use this command."); + return; + } + if (command.getPermissionNode() == null || sender.hasPermission(command.getPermissionNode())) { + AbstractCommand.ReturnType returnType = command.runCommand(instance, sender, strings); + if (returnType == AbstractCommand.ReturnType.SYNTAX_ERROR) { + sender.sendMessage(instance.getPrefix() + Methods.formatText("&cInvalid Syntax!")); + sender.sendMessage(instance.getPrefix() + Methods.formatText("&7The valid syntax is: &6" + command.getSyntax() + "&7.")); + } + return; + } + sender.sendMessage(instance.getPrefix() + "You do not have permission to run this command."); + } + + public List getCommands() { + return Collections.unmodifiableList(commands); + } + + private void registerCommandDynamically(String command, CommandExecutor executor) { + try { + // Retrieve the SimpleCommandMap from the server + Class classCraftServer = Bukkit.getServer().getClass(); + Field fieldCommandMap = classCraftServer.getDeclaredField("commandMap"); + fieldCommandMap.setAccessible(true); + SimpleCommandMap commandMap = (SimpleCommandMap) fieldCommandMap.get(Bukkit.getServer()); + + // Construct a new Command object + Constructor constructorPluginCommand = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); + constructorPluginCommand.setAccessible(true); + PluginCommand commandObject = constructorPluginCommand.newInstance(command, SongodaUpdate.getHijackedPlugin()); + commandObject.setExecutor(executor); + + // Set tab complete + commandObject.setTabCompleter(tabManager); + + // Register the command + Field fieldKnownCommands = SimpleCommandMap.class.getDeclaredField("knownCommands"); + fieldKnownCommands.setAccessible(true); + Map knownCommands = (Map) fieldKnownCommands.get(commandMap); + knownCommands.put(command, commandObject); + } catch (ReflectiveOperationException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/songoda/update/command/TabManager.java b/src/main/java/com/songoda/update/command/TabManager.java new file mode 100644 index 00000000..b298386a --- /dev/null +++ b/src/main/java/com/songoda/update/command/TabManager.java @@ -0,0 +1,63 @@ +package com.songoda.update.command; + +import com.songoda.update.SongodaUpdate; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; + +import java.util.ArrayList; +import java.util.List; + +public class TabManager implements TabCompleter { + + private final CommandManager commandManager; + + TabManager(CommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] strings) { + for (AbstractCommand abstractCommand : commandManager.getCommands()) { + if (abstractCommand.getCommand() != null && abstractCommand.getCommand().equalsIgnoreCase(command.getName()) && !abstractCommand.hasArgs()) { + if (strings.length == 1) { + List subs = new ArrayList<>(); + for (AbstractCommand ac : commandManager.getCommands()) { + if (ac.getSubCommand() == null) continue; + subs.addAll(ac.getSubCommand()); + } + subs.removeIf(s -> !s.toLowerCase().startsWith(strings[0].toLowerCase())); + return subs; + } + } else if (strings.length != 0 + && abstractCommand.getCommand() != null + && abstractCommand.getCommand().equalsIgnoreCase(command.getName().toLowerCase())) { + String cmd = strings[0]; + String cmd2 = strings.length >= 2 ? String.join(" ", strings[0], strings[1]) : null; + if (abstractCommand.hasArgs()) { + return onCommand(abstractCommand, strings, sender); + } else { + for (String cmds : abstractCommand.getSubCommand()) { + if (cmd.equalsIgnoreCase(cmds) || (cmd2 != null && cmd2.equalsIgnoreCase(cmds))) { + return onCommand(abstractCommand, strings, sender); + } + } + } + } + } + return null; + } + + + private List onCommand(AbstractCommand abstractCommand, String[] strings, CommandSender sender) { + List list = abstractCommand.onTab(SongodaUpdate.getInstance(), sender, strings); + String str = strings[strings.length - 1]; + if (list != null && str != null && str.length() >= 1) { + try { + list.removeIf(s -> !s.toLowerCase().startsWith(str.toLowerCase())); + } catch (UnsupportedOperationException ignored) { + } + } + return list; + } +} diff --git a/src/main/java/com/songoda/update/command/commands/CommandDiag.java b/src/main/java/com/songoda/update/command/commands/CommandDiag.java new file mode 100644 index 00000000..0b1309fc --- /dev/null +++ b/src/main/java/com/songoda/update/command/commands/CommandDiag.java @@ -0,0 +1,93 @@ +package com.songoda.update.command.commands; + +import com.songoda.update.Plugin; +import com.songoda.update.SongodaUpdate; +import com.songoda.update.command.AbstractCommand; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.DecimalFormat; +import java.util.List; + +public class CommandDiag extends AbstractCommand { + + private final String name = Bukkit.getServer().getClass().getPackage().getName(); + private final String version = name.substring(name.lastIndexOf('.') + 1); + + private final DecimalFormat format = new DecimalFormat("##.##"); + + private Object serverInstance; + private Field tpsField; + + + public CommandDiag(AbstractCommand parent) { + super(parent, false, "diag"); + + try { + serverInstance = getNMSClass("MinecraftServer").getMethod("getServer").invoke(null); + tpsField = serverInstance.getClass().getField("recentTps"); + } catch (NoSuchFieldException | SecurityException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + } + + } + + @Override + protected ReturnType runCommand(SongodaUpdate instance, CommandSender sender, String... args) { + + sender.sendMessage(""); + sender.sendMessage("Songoda Diagnostics Information"); + sender.sendMessage(""); + sender.sendMessage("Plugins:"); + for (Plugin plugin : instance.getPlugins()) { + sender.sendMessage(plugin.getJavaPlugin().getName() + + " (" + plugin.getJavaPlugin().getDescription().getVersion() + ")"); + } + sender.sendMessage(""); + sender.sendMessage("Server Version: " + Bukkit.getVersion()); + sender.sendMessage("Operating System: " + System.getProperty("os.name")); + sender.sendMessage("Allocated Memory: " + format.format(Runtime.getRuntime().maxMemory() / (1024 * 1024)) + "Mb"); + sender.sendMessage("Online Players: " + Bukkit.getOnlinePlayers().size()); + try { + double[] tps = ((double[]) tpsField.get(serverInstance)); + + sender.sendMessage("TPS from last 1m, 5m, 15m: " + format.format(tps[0]) + ", " + + format.format(tps[1]) + ", " + format.format(tps[2])); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + return ReturnType.SUCCESS; + } + + @Override + protected List onTab(SongodaUpdate instance, CommandSender sender, String... args) { + return null; + } + + @Override + public String getPermissionNode() { + return "songoda.admin"; + } + + @Override + public String getSyntax() { + return "/songoda diag"; + } + + @Override + public String getDescription() { + return "Display diagnostics information."; + } + + private Class getNMSClass(String className) { + try { + return Class.forName("net.minecraft.server." + version + "." + className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/songoda/update/command/commands/CommandSongoda.java b/src/main/java/com/songoda/update/command/commands/CommandSongoda.java new file mode 100644 index 00000000..81289ffa --- /dev/null +++ b/src/main/java/com/songoda/update/command/commands/CommandSongoda.java @@ -0,0 +1,43 @@ +package com.songoda.update.command.commands; + +import com.songoda.update.SongodaUpdate; +import com.songoda.update.command.AbstractCommand; +import com.songoda.update.gui.GUIOverview; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; + +public class CommandSongoda extends AbstractCommand { + + public CommandSongoda() { + super(true, false, "songoda"); + } + + @Override + protected ReturnType runCommand(SongodaUpdate instance, CommandSender sender, String... args) { + new GUIOverview(instance, (Player) sender); + return ReturnType.SUCCESS; + } + + @Override + protected List onTab(SongodaUpdate instance, CommandSender sender, String... args) { + return null; + } + + @Override + public String getPermissionNode() { + return "songoda.admin"; + } + + @Override + public String getSyntax() { + return "/songoda"; + } + + @Override + public String getDescription() { + return "Displays this interface."; + } +} diff --git a/src/main/java/com/songoda/update/gui/GUIOverview.java b/src/main/java/com/songoda/update/gui/GUIOverview.java new file mode 100644 index 00000000..14aca399 --- /dev/null +++ b/src/main/java/com/songoda/update/gui/GUIOverview.java @@ -0,0 +1,52 @@ +package com.songoda.update.gui; + +import com.songoda.update.Plugin; +import com.songoda.update.SongodaUpdate; +import com.songoda.update.utils.gui.AbstractGUI; +import org.bukkit.Material; +import org.bukkit.entity.Player; + +import java.util.List; + +public class GUIOverview extends AbstractGUI { + + private final SongodaUpdate update; + + public GUIOverview(SongodaUpdate update, Player player) { + super(player); + this.update = update; + + init("Songoda Update", 36); + } + + @Override + protected void constructGUI() { + List plugins = update.getPlugins(); + for (int i = 0; i < plugins.size(); i++) { + Plugin plugin = plugins.get(i); + + createButton(i + 9, Material.STONE, "&6" + plugin.getJavaPlugin().getName(), + "&7Latest Version: " + plugin.getLatestVersion(), + "&7Installed Version: " + plugin.getJavaPlugin().getDescription().getVersion(), + "", + "Change log:", + plugin.getChangeLog(), + "", + "&6Click for the marketplace page link."); + + registerClickable(i + 9, ((player1, inventory1, cursor, slot, type) -> + player.sendMessage(plugin.getMarketplaceLink()))); + + } + } + + @Override + protected void registerClickables() { + + } + + @Override + protected void registerOnCloses() { + + } +} diff --git a/src/main/java/com/songoda/update/utils/AbstractChatConfirm.java b/src/main/java/com/songoda/update/utils/AbstractChatConfirm.java new file mode 100644 index 00000000..77212692 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/AbstractChatConfirm.java @@ -0,0 +1,99 @@ +package com.songoda.update.utils; + +import com.songoda.update.SongodaUpdate; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class AbstractChatConfirm implements Listener { + + private static final List registered = new ArrayList<>(); + + private final Player player; + private final ChatConfirmHandler handler; + + private OnClose onClose = null; + private Listener listener; + + public AbstractChatConfirm(Player player, ChatConfirmHandler hander) { + this.player = player; + this.handler = hander; + player.closeInventory(); + initializeListeners(SongodaUpdate.getHijackedPlugin()); + registered.add(player.getUniqueId()); + } + + public static boolean isRegistered(Player player) { + return registered.contains(player.getUniqueId()); + } + + public static boolean unregister(Player player) { + return registered.remove(player.getUniqueId()); + } + + public void initializeListeners(JavaPlugin plugin) { + + this.listener = new Listener() { + @EventHandler + public void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + if (!AbstractChatConfirm.isRegistered(player)) return; + + AbstractChatConfirm.unregister(player); + event.setCancelled(true); + + ChatConfirmEvent chatConfirmEvent = new ChatConfirmEvent(player, event.getMessage()); + + handler.onChat(chatConfirmEvent); + + if (onClose != null) { + onClose.onClose(); + } + HandlerList.unregisterAll(listener); + } + }; + + + Bukkit.getPluginManager().registerEvents(listener, SongodaUpdate.getHijackedPlugin()); + } + + public void setOnClose(OnClose onClose) { + this.onClose = onClose; + } + + public interface ChatConfirmHandler { + void onChat(ChatConfirmEvent event); + } + + public interface OnClose { + void onClose(); + } + + public class ChatConfirmEvent { + + private final Player player; + private final String message; + + public ChatConfirmEvent(Player player, String message) { + this.player = player; + this.message = message; + } + + public Player getPlayer() { + return player; + } + + public String getMessage() { + return message; + } + } + +} diff --git a/src/main/java/com/songoda/update/utils/Methods.java b/src/main/java/com/songoda/update/utils/Methods.java new file mode 100644 index 00000000..3410cd93 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/Methods.java @@ -0,0 +1,19 @@ +package com.songoda.update.utils; + +import org.bukkit.ChatColor; + +public class Methods { + + public static String formatText(String text) { + return formatText(text, false); + } + + public static String formatText(String text, boolean cap) { + if (text == null || text.equals("")) + return ""; + if (cap) + text = text.substring(0, 1).toUpperCase() + text.substring(1); + return ChatColor.translateAlternateColorCodes('&', text); + } + +} diff --git a/src/main/java/com/songoda/update/utils/ServerVersion.java b/src/main/java/com/songoda/update/utils/ServerVersion.java new file mode 100644 index 00000000..e5749c17 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/ServerVersion.java @@ -0,0 +1,27 @@ +package com.songoda.update.utils; + +public enum ServerVersion { + + UNKNOWN("unknown_server_version"), + V1_7("org.bukkit.craftbukkit.v1_7"), + V1_8("org.bukkit.craftbukkit.v1_8"), + V1_9("org.bukkit.craftbukkit.v1_9"), + V1_10("org.bukkit.craftbukkit.v1_10"), + V1_11("org.bukkit.craftbukkit.v1_11"), + V1_12("org.bukkit.craftbukkit.v1_12"), + V1_13("org.bukkit.craftbukkit.v1_13"), + V1_14("org.bukkit.craftbukkit.v1_14"); + + + private final String packagePrefix; + + private ServerVersion(String packagePrefix) { + this.packagePrefix = packagePrefix; + } + + public static ServerVersion fromPackageName(String packageName) { + for (ServerVersion version : values()) + if (packageName.startsWith(version.packagePrefix)) return version; + return ServerVersion.UNKNOWN; + } +} \ No newline at end of file diff --git a/src/main/java/com/songoda/update/utils/gui/AbstractAnvilGUI.java b/src/main/java/com/songoda/update/utils/gui/AbstractAnvilGUI.java new file mode 100644 index 00000000..fc80c7d7 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/gui/AbstractAnvilGUI.java @@ -0,0 +1,304 @@ +package com.songoda.update.utils.gui; + +import com.songoda.update.SongodaUpdate; +import com.songoda.update.utils.version.NMSUtil; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +public class AbstractAnvilGUI { + + private static Class BlockPositionClass; + private static Class PacketPlayOutOpenWindowClass; + private static Class IChatBaseComponentClass; + private static Class ICraftingClass; + private static Class ContainerAnvilClass; + private static Class ChatMessageClass; + private static Class EntityHumanClass; + private static Class ContainerClass; + private static Class ContainerAccessClass; + private static Class WorldClass; + private static Class PlayerInventoryClass; + private static Class ContainersClass; + + private Player player; + private Map items = new HashMap<>(); + private OnClose onClose = null; + private Inventory inv; + private Listener listener; + + private Sound closeSound = Sound.ENTITY_PLAYER_LEVELUP; + + static { + BlockPositionClass = NMSUtil.getNMSClass("BlockPosition"); + PacketPlayOutOpenWindowClass = NMSUtil.getNMSClass("PacketPlayOutOpenWindow"); + IChatBaseComponentClass = NMSUtil.getNMSClass("IChatBaseComponent"); + ICraftingClass = NMSUtil.getNMSClass("ICrafting"); + ContainerAnvilClass = NMSUtil.getNMSClass("ContainerAnvil"); + EntityHumanClass = NMSUtil.getNMSClass("EntityHuman"); + ChatMessageClass = NMSUtil.getNMSClass("ChatMessage"); + ContainerClass = NMSUtil.getNMSClass("Container"); + WorldClass = NMSUtil.getNMSClass("World"); + PlayerInventoryClass = NMSUtil.getNMSClass("PlayerInventory"); + + if (NMSUtil.getVersionNumber() > 13) { + ContainerAccessClass = NMSUtil.getNMSClass("ContainerAccess"); + ContainersClass = NMSUtil.getNMSClass("Containers"); + } + } + + public AbstractAnvilGUI(final Player player, final AnvilClickEventHandler handler) { + this.player = player; + + this.listener = new Listener() { + @EventHandler(priority = EventPriority.LOWEST) + public void onInventoryClick(InventoryClickEvent event) { + if (event.getWhoClicked() instanceof Player) { + + if (event.getInventory().equals(inv)) { + event.setCancelled(true); + + ItemStack item = event.getCurrentItem(); + int slot = event.getRawSlot(); + String name = ""; + + if (item != null) { + if (item.hasItemMeta()) { + ItemMeta meta = item.getItemMeta(); + + if (meta != null && meta.hasDisplayName()) { + name = meta.getDisplayName(); + } + } + } + + AnvilClickEvent clickEvent = new AnvilClickEvent(AnvilSlot.bySlot(slot), name); + + handler.onAnvilClick(clickEvent); + + if (clickEvent.getWillClose()) { + event.getWhoClicked().closeInventory(); + } + + if (clickEvent.getWillDestroy()) { + destroy(); + } + } + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getPlayer() instanceof Player) { + Inventory inv = event.getInventory(); + player.setLevel(player.getLevel() - 1); + if (inv.equals(inv)) { + inv.clear(); + player.playSound(player.getLocation(), closeSound, 1F, 1F); + Bukkit.getScheduler().scheduleSyncDelayedTask(SongodaUpdate.getHijackedPlugin(), () -> { + if (onClose != null) onClose.OnClose(player, inv); + destroy(); + }, 1L); + + } + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerQuit(PlayerQuitEvent event) { + if (event.getPlayer().equals(getPlayer())) { + player.setLevel(player.getLevel() - 1); + destroy(); + } + } + }; + + Bukkit.getPluginManager().registerEvents(listener, SongodaUpdate.getHijackedPlugin()); + } + + public Player getPlayer() { + return player; + } + + public void setSlot(AnvilSlot slot, ItemStack item) { + items.put(slot, item); + } + + public void open() { + player.setLevel(player.getLevel() + 1); + + try { + Object craftPlayer = NMSUtil.getCraftClass("entity.CraftPlayer").cast(player); + Method getHandleMethod = craftPlayer.getClass().getMethod("getHandle"); + Object entityPlayer = getHandleMethod.invoke(craftPlayer); + Object playerInventory = NMSUtil.getFieldObject(entityPlayer, NMSUtil.getField(entityPlayer.getClass(), "inventory", false)); + Object world = NMSUtil.getFieldObject(entityPlayer, NMSUtil.getField(entityPlayer.getClass(), "world", false)); + Object blockPosition = BlockPositionClass.getConstructor(int.class, int.class, int.class).newInstance(0, 0, 0); + + Object container; + + if (NMSUtil.getVersionNumber() > 13) { + container = ContainerAnvilClass + .getConstructor(int.class, PlayerInventoryClass, ContainerAccessClass) + .newInstance(7, playerInventory, ContainerAccessClass.getMethod("at", WorldClass, BlockPositionClass).invoke(null, world, blockPosition)); + } else { + container = ContainerAnvilClass + .getConstructor(PlayerInventoryClass, WorldClass, BlockPositionClass, EntityHumanClass) + .newInstance(playerInventory, world, blockPosition, entityPlayer); + } + + NMSUtil.getField(ContainerClass, "checkReachable", true).set(container, false); + + Method getBukkitViewMethod = container.getClass().getMethod("getBukkitView"); + Object bukkitView = getBukkitViewMethod.invoke(container); + Method getTopInventoryMethod = bukkitView.getClass().getMethod("getTopInventory"); + inv = (Inventory) getTopInventoryMethod.invoke(bukkitView); + + for (AnvilSlot slot : items.keySet()) { + inv.setItem(slot.getSlot(), items.get(slot)); + } + + Method nextContainerCounterMethod = entityPlayer.getClass().getMethod("nextContainerCounter"); + int c = (int) nextContainerCounterMethod.invoke(entityPlayer); + + Constructor chatMessageConstructor = ChatMessageClass.getConstructor(String.class, Object[].class); + Object inventoryTitle = chatMessageConstructor.newInstance("Repairing", new Object[]{}); + + Object packet; + + if (NMSUtil.getVersionNumber() > 13) { + packet = PacketPlayOutOpenWindowClass + .getConstructor(int.class, ContainersClass, IChatBaseComponentClass) + .newInstance(c, ContainersClass.getField("ANVIL").get(null), inventoryTitle); + } else { + packet = PacketPlayOutOpenWindowClass + .getConstructor(int.class, String.class, IChatBaseComponentClass, int.class) + .newInstance(c, "minecraft:anvil", inventoryTitle, 0); + } + + NMSUtil.sendPacket(player, packet); + + Field activeContainerField = NMSUtil.getField(EntityHumanClass, "activeContainer", true); + + if (activeContainerField != null) { + activeContainerField.set(entityPlayer, container); + NMSUtil.getField(ContainerClass, "windowId", true).set(activeContainerField.get(entityPlayer), c); + Method addSlotListenerMethod = activeContainerField.get(entityPlayer).getClass().getMethod("addSlotListener", ICraftingClass); + addSlotListenerMethod.invoke(activeContainerField.get(entityPlayer), entityPlayer); + + if (NMSUtil.getVersionNumber() > 13) { + ContainerClass.getMethod("setTitle", IChatBaseComponentClass).invoke(container, inventoryTitle); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void destroy() { + player = null; + items = null; + + HandlerList.unregisterAll(listener); + + listener = null; + } + + private OnClose getOnClose() { + return onClose; + } + + public void setOnClose(OnClose onClose) { + this.onClose = onClose; + } + + public void setCloseSound(Sound sound) { + closeSound = sound; + } + + public enum AnvilSlot { + INPUT_LEFT(0), + INPUT_RIGHT(1), + OUTPUT(2); + + private int slot; + + AnvilSlot(int slot) { + this.slot = slot; + } + + public static AnvilSlot bySlot(int slot) { + for (AnvilSlot anvilSlot : values()) { + if (anvilSlot.getSlot() == slot) { + return anvilSlot; + } + } + + return null; + } + + public int getSlot() { + return slot; + } + } + + @FunctionalInterface + public interface AnvilClickEventHandler { + void onAnvilClick(AnvilClickEvent event); + } + + public class AnvilClickEvent { + private AnvilSlot slot; + + private String name; + + private boolean close = true; + private boolean destroy = true; + + public AnvilClickEvent(AnvilSlot slot, String name) { + this.slot = slot; + this.name = name; + } + + public AnvilSlot getSlot() { + return slot; + } + + public String getName() { + return name; + } + + public boolean getWillClose() { + return close; + } + + public void setWillClose(boolean close) { + this.close = close; + } + + public boolean getWillDestroy() { + return destroy; + } + + public void setWillDestroy(boolean destroy) { + this.destroy = destroy; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/songoda/update/utils/gui/AbstractGUI.java b/src/main/java/com/songoda/update/utils/gui/AbstractGUI.java new file mode 100644 index 00000000..fc4e3ae5 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/gui/AbstractGUI.java @@ -0,0 +1,254 @@ +package com.songoda.update.utils.gui; + +import com.songoda.update.SongodaUpdate; +import com.songoda.update.utils.Methods; +import com.songoda.update.utils.gui.Clickable; +import com.songoda.update.utils.gui.OnClose; +import com.songoda.update.utils.gui.Range; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.*; + +public abstract class AbstractGUI implements Listener { + + private static boolean listenersInitialized = false; + protected final Player player; + protected Inventory inventory = null; + protected String setTitle = null; + protected boolean cancelBottom = false; + private Map clickables = new HashMap<>(); + private List onCloses = new ArrayList<>(); + private Map draggableRanges = new HashMap<>(); + + public AbstractGUI(Player player) { + this.player = player; + } + + public static void initializeListeners(JavaPlugin plugin) { + if (listenersInitialized) return; + + Bukkit.getPluginManager().registerEvents(new Listener() { + @EventHandler + public void onClickGUI(InventoryClickEvent event) { + Inventory inventory = event.getClickedInventory(); + if (inventory == null) return; + AbstractGUI gui = getGUIFromInventory(inventory); + Player player = (Player) event.getWhoClicked(); + + boolean bottom = false; + + InventoryType type = event.getClickedInventory().getType(); + if (type != InventoryType.CHEST && type != InventoryType.PLAYER) return; + + if (gui == null && event.getWhoClicked().getOpenInventory().getTopInventory() != null) { + Inventory top = event.getWhoClicked().getOpenInventory().getTopInventory(); + gui = getGUIFromInventory(top); + + if (gui != null && gui.cancelBottom) event.setCancelled(true); + bottom = true; + } + + if (gui == null) return; + + if (!bottom) event.setCancelled(true); + + if (!gui.draggableRanges.isEmpty() && !bottom) { + for (Map.Entry entry : gui.draggableRanges.entrySet()) { + Range range = entry.getKey(); + if (range.getMax() == range.getMin() && event.getSlot() == range.getMin() + || event.getSlot() >= range.getMin() && event.getSlot() <= range.getMax()) { + event.setCancelled(!entry.getValue()); + if (!entry.getValue()) break; + } + } + } + + Map entries = new HashMap<>(gui.clickables); + + for (Map.Entry entry : entries.entrySet()) { + Range range = entry.getKey(); + if (range.isBottom() && !bottom || !range.isBottom() && bottom || range.getClickType() != null && range.getClickType() != event.getClick()) + continue; + if (event.getSlot() >= range.getMin() && event.getSlot() <= range.getMax()) { + entry.getValue().Clickable(player, inventory, event.getCursor(), event.getSlot(), event.getClick()); + player.playSound(player.getLocation(), entry.getKey().getOnClickSound(), 1F, 1F); + } + } + } + + @EventHandler + public void onCloseGUI(InventoryCloseEvent event) { + Inventory inventory = event.getInventory(); + AbstractGUI gui = getGUIFromInventory(inventory); + + if (gui == null || gui.inventory == null) return; + + for (OnClose onClose : gui.onCloses) { + onClose.OnClose((Player) event.getPlayer(), inventory); + } + } + + private AbstractGUI getGUIFromInventory(Inventory inventory) { + if (inventory.getHolder() == null) return null; + InventoryHolder holder = inventory.getHolder(); + if (!(holder instanceof GUIHolder)) return null; + + return ((AbstractGUI.GUIHolder) holder).getGUI(); + } + }, plugin); + listenersInitialized = true; + } + + public void init(String title, int slots) { + if (inventory == null + || inventory.getSize() != slots + || ChatColor.translateAlternateColorCodes('&', title) != player.getOpenInventory().getTitle()) { + this.inventory = Bukkit.getServer().createInventory(new GUIHolder(), slots, Methods.formatText(title)); + this.setTitle = Methods.formatText(title); + if (this.clickables.size() == 0) + registerClickables(); + if (this.onCloses.size() == 0) + registerOnCloses(); + } + constructGUI(); + initializeListeners(SongodaUpdate.getHijackedPlugin()); + player.openInventory(inventory); + } + + protected abstract void constructGUI(); + + protected void addDraggable(Range range, boolean option) { + this.draggableRanges.put(range, option); + } + + protected void removeDraggable() { + this.draggableRanges.clear(); + } + + protected abstract void registerClickables(); + + protected abstract void registerOnCloses(); + + protected ItemStack createButton(int slot, Inventory inventory, ItemStack item, String name, String... lore) { + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(Methods.formatText(name)); + if (lore != null && lore.length != 0) { + List newLore = new ArrayList<>(); + for (String line : lore) { + for (String string : line.split("\\s*\\r?\\n\\s*")) { + int lastIndex = 0; + for (int n = 0; n < string.length(); n++) { + if (n - lastIndex < 35) + continue; + + if (string.charAt(n) == ' ') { + newLore.add(Methods.formatText("&7" + string.substring(lastIndex, n).trim())); + lastIndex = n; + } + } + + if (lastIndex - string.length() < 35) + newLore.add(Methods.formatText("&7" + string.substring(lastIndex, string.length()).trim())); + } + } + meta.setLore(newLore); + } + item.setItemMeta(meta); + inventory.setItem(slot, item); + return item; + } + + protected ItemStack createButton(int slot, ItemStack item, String name, ArrayList lore) { + return createButton(slot, inventory, item, name, lore.toArray(new String[0])); + } + + + protected ItemStack createButton(int slot, ItemStack item, String name, String... lore) { + return createButton(slot, inventory, item, name, lore); + } + + protected ItemStack createButton(int slot, Object item, String name, String... lore) { + if (item instanceof ItemStack) + return createButton(slot, inventory, (ItemStack) item, name, lore); + else + return createButton(slot, inventory, (Material) item, name, lore); + } + + protected ItemStack createButton(int slot, Inventory inventory, Material material, String name, String... lore) { + return createButton(slot, inventory, new ItemStack(material), name, lore); + } + + protected ItemStack createButton(int slot, Material material, String name, String... lore) { + return createButton(slot, inventory, new ItemStack(material), name, lore); + } + + protected ItemStack createButton(int slot, Material material, String name, ArrayList lore) { + return createButton(slot, material, name, lore.toArray(new String[0])); + } + + protected void registerClickable(int min, int max, ClickType clickType, boolean bottom, Clickable clickable) { + clickables.put(new Range(min, max, clickType, bottom), clickable); + } + + protected void registerClickable(int min, int max, ClickType clickType, Clickable clickable) { + registerClickable(min, max, clickType, false, clickable); + } + + protected void registerClickable(int slot, ClickType clickType, Clickable clickable) { + registerClickable(slot, slot, clickType, false, clickable); + } + + protected void registerClickable(int min, int max, Clickable clickable) { + registerClickable(min, max, null, false, clickable); + } + + protected void registerClickable(int slot, boolean bottom, Clickable clickable) { + registerClickable(slot, slot, null, bottom, clickable); + } + + protected void registerClickable(int slot, Clickable clickable) { + registerClickable(slot, slot, null, false, clickable); + } + + protected void resetClickables() { + clickables.clear(); + } + + protected void registerOnClose(OnClose onClose) { + onCloses.add(onClose); + } + + public Inventory getInventory() { + return inventory; + } + + public class GUIHolder implements InventoryHolder { + + @Override + public Inventory getInventory() { + return inventory; + } + + public AbstractGUI getGUI() { + return AbstractGUI.this; + } + } + + public String getSetTitle() { + return setTitle; + } +} diff --git a/src/main/java/com/songoda/update/utils/gui/Clickable.java b/src/main/java/com/songoda/update/utils/gui/Clickable.java new file mode 100644 index 00000000..f198fa72 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/gui/Clickable.java @@ -0,0 +1,11 @@ +package com.songoda.update.utils.gui; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public interface Clickable { + + void Clickable(Player player, Inventory inventory, ItemStack cursor, int slot, ClickType type); +} diff --git a/src/main/java/com/songoda/update/utils/gui/OnClose.java b/src/main/java/com/songoda/update/utils/gui/OnClose.java new file mode 100644 index 00000000..0660d8e4 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/gui/OnClose.java @@ -0,0 +1,10 @@ +package com.songoda.update.utils.gui; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +public interface OnClose { + + void OnClose(Player player, Inventory inventory); + +} diff --git a/src/main/java/com/songoda/update/utils/gui/Range.java b/src/main/java/com/songoda/update/utils/gui/Range.java new file mode 100644 index 00000000..b440e231 --- /dev/null +++ b/src/main/java/com/songoda/update/utils/gui/Range.java @@ -0,0 +1,51 @@ +package com.songoda.update.utils.gui; + +import com.songoda.update.SongodaUpdate; +import com.songoda.update.utils.ServerVersion; +import org.bukkit.Sound; +import org.bukkit.event.inventory.ClickType; + +public class Range { + + private int min; + private int max; + private ClickType clickType; + private boolean bottom; + private Sound onClickSound; + + public Range(int min, int max, ClickType clickType, boolean bottom) { + this.min = min; + this.max = max; + this.clickType = clickType; + this.bottom = bottom; + if (SongodaUpdate.getInstance().isServerVersionAtLeast(ServerVersion.V1_9)) onClickSound = Sound.UI_BUTTON_CLICK; + } + + public Range(int min, int max, Sound onClickSound, ClickType clickType, boolean bottom) { + this.min = min; + this.max = max; + this.onClickSound = onClickSound; + this.clickType = clickType; + this.bottom = bottom; + } + + public int getMin() { + return min; + } + + public int getMax() { + return max; + } + + public ClickType getClickType() { + return clickType; + } + + public boolean isBottom() { + return bottom; + } + + public Sound getOnClickSound() { + return onClickSound; + } +} diff --git a/src/main/java/com/songoda/update/utils/version/NMSUtil.java b/src/main/java/com/songoda/update/utils/version/NMSUtil.java new file mode 100644 index 00000000..7f5acbfd --- /dev/null +++ b/src/main/java/com/songoda/update/utils/version/NMSUtil.java @@ -0,0 +1,100 @@ +package com.songoda.update.utils.version; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; + +public class NMSUtil { + + public static String getVersion() { + String name = Bukkit.getServer().getClass().getPackage().getName(); + return name.substring(name.lastIndexOf('.') + 1) + "."; + } + + public static int getVersionNumber() { + String name = getVersion().substring(3); + return Integer.valueOf(name.substring(0, name.length() - 4)); + } + + public static int getVersionReleaseNumber() { + String NMSVersion = getVersion(); + return Integer.valueOf(NMSVersion.substring(NMSVersion.length() - 2).replace(".", "")); + } + + public static Class getNMSClass(String className) { + try { + String fullName = "net.minecraft.server." + getVersion() + className; + Class clazz = Class.forName(fullName); + return clazz; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static Class getCraftClass(String className) throws ClassNotFoundException { + try { + String fullName = "org.bukkit.craftbukkit." + getVersion() + className; + Class clazz = Class.forName(fullName); + return clazz; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static Field getField(Class clazz, String name, boolean declared) { + try { + Field field; + + if (declared) { + field = clazz.getDeclaredField(name); + } else { + field = clazz.getField(name); + } + + field.setAccessible(true); + return field; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static Object getFieldObject(Object object, Field field) { + try { + return field.get(object); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static void setField(Object object, String fieldName, Object fieldValue, boolean declared) { + try { + Field field; + + if (declared) { + field = object.getClass().getDeclaredField(fieldName); + } else { + field = object.getClass().getField(fieldName); + } + + field.setAccessible(true); + field.set(object, fieldValue); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void sendPacket(Player player, Object packet) { + try { + Object handle = player.getClass().getMethod("getHandle").invoke(player); + Object playerConnection = handle.getClass().getField("playerConnection").get(handle); + playerConnection.getClass().getMethod("sendPacket", getNMSClass("Packet")).invoke(playerConnection, packet); + } catch (Exception e) { + e.printStackTrace(); + } + } +}