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();
+ }
+ }
+}