diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..d362284 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,14 @@ +name: Format + +on: + push: + branches: [ main ] + +jobs: + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: axel-op/googlejavaformat-action@v3 + with: + args: "--skip-sorting-imports --replace --aosp" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/action-item/pom.xml b/action-item/pom.xml new file mode 100644 index 0000000..e17e898 --- /dev/null +++ b/action-item/pom.xml @@ -0,0 +1,32 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + action-item + + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + com.pepedevs + event-utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/action-item/src/main/java/com/pepedevs/corelib/item/ActionItem.java b/action-item/src/main/java/com/pepedevs/corelib/item/ActionItem.java new file mode 100644 index 0000000..7c62ebd --- /dev/null +++ b/action-item/src/main/java/com/pepedevs/corelib/item/ActionItem.java @@ -0,0 +1,90 @@ +package com.pepedevs.corelib.item; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.List; + +public interface ActionItem { + + /** + * Gets the display name of the Action Item. + * + *

+ * + * @return Display name of the Action Item + */ + public String getDisplayName(); + + /** + * Gets the lore of the Action Item. + * + *

+ * + * @return Lore of the Action Item + */ + public List getLore(); + + /** + * Gets the material of the Action Item. + * + *

+ * + * @return Material of the Action Item + */ + public Material getMaterial(); + + /** + * Gets the {@link EventPriority} for the Action Item. + * + *

+ * + * @return Event priority of the Action Item + */ + public EventPriority getPriority(); + + /** + * Get the ItemStack of the Action Item. + * + *

+ * + * @return ItemStack of the Action Item + */ + public ItemStack toItemStack(); + + /** + * Checks if the provided ItemStack is of this Action Item. + * + *

+ * + * @param item ItemStack to check + * @return {@code true} if the ItemStack is of this Action Item, else false + */ + public boolean isThis(ItemStack item); + + /** + * Register the action to be performed on interact with this item. + * + *

+ * + * @param player Player who performs the action + * @param action {@link EnumAction} performed on this item + * @param event {@link PlayerInteractEvent} triggered in the action event + */ + public void onActionPerform(Player player, EnumAction action, PlayerInteractEvent event); + + /** Enumeration for actions defined for an item. */ + public enum EnumAction { + LEFT_CLICK, + LEFT_CLICK_SNEAKING, + LEFT_CLICK_SPRINTING, + + RIGHT_CLICK, + RIGHT_CLICK_SNEAKING, + RIGHT_CLICK_SPRINTING, + ; + } +} diff --git a/action-item/src/main/java/com/pepedevs/corelib/item/ActionItemBase.java b/action-item/src/main/java/com/pepedevs/corelib/item/ActionItemBase.java new file mode 100644 index 0000000..c218e53 --- /dev/null +++ b/action-item/src/main/java/com/pepedevs/corelib/item/ActionItemBase.java @@ -0,0 +1,97 @@ +package com.pepedevs.corelib.item; + +import com.google.common.base.Objects; +import com.pepedevs.corelib.utils.StringUtils; +import com.pepedevs.corelib.utils.itemstack.ItemMetaBuilder; +import com.pepedevs.corelib.utils.itemstack.ItemStackUtils; +import org.bukkit.Material; +import org.bukkit.event.EventPriority; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** Abstract class to be used for creating Action Items. */ +public abstract class ActionItemBase implements ActionItem { + + protected final String display_name; + protected final List lore; + protected final Material material; + protected final EventPriority priority; + + /** + * Constructs the Action Item. + * + *

+ * + * @param display_name Display name of the Action Item + * @param lore Lore of the Action Item + * @param material Material of the Action Item + * @param priority {@link EventPriority} of the Action Item + */ + public ActionItemBase( + String display_name, + Collection lore, + Material material, + EventPriority priority) { + this.display_name = StringUtils.translateAlternateColorCodes(display_name); + this.lore = + StringUtils.translateAlternateColorCodes( + StringUtils.translateAlternateColorCodes(new ArrayList<>(lore))); + this.material = material; + this.priority = priority; + } + + /** + * Constructs the Action Item. + * + *

+ * + * @param display_name Display name of the Action Item + * @param lore Lore of the Action Item + * @param material Material of the Action Item + */ + public ActionItemBase(String display_name, Collection lore, Material material) { + this(display_name, lore, material, EventPriority.NORMAL); + } + + @Override + public String getDisplayName() { + return display_name; + } + + @Override + public List getLore() { + return lore; + } + + @Override + public Material getMaterial() { + return material; + } + + @Override + public EventPriority getPriority() { + return priority; + } + + @Override + public ItemStack toItemStack() { + return new ItemMetaBuilder(getMaterial()) + .withDisplayName(getDisplayName()) + .withLore(getLore()) + .toItemStack(); + } + + @Override + public boolean isThis(ItemStack item) { + if (item != null) { + return item.getType() == getMaterial() + && Objects.equal(ItemStackUtils.extractName(item, false), getDisplayName()) + && Objects.equal(ItemStackUtils.extractLore(item, false), getLore()); + } else { + return false; + } + } +} diff --git a/action-item/src/main/java/com/pepedevs/corelib/item/ActionItemHandler.java b/action-item/src/main/java/com/pepedevs/corelib/item/ActionItemHandler.java new file mode 100644 index 0000000..12ca9a0 --- /dev/null +++ b/action-item/src/main/java/com/pepedevs/corelib/item/ActionItemHandler.java @@ -0,0 +1,135 @@ +package com.pepedevs.corelib.item; + +import com.pepedevs.corelib.events.EventHandler; +import com.pepedevs.corelib.events.EventUtils; +import com.pepedevs.corelib.utils.PluginHandler; +import org.bukkit.entity.Player; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +/** Handler Class for Action Items. */ +public class ActionItemHandler extends PluginHandler { + + /** Stores the active {@link ActionItem}s. */ + protected static final Set ACTION_ITEMS = new HashSet<>(); + + public ActionItemHandler(Plugin plugin) { + super(plugin); + register(); + + // registering executors + for (EventPriority priority : EventPriority.values()) { + EventHandler.listen(this.getPlugin(), + PlayerInteractEvent.class, + priority, + new Consumer() { + @Override + public void accept(PlayerInteractEvent event) { + Player player = event.getPlayer(); + ItemStack item = event.getItem(); + + if (item != null && event.getAction() != Action.PHYSICAL) { + ActionItem action_item = + ACTION_ITEMS.stream() + .filter( + value -> + value.getPriority() + == priority) + .filter(value -> value.isThis(item)) + .findAny() + .orElse(null); + + if (action_item != null) { + ActionItem.EnumAction action_type; + boolean sneaking = player.isSneaking(); + boolean sprinting = player.isSprinting(); + boolean left_click = + EventUtils.isLeftClick(event.getAction()); + boolean right_click = + EventUtils.isRightClick(event.getAction()); + + if (sneaking) { + action_type = + left_click + ? ActionItem.EnumAction + .LEFT_CLICK_SNEAKING + : (right_click + ? ActionItem.EnumAction + .RIGHT_CLICK_SNEAKING + : null); + } else if (sprinting) { + action_type = + left_click + ? ActionItem.EnumAction + .LEFT_CLICK_SPRINTING + : (right_click + ? ActionItem.EnumAction + .RIGHT_CLICK_SPRINTING + : null); + } else { + action_type = + left_click + ? ActionItem.EnumAction + .LEFT_CLICK + : (right_click + ? ActionItem.EnumAction + .RIGHT_CLICK + : null); + } + + if (action_type != null) { + action_item.onActionPerform( + event.getPlayer(), action_type, event); + } else { + throw new IllegalStateException( + "couldn't determine performed action"); + } + } + } + } + }); + } + } + + /** + * Registers the specified {@link ActionItem}. + * + *

+ * + * @param item Action item to register. + * @return true if the provided item is not already registered. + */ + public static boolean register(ActionItem item) { + return ACTION_ITEMS.add(item); + } + + /** + * Unregisters the specified {@link ActionItem}. + * + *

+ * + * @param item Action item to unregister. + * @return true if the provided item was unregistered successfully. + */ + public static boolean unregister(ActionItem item) { + return ACTION_ITEMS.remove(item); + } + + @Override + protected boolean isAllowMultipleInstances() { + return false; + } + + @Override + protected boolean isSingleInstanceForAllPlugin() { + return false; + } + +} diff --git a/bungee-utils/pom.xml b/bungee-utils/pom.xml new file mode 100644 index 0000000..98ccda8 --- /dev/null +++ b/bungee-utils/pom.xml @@ -0,0 +1,19 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + bungee-utils + + + 8 + 8 + + + \ No newline at end of file diff --git a/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/MessagingUtils.java b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/MessagingUtils.java new file mode 100644 index 0000000..4feff93 --- /dev/null +++ b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/MessagingUtils.java @@ -0,0 +1,158 @@ +package com.pepedevs.corelib.utils.bungeecord; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** Class for sending plugin messages via BungeeCord. */ +public class MessagingUtils { + + /** Plugin Messaging arguments. */ + public static final String MESSAGING_CHANNEL = "BungeeCord"; + + public static final String PLAYER_IP_ARGUMENT = "IP"; + public static final String SERVER_PLAYER_COUNT_ARGUMENT = "PlayerCount"; + public static final String SERVER_PLAYER_LIST_ARGUMENT = "PlayerList"; + public static final String SERVER_NAME_ARGUMENT = "GetServer"; + public static final String SERVERS_NAMES_ARGUMENT = "GetServers"; + public static final String SERVER_IP_ARGUMENT = "ServerIP"; + public static final String CONNECT_OTHER_ARGUMENT = "ConnectOther"; + + /** + * Send plugin message using the channel 'BungeeCord'. + * + *

+ * + * @param plugin Messaging plugin + * @param arguments Arguments to send + * @throws IOException thrown when an issue occur while writing to message stream + * @throws SecurityException thrown by the security manager to indicate a security violation + * @throws IllegalArgumentException thrown when a illegal argument is passed + */ + public static void sendPluginMessage(Plugin plugin, Writable... arguments) + throws IOException, IllegalArgumentException, SecurityException { + /* do not send empty arguments */ + if (arguments == null || arguments.length == 0) { + return; + } + + /* make streams and write arguments */ + final ByteArrayOutputStream array_stream = new ByteArrayOutputStream(); + final DataOutputStream out_stream = new DataOutputStream(array_stream); + for (Writable argument : arguments) { + if (argument != null + && argument.getObjectToWrite() != null + && argument.getWriteType() != null) { + argument.writeTo(out_stream); + } + } + + /* send */ + Bukkit.getServer().sendPluginMessage(plugin, MESSAGING_CHANNEL, array_stream.toByteArray()); + } + + /** + * Send plugin message using the channel 'BungeeCord'. + * + *

+ * + * @param plugin Messaging plugin + * @param written Arguments from {@link Written} + * @throws IOException thrown when an issue occur while writing to message stream + * @throws SecurityException thrown by the security manager to indicate a security violation + * @throws IllegalArgumentException thrown when a illegal argument is passed + */ + public static void sendPluginMessage(Plugin plugin, Written written) + throws IOException, IllegalArgumentException, SecurityException { + /* donnot send empty arguments */ + if (written == null || written.getWritables().isEmpty()) { + return; + } + + /* make streams and write arguments */ + final ByteArrayOutputStream array_stream = new ByteArrayOutputStream(); + final DataOutputStream out_stream = new DataOutputStream(array_stream); + for (Writable argument : written.getWritables()) { + if (argument != null + && argument.getObjectToWrite() != null + && argument.getWriteType() != null) { + argument.writeTo(out_stream); + } + } + + /* send */ + Bukkit.getServer().sendPluginMessage(plugin, MESSAGING_CHANNEL, array_stream.toByteArray()); + } + + /** + * Send plugin message using the channel 'BungeeCord' to a specific {@link Player}. + * + *

+ * + * @param plugin Messaging plugin + * @param player Player messenger + * @param arguments Arguments to send + * @throws IOException thrown when an issue occur while writing to message stream + * @throws SecurityException thrown by the security manager to indicate a security violation + * @throws IllegalArgumentException thrown when a illegal argument is passed + */ + public static void sendPluginMessage(Plugin plugin, Player player, Writable... arguments) + throws IOException, IllegalArgumentException, SecurityException { + /* donnot send empty arguments */ + if (player == null || arguments == null || arguments.length == 0) { + return; + } + + /* make streams and write arguments */ + final ByteArrayOutputStream array_stream = new ByteArrayOutputStream(); + final DataOutputStream out_stream = new DataOutputStream(array_stream); + for (Writable argument : arguments) { + if (argument != null + && argument.getObjectToWrite() != null + && argument.getWriteType() != null) { + argument.writeTo(out_stream); + } + } + + /* send */ + player.sendPluginMessage(plugin, MESSAGING_CHANNEL, array_stream.toByteArray()); + } + + /** + * Send plugin message using the channel 'BungeeCord' to a specific {@link Player}. + * + *

+ * + * @param plugin Messaging plugin + * @param player Player messenger + * @param written Arguments to send + * @throws IOException thrown when an issue occur while writing to message stream + * @throws SecurityException thrown by the security manager to indicate a security violation + * @throws IllegalArgumentException thrown when a illegal argument is passed + */ + public static void sendPluginMessage(Plugin plugin, Player player, Written written) + throws IOException, IllegalArgumentException, SecurityException { + /* donnot send empty arguments */ + if (player == null || written == null || written.getWritables().isEmpty()) { + return; + } + + /* make streams and write arguments */ + final ByteArrayOutputStream array_stream = new ByteArrayOutputStream(); + final DataOutputStream out_stream = new DataOutputStream(array_stream); + for (Writable argument : written.getWritables()) { + if (argument != null + && argument.getObjectToWrite() != null + && argument.getWriteType() != null) { + argument.writeTo(out_stream); + } + } + + /* send */ + player.sendPluginMessage(plugin, MESSAGING_CHANNEL, array_stream.toByteArray()); + } +} diff --git a/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/ReadUtils.java b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/ReadUtils.java new file mode 100644 index 0000000..77c1b4a --- /dev/null +++ b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/ReadUtils.java @@ -0,0 +1,123 @@ +package com.pepedevs.corelib.utils.bungeecord; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; + +/** Class for reading responses from BungeeCord channel. */ +public class ReadUtils { + + /** + * Read messages from 'BungeeCord' message channel. + * + *

+ * + * @param message Message from bungeecord channel + * @return Parsed message + */ + public static Object[] read(byte[] message) { + if (message == null || message.length == 0) { // don't read empty messages + return new Object[0]; + } + + try { + final DataInputStream in_stream = + new DataInputStream(new ByteArrayInputStream(message)); + final String argument = in_stream.readUTF(); + final Object[] reponse = readResponseFully(argument, in_stream); + + if (reponse == null || reponse.length == 0) { + return new String[0]; + } + + final Object[] all = new Object[reponse.length + 1]; + for (int x = 0; x < all.length; x++) { + if (x == 0) { + all[x] = argument; + } else { + all[x] = reponse[(x - 1)]; + } + } + return all; + } catch (Throwable t) { + t.printStackTrace(); + return new String[0]; + } + } + + /** + * Read arguments from 'BungeeCord' message channel. + * + *

+ * + * @param message Argument from bungeecord channel + * @return Parsed message + */ + public static String readArgument(byte[] message) { + final Object[] arg_repo = read(message); + if (arg_repo.length > 0) { + return (String) arg_repo[0]; + } + return ""; + } + + /** + * Read response from 'BungeeCord' message channel. + * + *

+ * + * @param message Response from bungeecord channel + * @return Parsed message + */ + public static Object[] readResponse(byte[] message) { + final Object[] arg_repo = read(message); + if (arg_repo.length > 1) { + Object[] response = new Object[arg_repo.length - 1]; + System.arraycopy(arg_repo, 1, response, 0, response.length); + return response; + } + return new Object[0]; + } + + /** + * Read response fully from 'BungeeCord' message channel. + * + *

+ * + * @param argument Arguments for the message + * @param in {@link DataInputStream} + * @return Parsed message + */ + private static Object[] readResponseFully(final String argument, final DataInputStream in) { + try { + // String fully = ""; /* readed from data input stream */ + Object[] all = null; + switch (argument) { + case MessagingUtils.PLAYER_IP_ARGUMENT: + case MessagingUtils.SERVER_PLAYER_COUNT_ARGUMENT: + all = new Object[] {in.readUTF(), in.readInt()}; + break; + case MessagingUtils.SERVER_PLAYER_LIST_ARGUMENT: + String server = in.readUTF(); + String[] player_list = in.readUTF().split(", "); + all = new Object[player_list.length + 1]; + all[0] = server; + System.arraycopy(player_list, 0, all, 1, player_list.length); + break; + case MessagingUtils.SERVER_NAME_ARGUMENT: + all = new Object[] {in.readUTF()}; + break; + case MessagingUtils.SERVERS_NAMES_ARGUMENT: + String[] server_list = in.readUTF().split(", "); + all = new Object[server_list.length]; + System.arraycopy(server_list, 0, all, 0, server_list.length); + break; + case MessagingUtils.SERVER_IP_ARGUMENT: + all = new Object[] {in.readUTF(), in.readUTF(), in.readUnsignedShort()}; + break; + } + return all; + } catch (Throwable t) { + return new Object[0]; + } + } +} diff --git a/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/Writable.java b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/Writable.java new file mode 100644 index 0000000..d2efc6c --- /dev/null +++ b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/Writable.java @@ -0,0 +1,116 @@ +package com.pepedevs.corelib.utils.bungeecord; + +import java.io.DataOutput; +import java.io.IOException; +import java.lang.reflect.Method; + +/** Class for writing plugin messages. */ +public class Writable { + + private final Object to_write; + private final WriteType type; + + /** + * Constructs the writable object. + * + *

+ * + * @param to_write Object to write + * @param type Write type + */ + public Writable(Object to_write, WriteType type) { + this.to_write = to_write; + this.type = type; + } + + /** + * Writes values to message. + * + *

+ * + * @param to_write Object value to write + * @return {@link Writable} object + */ + public static Writable of(Object to_write) { + if (to_write instanceof String) { + throw new UnsupportedOperationException( + "The class of the object to write is not compatible with this operation!"); + } + + WriteType type = WriteType.of(to_write); + if (type != null) { + return new Writable(to_write, type); + } + return null; + } + + /** + * Returns the object which is to write. + * + *

+ * + * @return Object to write + */ + public Object getObjectToWrite() { + return to_write; + } + + /** + * Returns the write type. + * + *

+ * + * @return {@link WriteType} + */ + public WriteType getWriteType() { + return type; + } + + /** + * Writes to data output stream. + * + *

+ * + * @param data DataOutput to write to + * @throws IOException thrown when an issue occur while writing to message stream + * @throws SecurityException thrown by the security manager to indicate a security violation + * @throws IllegalArgumentException thrown when a illegal argument is passed + */ + public void writeTo(DataOutput data) + throws IOException, IllegalArgumentException, SecurityException { + /* cannot write when the object to write is null or the write type is null */ + if (to_write == null || type == null) { + throw new IllegalArgumentException("The WriteType or the object to write is null!"); + } + + /* check instance of object to write */ + for (int x = 0; x < type.getPrimitiveClasses().length; x++) { + Class type_pr_classes = type.getPrimitiveClasses()[x]; + if (type_pr_classes.equals(to_write.getClass())) { + break; + } else if ((x + 1) == type.getPrimitiveClasses().length) { + throw new IllegalArgumentException( + "The WriteType does not match with the class of the object to write!"); + } + } + + if (type == WriteType.UTF) { + data.writeUTF((String) to_write); + } else { + for (Class type_pr_classes : type.getPrimitiveClasses()) { + try { + final String write_method_name = + "write" + + (type.name().charAt(0) + + type.name().toLowerCase().substring(1)); + final Method write = + data.getClass().getMethod(write_method_name, type_pr_classes); + write.invoke(data, to_write); + break; + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + } +} diff --git a/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/WriteType.java b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/WriteType.java new file mode 100644 index 0000000..24fd02b --- /dev/null +++ b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/WriteType.java @@ -0,0 +1,61 @@ +package com.pepedevs.corelib.utils.bungeecord; + +/** Enumeration of all write types. */ +public enum WriteType { + BOOLEAN(boolean.class, Boolean.class), + BYTE(byte.class, Byte.class), + CHAR(char.class, Character.class), + DOUBLE(double.class, Double.class), + FLOAT(float.class, Float.class), + INT(int.class, Integer.class), + LONG(long.class, Long.class), + SHORT(short.class, Short.class), + BYTES(String.class), + CHARS(String.class), + UTF(String.class); + + private final Class[] primitive_classes; + + WriteType(Class... primitive_classes) { + this.primitive_classes = primitive_classes; + } + + /** + * Returns the write type of the given data. + * + *

+ * + * @param to_write Object to get write type of + * @return WriteType of the object + */ + public static WriteType of(Object to_write) { + if (to_write == null) { + return null; + } + + if (to_write instanceof String) { + throw new UnsupportedOperationException( + "The class of the object to write is not compatible with this operation!"); + } + + for (WriteType type : values()) { + for (Class type_pr_classes : type.getPrimitiveClasses()) { + if (type_pr_classes.equals(to_write.getClass())) { + return type; + } + } + } + return null; + } + + /** + * Returns the primitive class. + * + *

+ * + * @return Primitive classes + */ + public Class[] getPrimitiveClasses() { + return primitive_classes; + } +} diff --git a/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/Written.java b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/Written.java new file mode 100644 index 0000000..45cf153 --- /dev/null +++ b/bungee-utils/src/main/java/com/pepedevs/corelib/utils/bungeecord/Written.java @@ -0,0 +1,161 @@ +package com.pepedevs.corelib.utils.bungeecord; + +import java.util.ArrayList; +import java.util.List; + +/** Builder/Wrapper class for writing values. */ +public class Written { + + private final List writables = new ArrayList<>(); + + /** + * Returns the list of {@link Writable}. + * + *

+ * + * @return List of {@link Writable} + */ + public List getWritables() { + return writables; + } + + /** + * Writes boolean value to message. + * + *

+ * + * @param voolean Boolean value + * @return This Object, for chaining + */ + public Written writeBoolean(boolean voolean) { + return writeOrdinary(voolean); + } + + /** + * Writes byte value to message. + * + *

+ * + * @param vyte Byte value + * @return This Object, for chaining + */ + public Written writeByte(byte vyte) { + return writeOrdinary(vyte); + } + + /** + * Writes character value to message. + * + *

+ * + * @param character Character value + * @return This Object, for chaining + */ + public Written writeChar(char character) { + return writeOrdinary(character); + } + + /** + * Writes double value to message. + * + *

+ * + * @param ddouble Double value + * @return This Object, for chaining + */ + public Written writeDouble(double ddouble) { + return writeOrdinary(ddouble); + } + + /** + * Writes float value to message. + * + *

+ * + * @param ffloat Float value + * @return This Object, for chaining + */ + public Written writeFloat(float ffloat) { + return writeOrdinary(ffloat); + } + + /** + * Writes int value to message. + * + *

+ * + * @param lnt Int value + * @return This Object, for chaining + */ + public Written writeInt(int lnt) { + return writeOrdinary(lnt); + } + + /** + * Writes long value to message. + * + *

+ * + * @param iong Long value + * @return This Object, for chaining + */ + public Written writeLong(long iong) { + return writeOrdinary(iong); + } + + /** + * Writes short value to message. + * + *

+ * + * @param chort Short value + * @return This Object, for chaining + */ + public Written writeShort(short chort) { + return writeOrdinary(chort); + } + + /** + * Writes string value in bytes to message. + * + *

+ * + * @param bytes String value + * @return This Object, for chaining + */ + public Written writeBytes(String bytes) { + writables.add(new Writable(bytes, WriteType.BYTES)); + return this; + } + + /** + * Writes string value in char to message. + * + *

+ * + * @param chars String value + * @return This Object, for chaining + */ + public Written writeChars(String chars) { + writables.add(new Writable(chars, WriteType.CHARS)); + return this; + } + + /** + * Writes String value in UTF8 to message. + * + *

+ * + * @param utf String value + * @return This Object, for chaining + */ + public Written writeUTF(String utf) { + writables.add(new Writable(utf, WriteType.UTF)); + return this; + } + + private Written writeOrdinary(Object to_write) { + writables.add(Writable.of(to_write)); + return this; + } +} diff --git a/configuration-utils/pom.xml b/configuration-utils/pom.xml new file mode 100644 index 0000000..1823f95 --- /dev/null +++ b/configuration-utils/pom.xml @@ -0,0 +1,32 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + configuration-utils + + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + com.pepedevs + reflection-utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Configurable.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Configurable.java new file mode 100644 index 0000000..a98cffa --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Configurable.java @@ -0,0 +1,7 @@ +package com.pepedevs.corelib.utils.configuration; + +/** + * Simple interface that represents Objects that can be loaded by {@link Loadable} interface, and + * saved by {@link Saveable} interface. + */ +public interface Configurable extends Loadable, Saveable {} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Loadable.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Loadable.java new file mode 100644 index 0000000..c3c7205 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Loadable.java @@ -0,0 +1,336 @@ +package com.pepedevs.corelib.utils.configuration; + +import com.pepedevs.corelib.utils.Validable; +import com.pepedevs.corelib.utils.configuration.annotations.LoadableCollectionEntry; +import com.pepedevs.corelib.utils.configuration.annotations.LoadableEntry; +import com.pepedevs.corelib.utils.reflection.DataType; +import com.pepedevs.corelib.utils.reflection.general.FieldReflection; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.configuration.ConfigurationSection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Represents the classes that can be loaded from a {@link ConfigurationSection}. + * + *

+ * + *

Note:

+ * + * Every single class that implements this interface must to have an empty constructor whose will + * act as 'uninitialized constructor', then the method {@link + * Loadable#loadEntries(ConfigurationSection)} can create uninitialized instances and initialize it + * by using the method {@link Loadable#load(ConfigurationSection)}, that may initialize that + * instance correctly. + */ +public interface Loadable extends Validable { + + /** + * Load the configuration of this from the given {@link ConfigurationSection}. + * + *

+ * + * @param section Section where the configuration is located + * @return This Object, for chaining + */ + public Loadable load(ConfigurationSection section); + + /** + * Load the configuration of every single field in this class from the given {@link + * ConfigurationSection} that has the annotation {@link LoadableEntry}/{@link + * LoadableCollectionEntry} present. + * + *

+ * + * @param section Section where the configuration is located + * @return This Object, for chaining + */ + @SuppressWarnings("unchecked") + public default Loadable loadEntries(ConfigurationSection section) { + Validate.notNull(section, "The section cannot be null!"); + + List entries = new ArrayList<>(); + + // finding out entries + Class current = getClass().asSubclass(Loadable.class); + + while (current != null) { + // extracting fields + for (Field field : current.getDeclaredFields()) { + if (entries.contains(field)) { + continue; + } else { + entries.add(field); + } + } + + // finding out next loadable class from super classes + Class super_clazz = current.getSuperclass(); + + if (Loadable.class.isAssignableFrom(super_clazz)) { + current = super_clazz.asSubclass(Loadable.class); + } else { + current = null; + } + } + + // then loading + for (Field entry : entries) { + ConfigurationSection sub_section = null; + if (entry.isAnnotationPresent(LoadableEntry.class)) { + if (Modifier.isFinal(entry.getModifiers())) { + throw new UnsupportedOperationException( + "The loadable entry '" + entry.getName() + "' cannot be final!"); + } + + LoadableEntry options = entry.getAnnotation(LoadableEntry.class); + boolean loadable = Loadable.class.isAssignableFrom(entry.getType()); + boolean blank_key = StringUtils.isBlank(options.key()); + ConfigurationSection destiny_section = + (!StringUtils.isBlank(options.subsection()) + ? section.getConfigurationSection(options.subsection()) + : null); + + if (blank_key && !loadable) { + throw new UnsupportedOperationException( + "Only the entries of type '" + + Loadable.class.getName() + + "' can be loadaed from a ConfigurationSection!"); + } + + Object getted = null; + if (blank_key) { + if (destiny_section != null) { + try { + Constructor uninitialized_constructor = + entry.getType().getConstructor(); + if (uninitialized_constructor == null) { + throw new UnsupportedOperationException( + "A new instance of '" + + entry.getType().getSimpleName() + + "' couldn't be created, because there is not an" + + " empty constructor within that class!"); + } + + uninitialized_constructor.setAccessible(true); + try { + getted = + uninitialized_constructor + .newInstance(); // create uninitialized instance + getted = ((Loadable) getted).load(destiny_section); + } catch (InstantiationException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + throw new UnsupportedOperationException( + "A new instance of '" + + entry.getType().getSimpleName() + + "' couldn't be created: ", + e); + } + } catch (NoSuchMethodException | SecurityException e) { + /* ignore */ + } + } + } else { + // System.out.println ( "Loadable: load: destiny_section = " + + // destiny_section ); + if (destiny_section != null) { + getted = destiny_section.get(options.key(), null); + } else { + getted = section.get(options.key()); + // System.out.println ( "Loadable: load: getted = " + getted ); + // System.out.println ( "Loadable: load: options.key ( ) = " + + // options.key ( ) ); + // System.out.println ( "Loadable: load: section keys = " + + // section.getKeys ( false ) ); + } + } + + if (getted != null) { + DataType getted_type = DataType.fromClass(getted.getClass()); + DataType entry_type = DataType.fromClass(entry.getType()); + + // System.out.println ( "entry.getType().isAssignableFrom(getted.getClass()) + // = " + entry.getType().isAssignableFrom(getted.getClass()) ); + // System.out.println ( "DataType.fromClass(entry.getType()) = " + + // DataType.fromClass(entry.getType()) ); + // System.out.println ( "DataType.fromClass(getted.getClass()) = " + + // DataType.fromClass(getted.getClass()) ); + // System.out.println ( "DataType.fromClass(entry.getType()) == + // DataType.fromClass(getted.getClass()) = " + ( + // DataType.fromClass(entry.getType()) == DataType.fromClass(getted.getClass()) + // ) ); + + if (entry.getType().isAssignableFrom(getted.getClass()) + || entry_type == getted_type + || (getted_type.isNumber() && entry_type.isNumber())) { + try { + FieldReflection.setValue(this, entry.getName(), getted); + } catch (SecurityException + | NoSuchFieldException + | IllegalAccessException e) { + throw new IllegalStateException( + "cannot load the value to the field '" + entry.getName() + "'"); + } + } + } + } else if (entry.isAnnotationPresent(LoadableCollectionEntry.class)) { + if (!(Collection.class.isAssignableFrom(entry.getType()) + || Map.class.isAssignableFrom(entry.getType()))) { + throw new UnsupportedOperationException( + "The loadable collection entry '" + + entry.getName() + + "' is not a valid instance of '" + + Collection.class.getName() + + "' or '" + + Map.class.getName() + + "'!"); + } + + Object value = null; + try { + value = FieldReflection.getValue(this, entry.getName()); + } catch (SecurityException + | NoSuchFieldException + | IllegalArgumentException + | IllegalAccessException e1) { + throw new IllegalStateException( + "Cannot get the value of the field '" + entry.getName() + "'"); + } + + if (value == null) { + throw new UnsupportedOperationException( + "The loadable collection entry '" + + entry.getName() + + "' must be already initialized!"); + } + + if (Collection.class.isAssignableFrom(entry.getType())) { + Collection collection = ((Collection) value); + LoadableCollectionEntry options = + entry.getAnnotation(LoadableCollectionEntry.class); + if (!Loadable.class.isAssignableFrom( + FieldReflection.getParameterizedTypesClasses(entry)[0])) { + throw new UnsupportedOperationException( + "The elements type of the loadable collection entry '" + + entry.getName() + + "' must be of the type '" + + Loadable.class.getName() + + "'!"); + } + + Class element_type = + (Class) + FieldReflection.getParameterizedTypesClasses(entry)[0]; + Constructor uninitialized_constructor = null; + try { + uninitialized_constructor = element_type.getConstructor(); + if (uninitialized_constructor == null) { + throw new UnsupportedOperationException( + "A new instance of '" + + element_type.getSimpleName() + + "' couldn't be created, because there is not an empty" + + " constructor within that class!"); + } + uninitialized_constructor.setAccessible(true); + } catch (NoSuchMethodException | SecurityException e) { + /* ignore */ + } + + sub_section = + !StringUtils.isBlank(options.subsection()) + ? section.getConfigurationSection(options.subsection()) + : null; + for (String key : + (sub_section != null ? sub_section : section).getKeys(false)) { + ConfigurationSection element_section = + (sub_section != null ? sub_section : section) + .getConfigurationSection(key); + if (element_section == null) { + continue; + } + + try { + collection.add( + ((Loadable) uninitialized_constructor.newInstance()) + .load(element_section)); // don't skip invalids + // Loadable element = ((Loadable) + // uninitialized_constructor.newInstance()).load(element_section); + // if (element.isValid()) { // skip invalids + // collection.add(element); + // } + } catch (Throwable t) { + /* ignore */ + } + } + } else if (Map.class.isAssignableFrom(entry.getType())) { + Map map = (Map) value; + LoadableCollectionEntry options = + entry.getAnnotation(LoadableCollectionEntry.class); + Class[] arr = FieldReflection.getParameterizedTypesClasses(entry); + if (!String.class.isAssignableFrom(arr[0]) + || !Loadable.class.isAssignableFrom(arr[1])) { + throw new UnsupportedOperationException( + "The key and value elements type of the loadable collection entry '" + + entry.getName() + + "' must be of the type '" + + String.class.getName() + + "' and '" + + Loadable.class.getName() + + "'!"); + } + + Class element_type = (Class) arr[1]; + Constructor uninitialized_constructor = null; + try { + uninitialized_constructor = element_type.getConstructor(); + if (uninitialized_constructor == null) { + throw new UnsupportedOperationException( + "A new instance of '" + + element_type.getSimpleName() + + "' couldn't be created, because there is not an empty" + + " constructor within that class!"); + } + uninitialized_constructor.setAccessible(true); + } catch (NoSuchMethodException | SecurityException e) { + /* ignore */ + } + + sub_section = + !StringUtils.isBlank(options.subsection()) + ? section.getConfigurationSection(options.subsection()) + : null; + + for (String key : + (sub_section != null ? sub_section : section).getKeys(false)) { + ConfigurationSection element_section = + (sub_section != null ? sub_section : section) + .getConfigurationSection(key); + if (element_section == null) { + continue; + } + + try { + map.put( + key, + ((Loadable) uninitialized_constructor.newInstance()) + .load(element_section)); + } catch (Throwable t) { + /* ignore */ + } + } + } + } + } + return this; + } +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/SaveActionType.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/SaveActionType.java new file mode 100644 index 0000000..62d01f2 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/SaveActionType.java @@ -0,0 +1,18 @@ +package com.pepedevs.corelib.utils.configuration; + +import org.bukkit.configuration.ConfigurationSection; + +public enum SaveActionType { + + /** Saves the entry normally. */ + NORMAL, + + /** Saves the entry only if it have not already set. */ + NOT_SET, + + /** + * Saves the entry only if the value already set on the {@link ConfigurationSection} is not + * equal this. + */ + NOT_EQUAL; +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Saveable.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Saveable.java new file mode 100644 index 0000000..e210c37 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/Saveable.java @@ -0,0 +1,180 @@ +package com.pepedevs.corelib.utils.configuration; + +import com.pepedevs.corelib.utils.configuration.annotations.SaveableCollectionEntry; +import com.pepedevs.corelib.utils.configuration.annotations.SaveableEntry; +import com.pepedevs.corelib.utils.configuration.yaml.YamlUtils; +import com.pepedevs.corelib.utils.reflection.general.FieldReflection; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.configuration.ConfigurationSection; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Represents an Object that can be saved on a {@link ConfigurationSection}. + * + *

The method {@link #saveEntries(ConfigurationSection)} saves the value of any field in this + * instance that has the annotation {@link SaveableEntry}. + */ +public interface Saveable { + + /** + * Save this Object on the provided {@link ConfigurationSection}. + * + *

+ * + * @param section Section to save on. + * @return Total done changes in the provided section after saving. + */ + public int save(ConfigurationSection section); + + /** + * Save on the given {@link ConfigurationSection}. + * + *

This method saves all the fields that has the annotation {@link SaveableEntry}. + * + *

+ * + * @param section Section in which this will be saved. + * @return Total done changes in the provided section after saving. + */ + @SuppressWarnings("unchecked") + default int saveEntries(ConfigurationSection section) { + Validate.notNull(section, "The section cannot be null!"); + + List entries = new ArrayList<>(); + int count = 0; + Class clazz = this.getClass(); + + // finding out entries + Class current = clazz.asSubclass(Saveable.class); + + while (current != null) { + // extracting fields + for (Field field : current.getDeclaredFields()) { + if (entries.contains(field)) { + continue; + } else { + entries.add(field); + } + } + + // finding out next saveable class from super classes + Class super_clazz = current.getSuperclass(); + + if (Saveable.class.isAssignableFrom(super_clazz)) { + current = super_clazz.asSubclass(Saveable.class); + } else { + current = null; + } + } + + // then saving + for (Field entry : entries) { + Object value = null; + + try { + value = FieldReflection.getValue(this, entry.getName()); + } catch (SecurityException + | NoSuchFieldException + | IllegalArgumentException + | IllegalAccessException ex) { + throw new IllegalStateException( + "cannot get the value of the field '" + entry.getName() + "'"); + } + + if (value == null) { + continue; + } + + ConfigurationSection sub_section = null; + + if (entry.isAnnotationPresent(SaveableEntry.class)) { + SaveableEntry options = entry.getAnnotation(SaveableEntry.class); + boolean saveable = Saveable.class.isAssignableFrom(entry.getType()); + boolean blank_key = StringUtils.isBlank(options.key()); + ConfigurationSection destiny_section = + (!StringUtils.isBlank(options.subsection()) + ? YamlUtils.createNotExisting(section, options.subsection()) + : section); + + if (blank_key && !saveable) { + throw new UnsupportedOperationException( + "Only the entries of type '" + + Saveable.class.getName() + + "' can be saved on a ConfigurationSection without a key!"); + } + + if (destiny_section != null) { + if (blank_key) { + count += ((Saveable) value).save(destiny_section); + } else { + switch (options.action()) { + case NOT_EQUAL: + count += + YamlUtils.setNotEqual(destiny_section, options.key(), value) + ? 1 + : 0; + break; + case NOT_SET: + count += + YamlUtils.setNotSet(destiny_section, options.key(), value) + ? 1 + : 0; + break; + case NORMAL: + default: + destiny_section.set(options.key(), value); + count++; + break; + } + } + } + } else if (entry.isAnnotationPresent(SaveableCollectionEntry.class)) { + SaveableCollectionEntry options = + entry.getAnnotation(SaveableCollectionEntry.class); + sub_section = + !StringUtils.isBlank(options.subsection()) + ? YamlUtils.createNotExisting(section, options.subsection()) + : null; + if (sub_section == null) { + throw new UnsupportedOperationException( + "The saveable collection entry '" + + entry.getName() + + "' must have a valid sub-section!"); + } + + if (!Collection.class.isAssignableFrom(entry.getType())) { + throw new UnsupportedOperationException( + "The saveable collection entry '" + + entry.getName() + + "' is not a valid instance of '" + + Collection.class.getName() + + "'!"); + } + + if (!Saveable.class.isAssignableFrom( + FieldReflection.getParameterizedTypesClasses(entry)[0])) { + throw new UnsupportedOperationException( + "The elements of the collection of the saveable collection entry " + + entry.getName() + + " must be of type '" + + Saveable.class.getName() + + "'!"); + } + + int item_count = 0; + for (Saveable item : (Collection) value) { + item.save( + sub_section.createSection( + StringUtils.defaultIfBlank(options.subsectionprefix(), "") + + item_count++)); + } + } + } + return count; + } +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/LoadableCollectionEntry.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/LoadableCollectionEntry.java new file mode 100644 index 0000000..8596869 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/LoadableCollectionEntry.java @@ -0,0 +1,20 @@ +package com.pepedevs.corelib.utils.configuration.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LoadableCollectionEntry { + + /** + * Gets the name of the sub-section where this is located. + * + *

+ * + * @return Name of the sub-section where this is located. + */ + String subsection(); +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/LoadableEntry.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/LoadableEntry.java new file mode 100644 index 0000000..614cfb9 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/LoadableEntry.java @@ -0,0 +1,24 @@ +package com.pepedevs.corelib.utils.configuration.annotations; + +import org.apache.commons.lang.StringUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LoadableEntry { + + String key(); + + /** + * Gets the name of the sub-section where this is located. + * + *

+ * + * @return Name of the sub-section where this is located. + */ + String subsection() default StringUtils.EMPTY; +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/SaveableCollectionEntry.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/SaveableCollectionEntry.java new file mode 100644 index 0000000..b5a5edb --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/SaveableCollectionEntry.java @@ -0,0 +1,31 @@ +package com.pepedevs.corelib.utils.configuration.annotations; + +import org.apache.commons.lang.StringUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SaveableCollectionEntry { + + /** + * Gets the name of the sub-section where this will be saved. + * + *

+ * + * @return Name of the sub-section where this will be saved. + */ + String subsection(); + + /** + * Gets the prefix for the name of sub-section where this will be saved. + * + *

+ * + * @return Prefix for the name of sub-section where this will be saved. + */ + String subsectionprefix() default StringUtils.EMPTY; +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/SaveableEntry.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/SaveableEntry.java new file mode 100644 index 0000000..efbb66f --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/annotations/SaveableEntry.java @@ -0,0 +1,27 @@ +package com.pepedevs.corelib.utils.configuration.annotations; + +import com.pepedevs.corelib.utils.configuration.SaveActionType; +import org.apache.commons.lang.StringUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SaveableEntry { + + String key(); + + SaveActionType action() default SaveActionType.NORMAL; + + /** + * Gets the name of the sub-section where this will be saved on. + * + *

+ * + * @return Name of the sub-section where this will be saved. + */ + String subsection() default StringUtils.EMPTY; +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/duration/ConfigurableDuration.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/duration/ConfigurableDuration.java new file mode 100644 index 0000000..4bbfed5 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/duration/ConfigurableDuration.java @@ -0,0 +1,83 @@ +package com.pepedevs.corelib.utils.configuration.configurable.duration; + +import com.pepedevs.corelib.utils.Duration; +import com.pepedevs.corelib.utils.configuration.Configurable; +import com.pepedevs.corelib.utils.configuration.yaml.YamlUtils; +import com.pepedevs.corelib.utils.reflection.general.EnumReflection; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.concurrent.TimeUnit; + +/** + * Represents a {@link Duration} that is 'Configurable' because can be loaded from/saved on a {@link + * ConfigurationSection}. + */ +public class ConfigurableDuration extends Duration implements Configurable { + + public static final String DURATION_KEY = "duration"; + public static final String UNIT_KEY = "unit"; + + /** + * Construct a new uninitialized duration. + * + *

This constructor is used only for {@link ConfigurableDuration}s that will be initialized + * after constructing by using the method {@link + * ConfigurableDuration#load(ConfigurationSection)}. + */ + public ConfigurableDuration() { // uninitialized + this(-1L, null); + } + + /** + * Construct duration. + * + *

+ * + * @param duration Duration + * @param unit Time unit + */ + public ConfigurableDuration(long duration, TimeUnit unit) { + super(duration, unit); + } + + /** + * Construct duration from milliseconds. + * + *

+ * + * @param millis Duration in milliseconds + */ + public ConfigurableDuration(long millis) { + this(millis, TimeUnit.MILLISECONDS); + } + + /** + * Construct duration using the values of another {@link Duration}. + * + *

+ * + * @param copy {@link Duration} to copy + */ + public ConfigurableDuration(Duration copy) { + this(copy.getDuration(), copy.getUnit()); + } + + @Override + public ConfigurableDuration load(ConfigurationSection section) { + this.duration = section.getLong(DURATION_KEY, -1L); + this.unit = + EnumReflection.getEnumConstant(TimeUnit.class, section.getString(UNIT_KEY, null)); + return this; + } + + @Override + public boolean isValid() { + return this.getDuration() > -1L && this.getUnit() != null; + } + + @Override + public int save(ConfigurationSection section) { + return (YamlUtils.setNotEqual(section, DURATION_KEY, getDuration()) ? 1 : 0) + + (YamlUtils.setNotEqual(section, UNIT_KEY, getUnit().name()) ? 1 : 0); + } +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/enchantment/ConfigurableEnchantment.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/enchantment/ConfigurableEnchantment.java new file mode 100644 index 0000000..873a32e --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/enchantment/ConfigurableEnchantment.java @@ -0,0 +1,114 @@ +package com.pepedevs.corelib.utils.configuration.configurable.enchantment; + +import com.pepedevs.corelib.utils.configuration.Configurable; +import com.pepedevs.corelib.utils.configuration.Loadable; +import com.pepedevs.corelib.utils.configuration.annotations.LoadableEntry; +import com.pepedevs.corelib.utils.configuration.annotations.SaveableEntry; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.enchantments.Enchantment; + +import java.util.Arrays; + +/** + * Represents a {@link Enchantment} that is 'Configurable' because can be loaded from/saved on a + * {@link ConfigurationSection}. + */ +public class ConfigurableEnchantment implements Configurable { + + public static final String ENCHANT_KEY = "enchant"; + public static final String LEVEL_KEY = "level"; + + @LoadableEntry(key = ENCHANT_KEY) + @SaveableEntry(key = ENCHANT_KEY) + public String name; + + @LoadableEntry(key = LEVEL_KEY) + @SaveableEntry(key = LEVEL_KEY) + public Integer level; + + /** Construct a new uninitialized {@link ConfigurableEnchantment}. */ + public ConfigurableEnchantment() { + /* uninitialized */ + } + + /** + * Constructs the {@link ConfigurableEnchantment}. + * + *

+ * + * @param enchantment_name Enchantment name + * @param level Enchantment level + */ + public ConfigurableEnchantment(String enchantment_name, Integer level) { + this.name = enchantment_name; + this.level = level; + } + + /** + * Constructs the {@link ConfigurableEnchantment}. + * + *

+ * + * @param enchantment Enchantment + * @param level Enchantment level + */ + @SuppressWarnings("deprecation") + public ConfigurableEnchantment(Enchantment enchantment, Integer level) { + this(enchantment.getName(), level); + } + + @Override + public Loadable load(ConfigurationSection section) { + return this.loadEntries(section); + } + + @Override + public int save(ConfigurationSection section) { + return this.saveEntries(section); + } + + /** + * Returns the Enchantment name. + * + *

+ * + * @return Enchantment name + */ + public String getEnchantmentName() { + return name; + } + + /** + * Returns the Enchantment level. + * + *

+ * + * @return Enchantment level + */ + public Integer getEnchantmentLevel() { + return level; + } + + /** + * Gets the enchantment bound to the enchantment name. + * + *

+ * + * @return Enchantment {@link Enchantment} bound to the {@link ConfigurableEnchantment#name} + */ + @SuppressWarnings("deprecation") + public Enchantment getEnchantment() { + return Arrays.stream(Enchantment.values()) + .filter(enchantment -> enchantment.getName().equalsIgnoreCase(getEnchantmentName())) + .findAny() + .orElse(null); + } + + @Override + public boolean isValid() { + return getEnchantment() != null + && this.getEnchantmentLevel() != null + && this.getEnchantmentLevel().intValue() > -1; + } + +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/itemstack/ConfigurableItemStack.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/itemstack/ConfigurableItemStack.java new file mode 100644 index 0000000..2542e59 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/itemstack/ConfigurableItemStack.java @@ -0,0 +1,360 @@ +package com.pepedevs.corelib.utils.configuration.configurable.itemstack; + +import com.pepedevs.corelib.utils.StringUtils; +import com.pepedevs.corelib.utils.configuration.Configurable; +import com.pepedevs.corelib.utils.configuration.annotations.LoadableCollectionEntry; +import com.pepedevs.corelib.utils.configuration.annotations.SaveableCollectionEntry; +import com.pepedevs.corelib.utils.configuration.annotations.SaveableEntry; +import com.pepedevs.corelib.utils.configuration.configurable.enchantment.ConfigurableEnchantment; +import com.pepedevs.corelib.utils.itemstack.ItemMetaBuilder; +import com.pepedevs.corelib.utils.material.MaterialUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Represents a {@link ItemStack} that is 'Configurable' because can be loaded from/saved on a + * {@link ConfigurationSection}. + */ +public class ConfigurableItemStack implements Configurable { + + public static final String TYPE_KEY = "type"; + public static final String SIZE_KEY = "size"; + public static final String NAME_KEY = "name"; + public static final String LORE_KEY = "lore"; + public static final String DATA_KEY = "data"; + public static final String ENCHANTS_SECTION = "enchantments"; + public static final String ENCHANT_SUBSECTION_PREFIX = "enchantment-"; + + @LoadableCollectionEntry(subsection = ENCHANTS_SECTION) + @SaveableCollectionEntry( + subsection = ENCHANTS_SECTION, + subsectionprefix = ENCHANT_SUBSECTION_PREFIX) + public final Set enchants = new HashSet<>(); + + public @SaveableEntry(key = TYPE_KEY) String type; + public @SaveableEntry(key = SIZE_KEY) int size; + public @SaveableEntry(key = NAME_KEY) String name; + public @SaveableEntry(key = LORE_KEY) List lore; + public @SaveableEntry(key = DATA_KEY) short data; + + /** Construct a new uninitialized {@link ConfigurableItemStack}. */ + public ConfigurableItemStack() { + /* uninitialized */ + } + + /** + * Construct a {@link ConfigurableItemStack} getting the default values from the given {@link + * ItemStack}. + * + *

+ * + * @param stack {@link ItemStack} to get the default values + */ + @SuppressWarnings("deprecation") + public ConfigurableItemStack(ItemStack stack) { + this( + MaterialUtils.getRightMaterial(stack).name(), + stack.getAmount(), + stack.getItemMeta().getDisplayName(), + stack.getItemMeta().getLore(), + stack.getDurability()); + } + + /** + * Construct a {@link ConfigurableItemStack} with default values. + * + *

+ * + * @param type Default type + * @param size Default size + * @param name Default name + * @param lore Default lore + * @param data Default data + */ + public ConfigurableItemStack( + String type, int size, String name, List lore, short data) { + this.type = type; + this.size = size; + this.name = name; + this.lore = lore; + this.data = data; + } + + /** + * Construct a {@link ConfigurableItemStack} with default values. + * + *

+ * + * @param type Default type + * @param size Default size + * @param name Default name + * @param lore Default lore + * @param data Default data + */ + public ConfigurableItemStack(String type, int size, String name, String[] lore, short data) { + this(type, size, name, Arrays.asList(lore), data); + } + + /** + * Load values from the given {@link ConfigurationSection}. + * + *

+ * + * @param section {@link ConfigurationSection} to load values + * @return This Object, for chaining + */ + @Override + public ConfigurableItemStack load(ConfigurationSection section) { + Validate.notNull(section, "The section cannot be null!"); + this.type = section.getString(TYPE_KEY, null); + this.size = section.getInt(SIZE_KEY, -1); + this.name = section.getString(NAME_KEY, null); + this.lore = section.isList(LORE_KEY) ? section.getStringList(LORE_KEY) : null; + this.data = (short) section.getInt(DATA_KEY, 0); + return (ConfigurableItemStack) + this.loadEntries(section); // load enchantments by loading entries + } + + @Override + public int save(ConfigurationSection section) { + return this.saveEntries(section); + } + + /** + * Returns the type string of the ItemStack. + * + *

+ * + * @return Type of ItemStack as string + */ + public String getType() { + return type; + } + + /** + * Sets the type string of the ItemStack. + * + *

+ * + * @param type Type for ItemStack + */ + public void setType(String type) { + this.setType(type, true); + } + + /** + * Sets the type string of the ItemStack. + * + *

+ * + * @param type Type for ItemStack + * @param overwrite whether to overwrite the previous value + */ + public void setType(String type, boolean overwrite) { + this.type = overwrite ? type : (this.type == null ? type : this.type); + } + + /** + * Returns the amount of the ItemStack. + * + *

+ * + * @return Amount of ItemStack + */ + public int getSize() { + return size; + } + + /** + * Sets the amount of the ItemStack. + * + *

+ * + * @param size Amount for ItemStack + */ + public void setSize(int size) { + this.setSize(size, true); + } + + /** + * Sets the amount of the ItemStack. + * + *

+ * + * @param size Amount for ItemStack + * @param overwrite whether to overwrite the previous value + */ + public void setSize(int size, boolean overwrite) { + this.size = overwrite ? size : (this.size == -1 ? size : this.size); + } + + /** + * Returns the name of the ItemStack. + * + *

+ * + * @return Name of ItemStack + */ + public String getName() { + return name; + } + + /** + * Sets the name of the ItemStack. + * + *

+ * + * @param name Name for ItemStack + */ + public void setName(String name) { + this.setName(name, true); + } + + /** + * Sets the name of the ItemStack. + * + *

+ * + * @param name Name for ItemStack + * @param overwrite whether to overwrite the previous value + */ + public void setName(String name, boolean overwrite) { + this.name = overwrite ? name : (this.name == null ? name : this.name); + } + + /** + * Returns the lore of the ItemStack. + * + *

+ * + * @return Lore of ItemStack + */ + public List getLore() { + return lore; + } + + /** + * Sets the lore of the ItemStack. + * + *

+ * + * @param lore Lore for ItemStack + */ + public void setLore(List lore) { + this.setLore(lore, true); + } + + /** + * Sets the lore of the ItemStack. + * + *

+ * + * @param lore Lore for ItemStack + * @param overwrite whether to overwrite the previous value + */ + public void setLore(List lore, boolean overwrite) { + this.lore = overwrite ? lore : (this.lore == null ? lore : this.lore); + } + + /** + * Returns the data of the ItemStack. + * + *

+ * + * @return Data of ItemStack + */ + public short getData() { + return data; + } + + /** + * Sets the data of the ItemStack. + * + *

+ * + * @param data Data for ItemStack + */ + public void setData(short data) { + this.setData(data, true); + } + + /** + * Sets the data of the ItemStack. + * + *

+ * + * @param data Data for ItemStack + * @param overwrite whether to overwrite the previous value + */ + public void setData(short data, boolean overwrite) { + this.data = overwrite ? data : (this.data == -1 ? data : this.data); + } + + /** + * Returns the enchantments of the ItemStack. + * + *

+ * + * @return {@link ConfigurableEnchantment} of ItemStack + */ + public Set getEnchantments() { + return enchants; + } + + /** + * Adds the enchantment of the ItemStack. + * + *

+ * + * @param enchantment {@link ConfigurableEnchantment} for ItemStack + */ + public void addEnchantment(ConfigurableEnchantment enchantment) { + Validate.notNull(enchantment, "The enchantment cannot be null!"); + Validate.isTrue(enchantment.isInvalid(), "The enchantment is invalid!"); + this.enchants.add(enchantment); + } + + /** + * Returns the ItemStack. + * + *

+ * + * @return ItemStack + */ + @SuppressWarnings("deprecation") + public ItemStack toItemStack() { + ItemMetaBuilder builder = + new ItemMetaBuilder(MaterialUtils.getRightMaterial(Material.valueOf(getType()))); + this.getEnchantments().stream() + .filter(ConfigurableEnchantment::isValid) + .forEach( + enchantment -> + builder.withEnchantment( + enchantment.getEnchantment(), + enchantment.getEnchantmentLevel())); + + return builder.withDisplayName(StringUtils.translateAlternateColorCodes(this.getName())) + .withLore(StringUtils.translateAlternateColorCodes(this.getLore())) + .applyTo( + new ItemStack( + MaterialUtils.getRightMaterial(Material.valueOf(this.getType())), + getSize(), + getData())); + } + + @Override + public boolean isValid() { + return getType() != null + && Material.valueOf(this.getType()) != null + && getName() != null + && getSize() > -1; + } + +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/location/ConfigurableLocation.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/location/ConfigurableLocation.java new file mode 100644 index 0000000..643e6a0 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/configurable/location/ConfigurableLocation.java @@ -0,0 +1,193 @@ +package com.pepedevs.corelib.utils.configuration.configurable.location; + +import com.pepedevs.corelib.utils.Initializable; +import com.pepedevs.corelib.utils.configuration.Configurable; +import com.pepedevs.corelib.utils.configuration.yaml.YamlUtils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.UUID; + +/** + * Represents a {@link Location} that is 'Configurable' because can be loaded from/saved on a {@link + * ConfigurationSection}. + */ +public class ConfigurableLocation extends Location implements Configurable, Initializable { + + public static final String WORLD_UID_KEY = "world-uid"; + public static final String X_KEY = "x"; + public static final String Y_KEY = "y"; + public static final String Z_KEY = "z"; + public static final String YAW_KEY = "yaw"; + public static final String PITCH_KEY = "pitch"; + public static final String[] CONFIGURABLE_LOCATION_KEYS = { + WORLD_UID_KEY, X_KEY, Y_KEY, Z_KEY, YAW_KEY, PITCH_KEY + }; + /** Whether {@link #load(ConfigurationSection)} method has been called. */ + protected boolean initialized; + + /** Constructs a uninitialized {@link ConfigurableLocation}. */ + public ConfigurableLocation() { // uninitialized + super(null, 0, 0, 0, 0, 0); + } + + /** + * Constructs the {@link ConfigurableLocation}. + * + *

+ * + * @param world World + * @param x X-axis + * @param y Y-axis + * @param z Z-axis + * @param yaw Yaw + * @param pitch Pitch + */ + public ConfigurableLocation(World world, double x, double y, double z, float yaw, float pitch) { + super(world, x, y, z, yaw, pitch); + this.initialized = true; + } + + /** + * Constructs the {@link ConfigurableLocation}. + * + *

+ * + * @param world World + * @param x X-axis + * @param y Y-axis + * @param z Z-axis + */ + public ConfigurableLocation(World world, double x, double y, double z) { + this(world, x, y, z, 0.0F, 0.0F); + } + + /** + * Constructs the {@link ConfigurableLocation}. + * + *

+ * + * @param copy Copy of the location + */ + public ConfigurableLocation(Location copy) { + this( + copy.getWorld(), + copy.getX(), + copy.getY(), + copy.getZ(), + copy.getYaw(), + copy.getPitch()); + } + + /** + * Returns a {@link ConfigurableLocation} loaded from the given {@link ConfigurationSection}, or + * null if there is no any valid {@link ConfigurableLocation} stored on the given {@link + * ConfigurationSection}. + * + *

Note that this method checks the given configuration section calling {@link + * #isConfigurableLocation(ConfigurationSection)}. + * + *

+ * + * @param section Section to parse + * @return Parsed location + */ + public static ConfigurableLocation of(ConfigurationSection section) { + return (isConfigurableLocation(section) ? new ConfigurableLocation().load(section) : null); + } + + /** + * Return true if and only if there is a valid {@link ConfigurableLocation} stored on the given + * {@link ConfigurationSection} + * + *

+ * + * @param section {@link ConfigurationSection} where the supposed {@link ConfigurableLocation} + * is stored + * @return true if is + */ + public static boolean isConfigurableLocation(ConfigurationSection section) { + for (String key : CONFIGURABLE_LOCATION_KEYS) { + switch (key) { + case WORLD_UID_KEY: + break; + + default: + if (!(section.get(key) instanceof Number)) { + return false; + } + break; + } + } + + if (section.isString(WORLD_UID_KEY)) { + try { + // this will thrown an exception if the UUID is invalid. + UUID.fromString(section.getString(WORLD_UID_KEY)); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } else { + return false; + } + } + + @Override + public ConfigurableLocation load(ConfigurationSection section) { + this.setWorld(Bukkit.getWorld(UUID.fromString(section.getString(WORLD_UID_KEY)))); + this.setX(section.getDouble(X_KEY, 0)); + this.setY(section.getDouble(Y_KEY, 0)); + this.setZ(section.getDouble(Z_KEY, 0)); + this.setYaw((float) section.getDouble(YAW_KEY, 0)); + this.setPitch((float) section.getDouble(PITCH_KEY, 0)); + this.initialized = true; + return this; + } + + @Override + public int save(ConfigurationSection section) { + return (YamlUtils.setNotEqual( + section, + WORLD_UID_KEY, + (getWorld() != null ? getWorld().getUID().toString() : "")) + ? 1 + : 0) + + (YamlUtils.setNotEqual(section, X_KEY, getX()) ? 1 : 0) + + (YamlUtils.setNotEqual(section, Y_KEY, getY()) ? 1 : 0) + + (YamlUtils.setNotEqual(section, Z_KEY, getZ()) ? 1 : 0) + + (YamlUtils.setNotEqual(section, YAW_KEY, getYaw()) ? 1 : 0) + + (YamlUtils.setNotEqual(section, PITCH_KEY, getPitch()) ? 1 : 0); + } + + /** + * Gets a clone of this location with the specified {@link World world}. + * + *

+ * + * @param world New world for the location + * @return Clone of this location with the specified {@code world} + */ + public ConfigurableLocation withWorld(World world) { + ConfigurableLocation location = clone(); + location.setWorld(world); + return location; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public boolean isValid() { + return isInitialized() && getWorld() != null; + } + + @Override + public ConfigurableLocation clone() { + return (ConfigurableLocation) super.clone(); + } +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlConfigurationComments.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlConfigurationComments.java new file mode 100644 index 0000000..bb0c84e --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlConfigurationComments.java @@ -0,0 +1,504 @@ +package com.pepedevs.corelib.utils.configuration.yaml; + +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.YamlConstructor; +import org.bukkit.configuration.file.YamlRepresenter; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.util.Map; +import java.util.logging.Level; + +/** + * An implementation of {@link Configuration} which saves all files in Yaml, and allows to comments + * the keys by using the method {@link #options()}.comment(). + * + *

Note that this implementation is not synchronized. + */ +public class YamlConfigurationComments extends YamlConfiguration { + + protected static final String LINE_SEPARATOR = "\n"; + protected static final String VALUE_INDICATOR = ":"; + private final DumperOptions yaml_options = new DumperOptions(); + private final Representer yaml_representer = new YamlRepresenter(); + private final Yaml yaml = new Yaml(new YamlConstructor(), yaml_representer, yaml_options); + + /** + * Creates a new {@link YamlConfigurationComments}, loading from the given file. + * + *

Any errors loading the Configuration will be logged and then ignored. If the specified + * input is not a valid config, a blank config will be returned. + * + *

The encoding used may follow the system dependent default. + * + *

+ * + * @param file Input file + * @return Resulting configuration + * @throws IllegalArgumentException Thrown if file is null + */ + public static YamlConfigurationComments loadConfiguration(File file) { + Validate.notNull(file, "File cannot be null"); + + YamlConfigurationComments config = new YamlConfigurationComments(); + try { + config.load(file); + } catch (FileNotFoundException ignored) { + } catch (IOException | InvalidConfigurationException ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex); + } + return config; + } + + /** + * Creates a new {@link YamlConfigurationComments}, loading from the given reader. + * + *

Any errors loading the Configuration will be logged and then ignored. If the specified + * input is not a valid config, a blank config will be returned. + * + *

+ * + * @param reader Input + * @return Resulting configuration + * @throws IllegalArgumentException Thrown if stream is null + */ + public static YamlConfigurationComments loadConfiguration(Reader reader) { + Validate.notNull(reader, "Stream cannot be null"); + + YamlConfigurationComments config = new YamlConfigurationComments(); + try { + config.load(reader); + } catch (IOException | InvalidConfigurationException ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load configuration from stream", ex); + } + return config; + } + + @Override + public String saveToString() { + yaml_options.setIndent(options().indent()); + yaml_options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + yaml_representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + String header = buildHeader(); + String dump = insertComments(yaml.dump(getValues(false))); + + if (dump.equals(BLANK_CONFIG)) { + dump = ""; + } + + dump = dump.substring(0, dump.lastIndexOf(LINE_SEPARATOR)); // clear + // last + // line + return header + dump; + } + + // protected String insertComments ( String dump ) { + // StringBuilder result = new StringBuilder ( ); + // String [ ] lines = dump.split ( "\r?\n" , -1 ); + // + // for ( int i = 0 ; i < lines.length ; i ++ ) { + // String line = lines [ i ]; + // + // + // } + // return dump; + // } + + protected String insertComments(String dump) { + StringBuilder builder = new StringBuilder(); + String[] lines = dump.split("\r?\n", -1); + String last_node = ""; + + for (int i = 0; i < lines.length; i++) { + /* calculate line node */ + String line = lines[i]; + String line_path = ""; + int indent = indentCount(line); + String node = ""; + + // System.out.println ( ">>>>> i = " + i ); + + if (i > 0 && indent >= options().indent() && line.indexOf(VALUE_INDICATOR) != -1) { + for (int j = i; j >= 0; j--) { + String line_back = lines[j]; + String line_back_path = ""; + int line_back_indent = indentCount(line_back); + + if ((indent - options().indent()) < 0) { + break; + } + + if (line_back_indent >= indent) { + continue; + } + + if (line_back.trim().startsWith(COMMENT_PREFIX) + || line_back + .trim() + .startsWith( + YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX)) { + continue; + } + + /* father section found */ + if (line_back_indent == (indent - options().indent())) { + line_path = line.trim().substring(0, line.trim().indexOf(VALUE_INDICATOR)); + line_back_path = + line_back + .trim() + .substring(0, line_back.trim().indexOf(VALUE_INDICATOR)); + node = line_back_path + options().pathSeparator() + line_path; + if (last_node.endsWith(options().pathSeparator() + line_back_path)) { + node = last_node + options().pathSeparator() + line_path; + } + + last_node = node; + break; + } + } + } + + // System.out.println ( ">>>> node = " + node ); + // System.out.println ( ">>>> line_path = " + line_path ); + + // String node_parent_path = node.isEmpty ( ) ? this.getCurrentPath ( ) + // : node.substring ( 0 , node.lastIndexOf ( options ( ).pathSeparator ( ) ) ); + + // System.out.println ( ">>>> node_parent_path = " + node_parent_path ); + + ConfigurationSection section = + node.isEmpty() + ? this + : getConfigurationSection( + node.substring(0, node.lastIndexOf(options().pathSeparator()))); + + // System.out.println ( ">>>> node parent section = " + ( section != null ? + // section.getCurrentPath ( ) : "null" ) ); + + // System.out.println ( ">>>>>>> node = " + node ); + // System.out.println ( ">>>>>>> line_path = " + line_path ); + + if (section != null) { + // System.out.println ( ">>>> 0" ); + if (line_path.trim().isEmpty() && line.trim().indexOf(VALUE_INDICATOR) != -1) { + // System.out.println ( ">>>> 1" ); + line_path = line.trim().substring(0, line.trim().indexOf(VALUE_INDICATOR)); + } + + // System.out.println ( ">>>> 2: section path = " + section.getCurrentPath ( ) ); + + String comment = options().getComment(section, line_path); + + Map comments = + options().getComments().get(section.getCurrentPath()); + + if (comments != null) { + for (String key : comments.keySet()) { + if (line_path.equalsIgnoreCase(key)) { + // System.out.println ( ">>>> - (FOUND) " + key + ": " + + // comments.get ( key ) ); + } else { + // System.out.println ( ">>>> - " + key + ": " + comments.get ( + // key ) ); + } + } + } + + if (comment != null) { + builder.append( + getIndent(indent) + + YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX + + comment); + builder.append(LINE_SEPARATOR); + } + + // if ( options ( ).isCommented ( section , line_path ) ) { + //// System.out.println ( ">>>> 3" ); + // builder.append ( getIndent ( indent ) + + // YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX + // + options ( ).getComment ( section , line_path ) ); + // builder.append ( LINE_SEPARATOR ); + // } + } + + // System.out.println ( ); + // System.out.println ( "------------------" ); + // System.out.println ( ); + + builder.append(line); + builder.append(LINE_SEPARATOR); + } + return builder.toString(); + } + + // protected String insertComments ( String dump ) { + // StringBuilder builder = new StringBuilder ( ); + // String [ ] lines = dump.split ( "\r?\n" , -1 ); + // String last_node = ""; + // + // System.out.println ( "dump = " + dump ); + // + // for ( int i = 0 ; i < lines.length ; i ++ ) { + // /* calculate line node */ + // String line = lines [ i ]; + // String line_path = ""; + // int indent = indentCount ( line ); + // String node = ""; + // + // System.out.println ( ">>>>> i = " + i ); + // + // if ( i > 0 && indent >= options ( ).indent ( ) && line.indexOf ( VALUE_INDICATOR ) != -1 ) + // { + // for ( int j = i ; j >= 0 ; j -- ) { + // String line_back = lines [ j ]; + // String line_back_path = ""; + // int line_back_indent = indentCount ( line_back ); + // + // if ( ( indent - options ( ).indent ( ) ) < 0 ) { + // break; + // } + // + // if ( line_back_indent >= indent ) { + // continue; + // } + // + // if ( line_back.trim ( ).startsWith ( COMMENT_PREFIX ) || line_back.trim ( ) + // .startsWith ( YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX ) ) { + // continue; + // } + // + // /* father section found */ + // if ( line_back_indent == ( indent - options ( ).indent ( ) ) ) { + // line_path = line.trim ( ).substring ( 0 , line.trim ( ).indexOf ( VALUE_INDICATOR ) + // ); + // line_back_path = line_back.trim ( ).substring ( 0 , + // line_back.trim ( ).indexOf ( VALUE_INDICATOR ) ); + // node = line_back_path + options ( ).pathSeparator ( ) + line_path; + // if ( last_node.endsWith ( options ( ).pathSeparator ( ) + line_back_path ) ) { + // node = last_node + options ( ).pathSeparator ( ) + line_path; + // } + // + // last_node = node; + // break; + // } + // } + // } + // + // ConfigurationSection section = node.isEmpty ( ) ? this : getConfigurationSection ( + // node.substring ( 0 , node.lastIndexOf ( options ( ).pathSeparator ( ) ) ) ); + // + // if ( section != null ) { + // if ( line_path.trim ( ).isEmpty ( ) && line.trim ( ).indexOf ( VALUE_INDICATOR ) != -1 ) { + // line_path = line.trim ( ).substring ( 0 , line.trim ( ).indexOf ( VALUE_INDICATOR ) ); + // } + // + // if ( options ( ).isCommented ( section , line_path ) ) { + // builder.append ( getIndent ( indent ) + + // YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX + // + options ( ).getComment ( section , line_path ) ); + // builder.append ( LINE_SEPARATOR ); + // } + // } + // + // builder.append ( line ); + // builder.append ( LINE_SEPARATOR ); + // } + // return builder.toString ( ); + // } + + /** + * Gets the equivalent of the given indent represented on a String. + * + *

Examples: - getIndent(2) = " " - getIndent(4) = " " - getIndent(6) = " " + * + *

+ * + * @param indent_count Indent count + * @return String representation of the given indent + */ + protected String getIndent(int indent_count) { + StringBuilder indent = new StringBuilder(); + + for (int i = 0; i < indent_count; i++) { + indent.append(" "); + } + + return indent.toString(); + } + + @Override + public void loadFromString(String contents) throws InvalidConfigurationException { + Validate.notNull(contents, "Contents cannot be null"); + + Map input; + try { + input = (Map) yaml.load(contents); + } catch (YAMLException e) { + throw new InvalidConfigurationException(e); + } catch (ClassCastException e) { + throw new InvalidConfigurationException("Top level is not a Map."); + } + + String header = parseHeader(contents); + if (header.length() > 0) { + options().header(header); + } + + if (input != null) { + convertMapsToSections(input, this); + } + + loadComments(contents); + } + + protected void convertMapsToSections(Map input, ConfigurationSection section) { + for (Map.Entry entry : input.entrySet()) { + String key = entry.getKey().toString(); + Object value = entry.getValue(); + + if (value instanceof Map) { + convertMapsToSections((Map) value, section.createSection(key)); + } else { + section.set(key, value); + } + } + } + + protected void loadComments(String input) { + String[] lines = input.split("\r?\n", -1); + String last_node = ""; + for (int i = 0; i < lines.length; i++) { + /* calculating line node */ + String line = lines[i]; + String line_path = ""; + int indent = indentCount(line); + String node = ""; + if (i > 0 && line.indexOf(VALUE_INDICATOR) != -1) { + if (indent >= options().indent()) { + for (int j = i - 1; j >= 0; j--) { + String line_back = lines[j]; + String line_back_path = ""; + int line_back_indent = indentCount(line_back); + if (line_back_indent >= indent) { + continue; + } + + if (line_back.trim().startsWith(COMMENT_PREFIX) + || line_back + .trim() + .startsWith( + YamlConfigurationCommentsOptions + .PATH_COMMENT_PREFIX)) { + continue; + } + + /* father section found */ + if (line_back_indent == (indent - options().indent())) { + line_path = + line.trim().substring(0, line.trim().indexOf(VALUE_INDICATOR)); + line_back_path = + line_back + .trim() + .substring( + 0, line_back.trim().indexOf(VALUE_INDICATOR)); + node = line_back_path + options().pathSeparator() + line_path; + if (last_node.contains(String.valueOf(options().pathSeparator()))) { + if (last_node.endsWith( + options().pathSeparator() + line_back_path)) { + node = last_node + options().pathSeparator() + line_path; + } + + if (last_node + .substring( + 0, last_node.lastIndexOf(options().pathSeparator())) + .endsWith(options().pathSeparator() + line_back_path)) { + node = + last_node.substring( + 0, + last_node.lastIndexOf( + options().pathSeparator())) + + options().pathSeparator() + + line_path; + } + } + + last_node = node; + break; + } + } + } else { + line_path = line.trim().substring(0, line.trim().indexOf(VALUE_INDICATOR)); + node = line_path; + last_node = node; + } + } + + if ((i - 1) < 0) { + continue; + } + + String comment_line = lines[i - 1]; + if (!comment_line + .trim() + .startsWith(YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX) + || comment_line.trim().length() + == YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX.length()) { + continue; + } + + String comment = + comment_line + .trim() + .substring( + YamlConfigurationCommentsOptions.PATH_COMMENT_PREFIX.length()) + .trim(); + if (node.lastIndexOf(options().pathSeparator()) == -1) { // root + // section + options().comment(this, line_path, comment); + continue; + } + + String parent_node = node.substring(0, node.lastIndexOf(options().pathSeparator())); + if (isConfigurationSection(parent_node) && !isConfigurationSection(node)) { // sub + // section + options().comment(getConfigurationSection(parent_node), line_path, comment); + } + } + } + + /** + * Counts the amount of indent at the start of a line. + * + *

+ * + * @param line Line to count + * @return Amount of indent + */ + protected int indentCount(String line) { + int count = 0; + while (line.startsWith(" ", count)) { + count++; + } + return count; + } + + @Override + public YamlConfigurationCommentsOptions options() { + return (YamlConfigurationCommentsOptions) + (options == null + ? (options = new YamlConfigurationCommentsOptions(this)) + : options); + } +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlConfigurationCommentsOptions.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlConfigurationCommentsOptions.java new file mode 100644 index 0000000..fb1ce7b --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlConfigurationCommentsOptions.java @@ -0,0 +1,216 @@ +package com.pepedevs.corelib.utils.configuration.yaml; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfigurationOptions; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** Various settings for controlling the input and output of a {@link YamlConfigurationComments} */ +public class YamlConfigurationCommentsOptions extends YamlConfigurationOptions { + + public static final String PATH_COMMENT_PREFIX = "## "; + + private final Map> comments; + private boolean copy_comments; + + protected YamlConfigurationCommentsOptions(YamlConfigurationComments configuration) { + super(configuration); + this.comments = new HashMap<>(); + this.copy_comments = true; + } + + @Override + public YamlConfigurationComments configuration() { + return (YamlConfigurationComments) super.configuration(); + } + + @Override + public YamlConfigurationCommentsOptions copyDefaults(boolean value) { + super.copyDefaults(value); + return this; + } + + @Override + public YamlConfigurationCommentsOptions pathSeparator(char value) { + super.pathSeparator(value); + return this; + } + + @Override + public YamlConfigurationCommentsOptions header(String value) { + super.header(value); + return this; + } + + @Override + public YamlConfigurationCommentsOptions copyHeader(boolean value) { + super.copyHeader(value); + return this; + } + + /** + * Sets the comment of the given path that will be applied to the top of the given path. + * + *

This comment will be applied directly at the top of the given path. It is not required a + * new line (\n) at the end of the comment as it will automatically be applied, but you may + * include one if you wish for extra spacing. + * + *

Null is a valid value which will indicate that no comment is to be applied at the top of + * the given path. + * + *

+ * + * @param section Section where the value of the given path is located + * @param path Path of the set object + * @param comment The comment + * @return This object, for chaining + */ + public YamlConfigurationCommentsOptions comment( + ConfigurationSection section, String path, String comment) { + Validate.notNull(section, "The section cannot be null"); + Validate.isTrue(!StringUtils.isBlank(path), "Cannot comment an empty path"); + Validate.isTrue( + !path.contains(String.valueOf(pathSeparator())), + "The path cannot contains references to sub-sections (path separator '" + + pathSeparator() + + "' found)!"); + + Map sc_comments = comments.get(section.getCurrentPath()); + + if (sc_comments == null) { + comments.put(section.getCurrentPath(), sc_comments = new HashMap()); + } + + if (StringUtils.isNotBlank(comment)) { + sc_comments.put(path, comment); + } else { // if the comment is blank, is the equivalent to use the + // method: removeComment() + sc_comments.remove(path); + } + return this; + } + + /** + * Sets the comment for the desired {@code path}. + * + *

This comment will be applied directly at the top of the given path. It is not required a + * new line (\n) at the end of the comment as it will automatically be applied, but you may + * include one if you wish for extra spacing. + * + *

Null/Empty comment is a valid value which will indicate that no comment is to be applied + * at the top of the given path. + * + *

+ * + * @param path Path to comment. + * @param comment Text of the comment. + * @return Object, for chaining. + */ + public YamlConfigurationCommentsOptions comment(String path, String comment) { + Validate.isTrue(!StringUtils.isBlank(path), "Cannot comment an empty path"); + + int index = path.lastIndexOf(pathSeparator()); + if (index == -1) { + // we're commenting a section that is at the root. + return comment(configuration(), path, comment); + } else { + // we're commenting something that is within a sub configuration + // section. + String parent_path = path.substring(0, index); + String path_name = path.substring(index + 1); + + return comment( + configuration().getConfigurationSection(parent_path), path_name, comment); + } + } + + /** + * Returns true if the given path is commented. + * + *

+ * + * @param section Section where the value of the given path is located. + * @param path Path of the set object. + * @return true if commented. + */ + public boolean isCommented(ConfigurationSection section, String path) { + boolean a = comments.get(section.getCurrentPath()) != null; + boolean b = a && comments.get(section.getCurrentPath()).containsKey(path); + + // System.out.println ( ">>>> isCommented: path = " + path ); + // System.out.println ( ">>>> isCommented: a = " + a ); + // System.out.println ( ">>>> isCommented: b = " + b ); + // + // if ( a ) { + // System.out.println ( ">>>> paths: " ); + // + // for ( String paths : comments.get ( section.getCurrentPath ( ) ).keySet ( ) ) { + // System.out.println ( ">>>> - " + paths ); + // } + // } + + return a && b; + // return comments.get ( section.getCurrentPath ( ) ) != null + // && comments.get ( section.getCurrentPath ( ) ).containsKey ( path ); + } + + /** + * Returns the comment of the given path or null if it is not commented. + * + *

+ * + * @param section Section where the value of the given path is located. + * @param path Path of the set object. + * @return Comment of the given path or null if not commented. + */ + public String getComment(ConfigurationSection section, String path) { + Map parent = comments.get(section.getCurrentPath()); + + return parent != null ? parent.get(path) : null; + // return isCommented ( section , path ) ? comments.get ( section.getCurrentPath ( ) ).get + // ( path ) : null; + } + + /** + * Return all the comments of all the path of all the {@link ConfigurationSection}. + * + *

The returned map is unmodifiable, so you should not try do modify it! + * + *

+ * + * @return All the comments of all the path of all the {@link ConfigurationSection}. + */ + public Map> getComments() { + return Collections.unmodifiableMap(comments); + } + + /** + * Gets whether the comments of the path should be inserted at the top of the paths when saving + * the configuration. + * + *

+ * + * @return true if the comments are enabled. + */ + public boolean copyComments() { + return this.copy_comments; + } + + /** + * Sets whether or not the comments should by copied to the top of the paths when saving the + * configuration. + * + *

+ * + * @param copy true to enable the comments. + * @return This object, for chaining. + */ + public YamlConfigurationCommentsOptions copyComments(boolean copy) { + this.copy_comments = copy; + return this; + } +} diff --git a/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlUtils.java b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlUtils.java new file mode 100644 index 0000000..410cda8 --- /dev/null +++ b/configuration-utils/src/main/java/com/pepedevs/corelib/utils/configuration/yaml/YamlUtils.java @@ -0,0 +1,133 @@ +package com.pepedevs.corelib.utils.configuration.yaml; + +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** Class for dealing with {@link YamlConfiguration} */ +public class YamlUtils { + + /** + * Sets the specified path to the given value if and only if it is not already set. + * + *

If value is null, the entry will be removed. Any existing entry will be replaced, + * regardless of what the new value is. + * + *

Some implementations may have limitations on what you may store. See their individual + * javadocs for details. No implementations should allow you to store {@link Configuration} or + * {@link ConfigurationSection}, please use {@link + * ConfigurationSection#createSection(String)} for that. + * + *

+ * + * @param section Configuration section to set + * @param path Path of the object to set + * @param value Value to set the path to + * @return true if set successfully, else false + */ + public static boolean setNotSet(ConfigurationSection section, String path, Object value) { + if (section.isSet(path)) { + return false; + } + + section.set(path, value); + return true; + } + + /** + * Sets the specified path to the given value if and only if it the value at the provided path + * is not the same as {@code value}. + * + *

If value is null, the entry will be removed. Any existing entry will be replaced, + * regardless of what the new value is. + * + *

Some implementations may have limitations on what you may store. See their individual + * javadocs for details. No implementations should allow you to store {@link Configuration} or + * {@link ConfigurationSection}, please use {@link + * ConfigurationSection#createSection(String)} for that. + * + *

+ * + * @param section Configuration section to set + * @param path Path of the object to set + * @param value Value to set the path to + * @return true if set successfully, else false + */ + public static boolean setNotEqual(ConfigurationSection section, String path, Object value) { + if (setNotSet(section, path, value)) { + return true; + } else if (Objects.equals(value, section.get(path))) { + return false; + } + + section.set(path, value); + return true; + } + + /** + * Creates an empty {@link ConfigurationSection} within the specified {@link + * ConfigurationSection} if and only if there is no another section with the same name as the + * provided. + * + *

+ * + * @param section Section at which the new section will be created + * @param name Name for the section + * @return Newly created section, or the already existing section + */ + public static ConfigurationSection createNotExisting( + ConfigurationSection section, String name) { + return section.isConfigurationSection(name) + ? section.getConfigurationSection(name) + : section.createSection(name); + } + + /** + * Gets the {@link ConfigurationSection} within the desired {@link ConfigurationSection} + * (sub-sections of the desired section). + * + *

+ * + * @param section {@link ConfigurationSection} where the sub-sections are stored + * @return Sub-sections of the desired section + */ + public static Set getSubSections(ConfigurationSection section) { + Set sections = new HashSet<>(); + section.getKeys(false).stream() + .filter(key -> section.isConfigurationSection(key)) + .forEach(key -> sections.add(section.getConfigurationSection(key))); + return sections; + } + + /** + * Replaces the desired character that is used as path separator by the specified {@code + * alt_char}. + * + *

+ * + * @param key Key that contains the separator to replace + * @param path_separator Path separator to replace + * @param alt_char Path separator to use instead the current one + * @return Key containing the new path separator + */ + public static String alternatePathSeparator(String key, char path_separator, char alt_char) { + return key.replace(path_separator, alt_char); + } + + /** + * Replaces the common path separator {@literal '.'} by the specified {@code alt_char}. + * + *

+ * + * @param key Key that contains the common separator to replace + * @param alt_char Path separator to use instead the common one + * @return Key containing the new path separator + */ + public static String alternatePathSeparator(String key, char alt_char) { + return alternatePathSeparator(key, '.', alt_char); + } +} diff --git a/corelib/pom.xml b/corelib/pom.xml new file mode 100644 index 0000000..9f86f15 --- /dev/null +++ b/corelib/pom.xml @@ -0,0 +1,114 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + corelib + + + 8 + 8 + + + + + + src/main/resources + true + + + + + + + com.pepedevs + action-item + ${project.parent.version} + + + com.pepedevs + bungee-utils + ${project.parent.version} + + + com.pepedevs + configuration-utils + ${project.parent.version} + + + com.pepedevs + database + ${project.parent.version} + + + com.pepedevs + event-utils + ${project.parent.version} + + + com.pepedevs + inventory-gui + ${project.parent.version} + + + com.pepedevs + minecraft-version + ${project.parent.version} + + + com.pepedevs + packet-utils + ${project.parent.version} + + + com.pepedevs + placeholder-utils + ${project.parent.version} + + + com.pepedevs + plugin-helper + ${project.parent.version} + + + com.pepedevs + reflection-utils + ${project.parent.version} + + + com.pepedevs + scoreboard + ${project.parent.version} + + + com.pepedevs + task-utils + ${project.parent.version} + + + com.pepedevs + utils + ${project.parent.version} + + + com.pepedevs + vault-hook + ${project.parent.version} + + + + + org.bstats + bstats-bukkit + 2.2.1 + compile + + + + \ No newline at end of file diff --git a/corelib/src/main/java/com/pepedevs/corelib/main/CoreLib.java b/corelib/src/main/java/com/pepedevs/corelib/main/CoreLib.java new file mode 100644 index 0000000..f99fbc7 --- /dev/null +++ b/corelib/src/main/java/com/pepedevs/corelib/main/CoreLib.java @@ -0,0 +1,29 @@ +package com.pepedevs.corelib.main; + +import com.pepedevs.corelib.plugin.PluginAdapter; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SingleLineChart; +import org.bukkit.Bukkit; +import org.bukkit.plugin.ServicePriority; +import org.bukkit.plugin.java.JavaPlugin; + +public class CoreLib extends PluginAdapter { + + private static Metrics metrics; + private static final int SERVICE_ID = 11582; + + @Override + protected boolean setUp() { + this.getLogger().info("CoreLib loaded!"); + Bukkit.getServicesManager() + .register(CoreLib.class, this, this, ServicePriority.Normal); + initMetrics(this); + return true; + } + + public static void initMetrics(JavaPlugin plugin) { + metrics = new Metrics(plugin, SERVICE_ID); + metrics.addCustomChart(new SingleLineChart("servers", () -> 1)); + } + +} diff --git a/corelib/src/main/resources/plugin.yml b/corelib/src/main/resources/plugin.yml new file mode 100644 index 0000000..f5b8e2e --- /dev/null +++ b/corelib/src/main/resources/plugin.yml @@ -0,0 +1,7 @@ +name: CoreLib +version: ${project.version} +main: com.pepedevs.corelib.main.CoreLib +api-version: 1.13 +author: PepeDevs +softdepend: [Vault, PlaceHolderAPI] +load: STARTUP \ No newline at end of file diff --git a/database/pom.xml b/database/pom.xml new file mode 100644 index 0000000..c2fb7bd --- /dev/null +++ b/database/pom.xml @@ -0,0 +1,35 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + database + + + 8 + 8 + + + + + + org.mongodb + mongo-java-driver + 3.12.8 + + + + + com.zaxxer + HikariCP + 4.0.3 + + + + \ No newline at end of file diff --git a/database/src/main/java/com/pepedevs/corelib/database/Database.java b/database/src/main/java/com/pepedevs/corelib/database/Database.java new file mode 100644 index 0000000..44f1a84 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/Database.java @@ -0,0 +1,21 @@ +package com.pepedevs.corelib.database; + +public abstract class Database { + + private final DatabaseType databaseType; + + public Database(DatabaseType type) { + this.databaseType = type; + } + + public abstract boolean isConnected(); + + public abstract void connect() throws Exception; + + public abstract void disconnect() throws Exception; + + public DatabaseType getDatabaseType() { + return this.databaseType; + } + +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/DatabaseType.java b/database/src/main/java/com/pepedevs/corelib/database/DatabaseType.java new file mode 100644 index 0000000..d1275bb --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/DatabaseType.java @@ -0,0 +1,12 @@ +package com.pepedevs.corelib.database; + +public enum DatabaseType { + + SQLite, + H2, + PostGreSQL, + MYSQL, + HikariCP, + MongoDB; + +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/mongo/MongoDB.java b/database/src/main/java/com/pepedevs/corelib/database/mongo/MongoDB.java new file mode 100644 index 0000000..a5e10c4 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/mongo/MongoDB.java @@ -0,0 +1,144 @@ +package com.pepedevs.corelib.database.mongo; + +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.MongoClient; +import com.pepedevs.corelib.database.Database; +import com.pepedevs.corelib.database.DatabaseType; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; + +import java.util.HashSet; +import java.util.Set; + +/** Class for interacting with a Mongo database. */ +public class MongoDB extends Database { + + private final String ip; + private final int port; + private final String dbname; + + private MongoClient client; + private DB db; + private final Set collections = new HashSet<>(); + + /** + * Constructs MongoDB with credentials. + * + *

+ * + * @param host Database host address + * @param port Database port + * @param dbname Database name + */ + public MongoDB(String host, int port, String dbname) { + super(DatabaseType.MongoDB); + + Validate.isTrue(!StringUtils.isEmpty(host), "Host Address cannot be null or empty!"); + + this.ip = host; + this.port = port; + this.dbname = dbname; + } + + /** + * Gets whether connected to MongoDB. + * + *

+ * + * @return true if connected. + */ + @Override + public boolean isConnected() { + return this.client != null && this.db != null; + } + + /** Starts the connection with MongoDB. */ + @Override + public synchronized void connect() { + this.client = new MongoClient(this.ip, this.port); + this.db = client.getDB(this.dbname); + } + + /** Closes the connection with MongoDB. */ + @Override + public void disconnect() { + this.client.close(); + this.client = null; + this.db = null; + } + + /** + * Gets Database Collection with given names and adds to database cache. + * + *

+ * + * @param name Names of collection + * @return Set of database collections + */ + public Set getCollections(String... name) { + Set collections = new HashSet<>(); + for (String s : name) { + collections.add(db.getCollection(s)); + this.collections.add(db.getCollection(s)); + } + return collections; + } + + /** + * Gets Database Collection with given name and adds to database cache. + * + *

+ * + * @param name Name of collection + * @return Database collections + */ + public DBCollection getCollection(String name) { + this.collections.add(db.getCollection(name)); + return db.getCollection(name); + } + + /** + * Gets all cached database collection. + * + *

+ * + * @return Set of database collection + */ + public Set getAllCachedCollection() { + return this.collections; + } + + /** + * Gets all raw existing database collections. + * + *

+ * + * @return Set of database collection + */ + public Set getAllRawCollectionsName() { + return db.getCollectionNames(); + } + + /** + * Returns the database + * + *

+ * + * @return Defined database + */ + public DB getDb() { + return db; + } + + /** + * Returns the MongoClient + * + *

+ * + * @return Defined MongoClient + */ + public MongoClient getClient() { + return client; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/mongo/MongoDocument.java b/database/src/main/java/com/pepedevs/corelib/database/mongo/MongoDocument.java new file mode 100644 index 0000000..d11e10f --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/mongo/MongoDocument.java @@ -0,0 +1,401 @@ +package com.pepedevs.corelib.database.mongo; + +import com.mongodb.*; +import org.apache.commons.lang.Validate; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** Class for interacting with a Mongo database collection {@link DBObject docuemnts}. */ +public class MongoDocument { + + private final MongoClient client; + private final DBCollection collection; + + /** + * Constructs MongoDocument. + * + *

+ * + * @param client MongoClient object + * @param collection Database collection object + */ + public MongoDocument(MongoClient client, DBCollection collection) { + Validate.notNull(collection, "Collection cannot be null!"); + + this.client = client; + this.collection = collection; + } + + /** + * Constructs MongoDocument. + * + *

+ * + * @param client MongoClient object + * @param database Database object + * @param collection Database collection name + */ + public MongoDocument(MongoClient client, DB database, String collection) { + this(client, database.getCollection(collection)); + } + + /** + * Constructs MongoDocument. + * + *

+ * + * @param host Database Host Address + * @param port Database Host port + * @param collection Database collection object + * @deprecated This initialization may conflict while storing and retrieving data. + */ + @Deprecated + public MongoDocument(String host, int port, DBCollection collection) { + this(new MongoClient(host, port), collection); + } + + /** + * Constructs MongoDocument. + * + *

+ * + * @param host Database Host Address + * @param port Database Host port + * @param database Database object + * @param collection Database collection name + * @deprecated This initialization may conflict while storing and retrieving data. + */ + @Deprecated + public MongoDocument(String host, int port, DB database, String collection) { + this(new MongoClient(host, port), database.getCollection(collection)); + } + + /** + * Constructs MongoDocument. + * + *

+ * + * @param collection Database collection object + */ + public MongoDocument(DBCollection collection) { + this(null, collection); + } + + /** + * Constructs MongoDocument. + * + *

+ * + * @param database Database Object + * @param collection Database collection name + */ + public MongoDocument(DB database, String collection) { + this(null, database.getCollection(collection)); + } + + /** + * Insert new document with the given id and data. + * + *

+ * + * @param id Document id + * @param key Key with which the specified value is to be associated + * @param value Value to be associated with the specified key + */ + public void insert(Object id, String key, Object value) { + BasicDBObject document = new BasicDBObject(); + document.put("_id", id); + document.put(key, value); + this.collection.insert(document); + } + + /** + * Insert new document with the given data. + * + *

+ * + * @param key Key with which the specified value is to be associated + * @param value Value to be associated with the specified key + */ + public void insert(String key, Object value) { + BasicDBObject document = new BasicDBObject(); + document.put(key, value); + this.collection.insert(document); + } + + /** + * Insert new document with the given id and data. + * + *

+ * + * @param id Document id + * @param values Map of keys and values + * @see #insert(Object, String, Object) + */ + public void insert(Object id, Map values) { + BasicDBObject document = new BasicDBObject(); + document.put("_id", id); + for (String key : values.keySet()) { + document.put(key, values.get(key)); + } + this.collection.insert(document); + } + + /** + * Insert new document with the given data. + * + *

+ * + * @param values Map of keys and values + * @see #insert(String, Object) + * @see #insert(Object, Map) + */ + public void insert(Map values) { + BasicDBObject document = new BasicDBObject(); + for (String key : values.keySet()) { + document.put(key, values.get(key)); + } + this.collection.insert(document); + } + + /** + * Find documents with the given query set and update/change the values + * + *

+ * + * @param queryKey Query key to search with + * @param queryValue Query value to search with + * @param key Key with which the specified value is to be associated + * @param value Value to be associated with the specified key + */ + public void update(String queryKey, Object queryValue, String key, Object value) { + BasicDBObject query = new BasicDBObject(); + query.put(queryKey, queryValue); + + BasicDBObject newDocument = new BasicDBObject(); + newDocument.put(key, value); + + BasicDBObject updateObject = new BasicDBObject(); + updateObject.put("$set", newDocument); + + collection.update(query, updateObject); + } + + /** + * Find documents with the given query set and update/change the values + * + *

+ * + * @param queryKey Query key to search with + * @param queryValue Query value to search with + * @param values Map of keys and values + * @see #update(String, Object, String, Object) + */ + public void update(String queryKey, Object queryValue, Map values) { + BasicDBObject query = new BasicDBObject(); + query.put(queryKey, queryValue); + + BasicDBObject newDocument = new BasicDBObject(); + for (String key : values.keySet()) { + newDocument.put(key, values.get(key)); + } + + BasicDBObject updateObject = new BasicDBObject(); + updateObject.put("$set", newDocument); + + collection.update(query, updateObject); + } + + /** + * Find documents with the given query set and update/change the values + * + *

+ * + * @param id Query document id + * @param key Key with which the specified value is to be associated + * @param value Value to be associated with the specified key + */ + public void update(Object id, String key, Object value) { + BasicDBObject query = new BasicDBObject(); + query.put("_id", id); + + BasicDBObject newDocument = new BasicDBObject(); + newDocument.put(key, value); + + BasicDBObject updateObject = new BasicDBObject(); + updateObject.put("$set", newDocument); + + collection.update(query, updateObject); + } + + /** + * Find documents with the given query set and update/change the values + * + *

+ * + * @param id Query document id + * @param values Map of keys and values + * @see #update(Object, String, Object) + * @see #update(String, Object, Map) + */ + public void update(Object id, Map values) { + BasicDBObject query = new BasicDBObject(); + query.put("_id", id); + + BasicDBObject newDocument = new BasicDBObject(); + for (String key : values.keySet()) { + newDocument.put(key, values.get(key)); + } + + BasicDBObject updateObject = new BasicDBObject(); + updateObject.put("$set", newDocument); + + collection.update(query, updateObject); + } + + /** + * Get the set of documents with the given search query. + * + *

+ * + * @param queryKey Query key to search with + * @param queryValue Query value to search with + * @return Set of {@link DBObject documents} + */ + public Set getDocument(String queryKey, Object queryValue) { + BasicDBObject searchQuery = new BasicDBObject(); + searchQuery.put(queryKey, queryValue); + DBCursor cursor = collection.find(searchQuery); + Set doc = new HashSet<>(); + if (!cursor.hasNext()) doc.add(cursor.one()); + while (cursor.hasNext()) { + doc.add(cursor.next()); + } + return doc; + } + + /** + * Get the set of documents with the given search query. + * + *

+ * + * @param query Map of query keys and values + * @return Set of {@link DBObject documents} + * @see #getDocument(String, Object) + */ + public Set getDocument(Map query) { + BasicDBObject searchQuery = new BasicDBObject(); + for (String key : query.keySet()) { + searchQuery.put(key, query.get(key)); + } + DBCursor cursor = collection.find(searchQuery); + Set doc = new HashSet<>(); + if (!cursor.hasNext()) doc.add(cursor.one()); + while (cursor.hasNext()) { + doc.add(cursor.next()); + } + return doc; + } + + /** + * Get the document with the given document id. + * + *

+ * + * @param id Document id to search with + * @return {@link DBObject Document} + */ + public DBObject getDocument(Object id) { + BasicDBObject searchQuery = new BasicDBObject(); + searchQuery.put("_id", id); + DBCursor cursor = collection.find(searchQuery); + return cursor.one(); + } + + /** + * Get the field with the given document id and key. + * + *

+ * + * @param id Document id + * @param key Field key to get value from + * @return Value of the given field if found else null + */ + public Object getField(Object id, String key) { + BasicDBObject searchQuery = new BasicDBObject(); + searchQuery.put("_id", id); + DBCursor cursor = collection.find(searchQuery); + try { + return cursor.one().get(key); + } catch (NullPointerException ex) { + throw new NullPointerException("No document can be found with the id!"); + } + } + + /** + * Get the field with the given document and key. + * + *

+ * + * @param document {@link DBObject Document} + * @param key Field key to get value from + * @return Value of the given field if found else null + */ + public Object getField(DBObject document, String key) { + return document.get(key); + } + + /** + * Deletes a document with the given id. + * + *

+ * + * @param id Document id + */ + public void delete(Object id) { + BasicDBObject searchQuery = new BasicDBObject(); + searchQuery.put("_id", id); + + collection.remove(searchQuery); + } + + /** + * Deletes a document with the given search parameter. + * + *

+ * + * @param key Key to find a document with + * @param value Value to find a document with + */ + public void delete(String key, Object value) { + BasicDBObject searchQuery = new BasicDBObject(); + searchQuery.put(key, value); + + collection.remove(searchQuery); + } + + /** + * Deletes a document with the given search parameter. + * + *

+ * + * @param queries Map of query keys and values + */ + public void delete(Map queries) { + BasicDBObject searchQuery = new BasicDBObject(); + for (String s : queries.keySet()) searchQuery.put(s, queries.get(s)); + + collection.remove(searchQuery); + } + + /** + * Gets the {@link DBCollection} registered in this class. + * + * @return The collection registered in this class + */ + public DBCollection getCollection() { + return collection; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/SQL.java b/database/src/main/java/com/pepedevs/corelib/database/sql/SQL.java new file mode 100644 index 0000000..6bdc132 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/SQL.java @@ -0,0 +1,241 @@ +package com.pepedevs.corelib.database.sql; + +import java.sql.*; +import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Deprecated +public class SQL { + + private final Connection connection; + + public SQL(Connection connection) { + this.connection = connection; + } + + public void createTable(String table, String... columns) throws SQLException { + String col = + Arrays.stream(columns) + .map( + new Function() { + @Override + public String apply(String str) { + if (!(str.equalsIgnoreCase(columns[columns.length - 1]))) + return str + ", "; + else return str; + } + }) + .collect(Collectors.joining()) + .trim(); + + PreparedStatement ps = + connection.prepareStatement( + "CREATE TABLE IF NOT EXISTS " + table + " (" + col + ");"); + ps.executeUpdate(); + ps.close(); + } + + public void insertData(String columns, String values, String table) throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "INSERT INTO " + table + " (" + columns + ") VALUES (" + values + ");"); + ps.executeUpdate(); + ps.close(); + } + + public void deleteData(String table, String column, Object value) throws SQLException { + PreparedStatement ps = + connection.prepareStatement("DELETE FROM " + table + " WHERE " + column + "=?;"); + ps.setObject(1, value); + ps.executeUpdate(); + ps.close(); + } + + public void set(String table, String gate, Object gate_value, String column, Object value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "UPDATE " + table + " SET " + column + "=? WHERE " + gate + "=?;"); + ps.setObject(1, value); + ps.setObject(2, gate_value); + ps.executeUpdate(); + ps.close(); + } + + public boolean exists(String table, String column, Object value) throws SQLException { + PreparedStatement ps = + connection.prepareStatement("SELECT * FROM " + table + " WHERE " + column + "=?;"); + ps.setObject(1, value); + + ResultSet results = ps.executeQuery(); + boolean b = results.next(); + results.close(); + return b; + } + + public ResultSet executeQuery(String statement) throws SQLException { + return this.executeQuery(connection.prepareStatement(statement)); + } + + public ResultSet executeQuery(PreparedStatement statement) throws SQLException { + return statement.executeQuery(); + } + + public int executeUpdate(String statement) throws SQLException { + return this.executeUpdate(connection.prepareStatement(statement)); + } + + public int executeUpdate(PreparedStatement statement) throws SQLException { + return statement.executeUpdate(); + } + + public String getString(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + String toReturn; + + if (rs.next()) { + toReturn = rs.getString(column); + ps.close(); + return toReturn; + } + + return null; + } + + public int getInt(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + int toReturn; + + if (rs.next()) { + toReturn = rs.getInt(column); + ps.close(); + return toReturn; + } + return 0; + } + + public Double getDouble(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + double toReturn; + + if (rs.next()) { + toReturn = rs.getDouble(column); + ps.close(); + return toReturn; + } + return null; + } + + public long getLong(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + long toReturn; + + if (rs.next()) { + toReturn = rs.getLong(column); + ps.close(); + return toReturn; + } + return 0; + } + + public byte getByte(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + byte toReturn; + + if (rs.next()) { + toReturn = rs.getByte(column); + ps.close(); + return toReturn; + } + return 0; + } + + public boolean getBoolean(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + boolean toReturn; + + if (rs.next()) { + toReturn = rs.getBoolean(column); + ps.close(); + return toReturn; + } + return false; + } + + public Array getArray(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + Array toReturn; + + if (rs.next()) { + toReturn = rs.getArray(column); + ps.close(); + return toReturn; + } + return null; + } + + public Object get(String table, String column, String gate, Object gate_value) + throws SQLException { + PreparedStatement ps = + connection.prepareStatement( + "SELECT " + column + " FROM " + table + " WHERE " + gate + "=?;"); + ps.setObject(1, gate_value); + + ResultSet rs = ps.executeQuery(); + Object toReturn; + + if (rs.next()) { + toReturn = rs.getObject(column); + ps.close(); + return toReturn; + } + return null; + } + + public Connection getConnection() { + return connection; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/SQLConsumer.java b/database/src/main/java/com/pepedevs/corelib/database/sql/SQLConsumer.java new file mode 100644 index 0000000..89bc4f3 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/SQLConsumer.java @@ -0,0 +1,9 @@ +package com.pepedevs.corelib.database.sql; + +import java.sql.SQLException; + +@FunctionalInterface +public interface SQLConsumer { + + void accept(T t) throws SQLException; +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/SQLDatabase.java b/database/src/main/java/com/pepedevs/corelib/database/sql/SQLDatabase.java new file mode 100644 index 0000000..2ec4f07 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/SQLDatabase.java @@ -0,0 +1,218 @@ +package com.pepedevs.corelib.database.sql; + +import com.pepedevs.corelib.database.Database; +import com.pepedevs.corelib.database.DatabaseType; + +import java.io.IOException; +import java.sql.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public abstract class SQLDatabase extends Database { + + public SQLDatabase(DatabaseType type) { + super(type); + } + + public abstract Connection getConnection() + throws IOException, SQLTimeoutException, IllegalStateException, SQLException; + + public abstract int getLostConnections(); + + public boolean execute(String query) throws SQLException { + try { + PreparedStatement statement = this.getConnection().prepareStatement(query); + boolean b = statement.execute(); + statement.close(); + return b; + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + public boolean execute(PreparedStatement statement) throws SQLException { + final boolean result = statement.execute(); + if (!statement.isClosed()) statement.close(); + return result; + } + + public CompletableFuture executeAsync(String query) { + return CompletableFuture.supplyAsync( + new Supplier() { + @Override + public Boolean get() { + try { + return execute(query); + } catch (SQLException e) { + e.printStackTrace(); + } + + return false; + } + }); + } + + public CompletableFuture executeAsync(PreparedStatement statement) { + return CompletableFuture.supplyAsync( + new Supplier() { + @Override + public Boolean get() { + try { + final boolean result = statement.execute(); + if (!statement.isClosed()) statement.close(); + return result; + } catch (SQLException e) { + e.printStackTrace(); + } + + return false; + } + }); + } + + public ResultSet query(String query) throws SQLException { + try { + PreparedStatement statement = this.getConnection().prepareStatement(query); + return statement.executeQuery(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + public ResultSet query(PreparedStatement statement) throws SQLException { + return statement.executeQuery(); + } + + public void query(String query, SQLConsumer consumer) throws SQLException { + ResultSet resultSet = this.query(query); + + consumer.accept(resultSet); + + if (!resultSet.isClosed()) resultSet.close(); + if (!resultSet.getStatement().isClosed()) resultSet.getStatement().close(); + } + + public void query(PreparedStatement statement, SQLConsumer consumer) + throws SQLException { + ResultSet resultSet = this.query(statement); + + consumer.accept(resultSet); + + if (!resultSet.isClosed()) resultSet.close(); + if (!resultSet.getStatement().isClosed()) resultSet.getStatement().close(); + } + + public CompletableFuture queryAsync(String query) { + return CompletableFuture.supplyAsync( + new Supplier() { + @Override + public ResultSet get() { + try { + return SQLDatabase.this.query(query); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + }); + } + + public CompletableFuture queryAsync(PreparedStatement statement) { + return CompletableFuture.supplyAsync( + new Supplier() { + @Override + public ResultSet get() { + try { + return SQLDatabase.this.query(statement); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + }); + } + + public void queryAsync(String query, SQLConsumer consumer) { + CompletableFuture.runAsync( + new Runnable() { + @Override + public void run() { + try { + SQLDatabase.this.query(query, consumer); + } catch (SQLException e) { + e.printStackTrace(); + } + } + }); + } + + public void queryAsync(PreparedStatement statement, SQLConsumer consumer) { + CompletableFuture.runAsync( + new Runnable() { + @Override + public void run() { + try { + SQLDatabase.this.query(statement, consumer); + } catch (SQLException e) { + e.printStackTrace(); + } + } + }); + } + + public int update(String update) throws SQLException { + try { + PreparedStatement statement = this.getConnection().prepareStatement(update); + int result = statement.executeUpdate(); + + statement.close(); + return result; + } catch (IOException e) { + e.printStackTrace(); + } + + return 0; + } + + public int update(PreparedStatement statement) throws SQLException { + int result = statement.executeUpdate(); + + statement.close(); + return result; + } + + public CompletableFuture updateAsync(String update) { + return CompletableFuture.supplyAsync( + new Supplier() { + @Override + public Integer get() { + int results = 0; + try { + results = SQLDatabase.this.update(update); + } catch (SQLException e) { + e.printStackTrace(); + } + return results; + } + }); + } + + public CompletableFuture updateAsync(PreparedStatement statement) { + return CompletableFuture.supplyAsync( + new Supplier() { + @Override + public Integer get() { + int results = 0; + try { + results = SQLDatabase.this.update(statement); + } catch (SQLException e) { + e.printStackTrace(); + } + return results; + } + }); + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/h2/H2.java b/database/src/main/java/com/pepedevs/corelib/database/sql/h2/H2.java new file mode 100644 index 0000000..786c7f5 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/h2/H2.java @@ -0,0 +1,173 @@ +package com.pepedevs.corelib.database.sql.h2; + +import com.pepedevs.corelib.database.DatabaseType; +import com.pepedevs.corelib.database.sql.SQLDatabase; +import org.apache.commons.lang.Validate; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; + +public class H2 extends SQLDatabase { + + /** The JDBC driver class. */ + private static final String DRIVER_CLASS = "org.h2.Driver"; + + private final File db; + private final boolean reconnect; + + private Connection connection; + private int lost_connections; + + /** + * Constructs the H2 database. + * + *

Note: Database file should end with {@code .db} extension. + * + *

+ * + * @param db Database file + * @param reconnect {@code true} to auto reconnect + */ + public H2(File db, boolean reconnect) { + super(DatabaseType.H2); + + Validate.notNull(db, "The database file cannot be null!"); + this.db = db; + this.reconnect = reconnect; + } + + /** + * Constructs the H2 database. Auto-reconnect is set {@code true}. + * + *

Note: Database file should end with {@code .db} extension. + * + *

+ * + * @param db Database file + */ + public H2(File db) { + this(db, true); + } + + /** + * Gets whether connected to H2. + * + *

+ * + * @return true if connected. + */ + @Override + public boolean isConnected() { + try { + return this.connection != null && !this.connection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + /** + * Starts the connection with H2. + * + *

+ * + * @throws IOException when the given database file cannot be created + * @throws IllegalStateException if the JDBC drivers is unavailable or the file is not s H2 + * database file + * @throws SQLException if a database access error occurs. + * @throws SQLTimeoutException when the driver has determined that the timeout has been exceeded + * and has at least tried to cancel the current database connection attempt. + */ + @Override + public synchronized void connect() + throws IOException, IllegalStateException, SQLException, SQLTimeoutException { + if (!this.db.getName().endsWith(".db")) + throw new IllegalStateException("The database file should have '.db' extension."); + + if (!this.db.getParentFile().exists()) this.db.getParentFile().mkdirs(); + + if (!this.db.exists()) this.db.createNewFile(); + try { + Class.forName(DRIVER_CLASS); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Could not connect to H2! The H2 driver is unavailable!"); + } + this.connection = DriverManager.getConnection("jdbc:h2:" + this.db.getAbsolutePath()); + } + + /** + * Closes the connection with H2. + * + *

+ * + * @throws IllegalStateException if currently not connected, the connection should be checked + * before calling this: {@link #isConnected()}. + * @throws SQLException if a database access error occurs. + */ + @Override + public void disconnect() throws SQLException { + if (!isConnected()) { + throw new IllegalStateException("Not connected!"); + } + + this.connection.close(); + this.connection = null; + } + + /** + * + * + *

Returns:

+ * + * + * + *

+ * + * @return Connection or null if not connected. + * @throws IOException when the given database file cannot be created + * @throws SQLTimeoutException when the driver has determined that the timeout value has been + * exceeded and has at least tried to cancel the current database connection attempt. + * @throws IllegalStateException if the JDBC drivers is unavailable or the file is not s H2 + * database file + * @throws SQLException if a database access error occurs. + */ + @Override + public Connection getConnection() + throws IOException, SQLTimeoutException, IllegalStateException, SQLException { + if (!isConnected() && reconnect) { + this.lost_connections++; + this.connect(); + } + return this.isConnected() ? this.connection : null; + } + + /** + * The times the connection was lost. + * + *

+ * + * @return Times the connection was lost, or {@code -1} if the + * auto-reconnection is disabled. + */ + @Override + public int getLostConnections() { + return reconnect ? lost_connections : -1; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/hikaricp/HikariCP.java b/database/src/main/java/com/pepedevs/corelib/database/sql/hikaricp/HikariCP.java new file mode 100644 index 0000000..706547e --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/hikaricp/HikariCP.java @@ -0,0 +1,198 @@ +package com.pepedevs.corelib.database.sql.hikaricp; + +import com.pepedevs.corelib.database.DatabaseType; +import com.pepedevs.corelib.database.sql.SQLDatabase; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.lang.Validate; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; + +/** Class for interacting with a Hikari database. */ +public class HikariCP extends SQLDatabase { + + /** The JDBC driver class. */ + private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver"; + + private final HikariConfig config; + private final HikariDataSource dataSource; + private final boolean reconnect; + + private Connection connection; + private int lost_connections; + + /** + * Constructs the HikariCP database. + * + *

+ * + * @param config HikariConfig for connection + * @param dataSource DataSource for given config + * @param reconnect {@code true} to auto reconnect + */ + public HikariCP(HikariConfig config, HikariDataSource dataSource, boolean reconnect) { + super(DatabaseType.HikariCP); + + Validate.notNull(config, "HikariConfig cannot be null!"); + Validate.notNull(dataSource, "HikariDataSource cannot be null!"); + + this.config = config; + this.dataSource = dataSource; + this.reconnect = reconnect; + } + + /** + * Constructs the HikariCP database. + * + *

+ * + * @param config HikariConfig for connection + * @param dataSource DataSource for given config + */ + public HikariCP(HikariConfig config, HikariDataSource dataSource) { + this(config, dataSource, true); + } + + /** + * Constructs the HikariCP database. + * + *

+ * + * @param builder {@link HikariClientBuilder} + */ + public HikariCP(HikariClientBuilder builder) { + this(builder.getConfig(), new HikariDataSource(builder.getConfig()), builder.isReconnect()); + } + + /** + * Gets whether connected to HikariCP. + * + *

+ * + * @return true if connected. + */ + @Override + public boolean isConnected() { + try { + return this.connection != null && !this.connection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + /** + * Starts the connection with HikariCP. + * + *

+ * + * @throws IllegalStateException if the JDBC drivers is unavailable. + * @throws SQLException if a database access error occurs. + * @throws SQLTimeoutException when the driver has determined that the timeout has been exceeded + * and has at least tried to cancel the current database connection attempt. + */ + @Override + public synchronized void connect() throws IllegalStateException, SQLException { + try { + Class.forName(DRIVER_CLASS); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Could not connect to HikariCP! The JDBC connection driver is unavailable!"); + } + + this.connection = dataSource.getConnection(); + } + + /** + * Closes the connection with HikariCP. + * + *

+ * + * @throws IllegalStateException if currently not connected, the connection should be checked + * before calling this: {@link #isConnected()}. + * @throws SQLException if a database access error occurs. + */ + @Override + public void disconnect() throws SQLException, IllegalStateException { + if (!isConnected()) { + throw new IllegalStateException("Not connected!"); + } + + this.connection.close(); + this.connection = null; + } + + /** + * + * + *

Returns:

+ * + * + * + *

+ * + * @return Connection or null if not connected + * @throws SQLTimeoutException when the driver has determined that the timeout value has been + * exceeded and has at least tried to cancel the current database connection attempt. + * @throws IllegalStateException if the JDBC drivers is unavailable + * @throws SQLException if a database access error occurs. + */ + @Override + public Connection getConnection() throws IllegalStateException, SQLException { + if (!isConnected() && reconnect) { + this.lost_connections++; + this.connect(); + } + return this.isConnected() ? this.connection : null; + } + + /** + * The times the connection was lost. + * + *

+ * + * @return Times the connection was lost, or {@code -1} if the + * auto-reconnection is disabled. + */ + @Override + public int getLostConnections() { + return reconnect ? lost_connections : -1; + } + + /** + * Get {@link HikariConfig}. + * + *

+ * + * @return Current HikariConfig + */ + public HikariConfig getConfig() { + return config; + } + + /** + * Get {@link HikariDataSource}. + * + *

+ * + * @return Datasource for current HikariConfig + */ + public HikariDataSource getDataSource() { + return dataSource; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/hikaricp/HikariClientBuilder.java b/database/src/main/java/com/pepedevs/corelib/database/sql/hikaricp/HikariClientBuilder.java new file mode 100644 index 0000000..6bca374 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/hikaricp/HikariClientBuilder.java @@ -0,0 +1,349 @@ +package com.pepedevs.corelib.database.sql.hikaricp; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; + +import java.util.Properties; + +/** Class for creating Hikari client. */ +public class HikariClientBuilder { + + private static final String URL_FORMAT = + "jdbc:mysql://" + + "%s" // host + + ":" + + "%d" // port + + "/" + + "%s" // database + + "?autoReconnect=" + + "%s" // auto reconnect + + "&" + + "useSSL=" + + "%s" // use ssl + ; + + private final String host; + private final int port; + private final String database; + private final String username; + private final String password; + private final boolean reconnect; + private final boolean ssl; + + private HikariConfig config; + + /** + * Constructs the Hikari Client Builder. + * + *

+ * + * @param host Hostname + * @param username Username + * @param password Password + * @param reconnect {@code true} to auto reconnect + */ + public HikariClientBuilder( + String host, + int port, + String database, + String username, + String password, + boolean reconnect, + boolean ssl) { + this.config = new HikariConfig(); + + Validate.isTrue(!StringUtils.isEmpty(host), "The host cannot be null or empty!"); + Validate.isTrue(!StringUtils.isEmpty(database), "The database cannot be null or empty!"); + Validate.notNull(username, "Username cannot be null!"); + Validate.notNull(password, "Password cannot be null!"); + + this.host = host; + this.port = port; + this.database = database; + this.username = username; + this.password = password; + this.reconnect = reconnect; + this.ssl = ssl; + + this.config.setJdbcUrl( + String.format( + URL_FORMAT, + this.host, + this.port, + this.database, + this.reconnect, + this.ssl)); + this.config.setUsername(this.username); + this.config.setPassword(this.password); + } + + /** + * Constructs the Hikari Client Builder. + * + *

+ * + * @param host Host name + * @param port Port number + * @param database Database name + * @param username User name + * @param password User password + * @param reconnect {@code true} to auto reconnect + */ + public HikariClientBuilder( + String host, + int port, + String database, + String username, + String password, + boolean reconnect) { + this(host, port, database, username, password, reconnect, true); + } + + /** + * Add a property (name/value pair) that will be used to configure the DataSource Driver. + * + *

+ * + * @param key The name of property + * @param value Value of the property + * @return This Object, for chaining + * @see HikariConfig#addDataSourceProperty(String, Object) + */ + public HikariClientBuilder addProperty(String key, String value) { + this.config.addDataSourceProperty(key, value); + return this; + } + + /** + * Set the fully qualified class name of the JDBC DataSource that will be used create + * Connections. + * + *

+ * + * @param className The fully qualified name of the JDBC DataSource class + * @return This Object, for chaining + * @see HikariConfig#setDataSourceClassName(String) + */ + public HikariClientBuilder setDataSourceClassName(String className) { + this.config.setDataSourceClassName(className); + return this; + } + + /** + * Set the provided {@link Properties} to configure the driver. + * + *

+ * + * @param property {@link Properties} to set to the driver + * @return This Object, for chaining + */ + public HikariClientBuilder setProperty(Properties property) { + this.config.setDataSourceProperties(property); + return this; + } + + /** + * Set the default auto-commit behaviour of connections in the pool. + * + *

+ * + * @param value The desired auto-commit value + * @return This Object, for chaining + * @see HikariConfig#setAutoCommit(boolean) + */ + public HikariClientBuilder setAutoCommit(boolean value) { + this.config.setAutoCommit(value); + return this; + } + + /** + * Set the maximum number of milliseconds that a client will wait for the connection from the + * pool. + * + *

+ * + * @param timeout The connection timeout in milliseconds + * @return This Object, for chaining + * @see HikariConfig#setConnectionTimeout(long) + */ + public HikariClientBuilder setConnectionTimeout(long timeout) { + this.config.setConnectionTimeout(timeout); + return this; + } + + /** + * Set the maximum amount a time (in milliseconds) that a connection is allowed to sit idle in + * the pool. + * + *

+ * + * @param timeout The idle timeout in milliseconds + * @return This Object, for chaining + * @see HikariConfig#setIdleTimeout(long) + */ + public HikariClientBuilder setIdleTimeout(long timeout) { + this.config.setIdleTimeout(timeout); + return this; + } + + /** + * Set the keep alive timeout for the connection in the pool. + * + *

+ * + * @param time The interval in milliseconds in which the connection will be tested for aliveness + * @return This Object, for chaining + * @see HikariConfig#setKeepaliveTime(long) + */ + public HikariClientBuilder setKeepAliveTime(long time) { + this.config.setKeepaliveTime(time); + return this; + } + + /** + * Set the maximum life of the connection in the pool. + * + *

+ * + * @param time The maximum connection lifetime in the pool + * @return This Object, for chaining + * @see HikariConfig#setMaxLifetime(long) + */ + public HikariClientBuilder setMaxLifeTime(long time) { + this.config.setMaxLifetime(time); + return this; + } + + /** + * Set the SQL query to be executed to test the validity of the connection. + * + *

+ * + * @param query SQL query string + * @return This Object, for chaining + * @see HikariConfig#setConnectionTestQuery(String) + */ + public HikariClientBuilder setConnectionTestQuery(String query) { + this.config.setConnectionTestQuery(query); + return this; + } + + /** + * Set the minimum number of idle connections that will be maintained in the pool. + * + *

+ * + * @param minIdle The minimum number of idle connections + * @return This Object, for chaining + * @see HikariConfig#setMinimumIdle(int) + */ + public HikariClientBuilder setMinimumIdle(int minIdle) { + this.config.setMinimumIdle(minIdle); + return this; + } + + /** + * Set the maximum size that the pool is allowed to reach, including both idle and in-use + * connection. + * + *

+ * + * @param size The maximum number of connections + * @return This Object, for chaining + * @see HikariConfig#setMaximumPoolSize(int) + */ + public HikariClientBuilder setMaximumPoolSize(int size) { + this.config.setMaximumPoolSize(size); + return this; + } + + /** + * Set a MetricRegistry instance to use for registration of metrics used by HikariCP. + * + *

+ * + * @param registry The MetricRegistry instance for use + * @return This Object, for chaining + * @see HikariConfig#setMetricRegistry(Object) + */ + public HikariClientBuilder setMetricRegistry(Object registry) { + this.config.setMetricRegistry(registry); + return this; + } + + /** + * Set the HealthRegistry that will be used for registration of health checks by HikariCP. + * + *

+ * + * @param registry The HikariRegistry to be used + * @return This Object, for chaining + * @see HikariConfig#setHealthCheckRegistry(Object) + */ + public HikariClientBuilder setHealthCheckRegistry(Object registry) { + this.config.setHealthCheckRegistry(registry); + return this; + } + + /** + * Set the name of the connection pool. + * + *

+ * + * @param name The name of connection pool to use + * @return This Object, for chaining + * @see HikariConfig#setPoolName(String) + */ + public HikariClientBuilder setPoolName(String name) { + this.config.setPoolName(name); + return this; + } + + /** + * Build the HikariCP client. + * + *

+ * + * @return {@link HikariCP} + */ + public HikariCP build() { + return new HikariCP(config, new HikariDataSource(config), reconnect); + } + + /** + * Gets the {@link HikariConfig}. + * + *

+ * + * @return {@link HikariConfig} + */ + public HikariConfig getConfig() { + return config; + } + + /** + * Sets the {@link HikariConfig}. + * + *

+ * + * @param config The {@link HikariConfig} to set + * @return This Object, for chaining + */ + public HikariClientBuilder setConfig(HikariConfig config) { + this.config = config; + return this; + } + + /** + * Checks if its auto re-connectable. + * + *

+ * + * @return {@code true} if auto reconnect is enabled, else false + */ + public boolean isReconnect() { + return reconnect; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/mysql/MySQL.java b/database/src/main/java/com/pepedevs/corelib/database/sql/mysql/MySQL.java new file mode 100644 index 0000000..1d5ce81 --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/mysql/MySQL.java @@ -0,0 +1,223 @@ +package com.pepedevs.corelib.database.sql.mysql; + +import com.pepedevs.corelib.database.DatabaseType; +import com.pepedevs.corelib.database.sql.SQLDatabase; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; + +/** Class for interacting with a MySQL database. */ +public class MySQL extends SQLDatabase { + + /** Connection URL format. */ + private static final String URL_FORMAT = + "jdbc:mysql://" + + "%s" // host + + ":" + + "%d" // port + + "/" + + "%s" // database + + "?autoReconnect=" + + "%s" // auto reconnect + + "&" + + "useSSL=" + + "%s" // use ssl + ; + + /** The JDBC driver class. */ + private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver"; + + private final String host; + private final int port; + private final String database; + private final String username; + private final String password; + private final boolean reconnect; + private final boolean ssl; + + private Connection connection; + private int lost_connections; + + /** + * Constructs the MySQL database. + * + *

+ * + * @param host Host name + * @param port Port number + * @param database Database name + * @param username User name + * @param password User password + * @param reconnect {@code true} to auto reconnect + * @param ssl {@code true} to use SSL + */ + public MySQL( + String host, + int port, + String database, + String username, + String password, + boolean reconnect, + boolean ssl) { + super(DatabaseType.MYSQL); + + Validate.isTrue(!StringUtils.isEmpty(host), "The host cannot be null or empty!"); + Validate.isTrue(!StringUtils.isEmpty(database), "The database cannot be null or empty!"); + Validate.notNull(username, "The username cannot be null!"); + Validate.notNull(password, "The password cannot be null!"); + + this.host = host; + this.port = port; + this.database = database; + this.username = username; + this.password = password; + this.reconnect = reconnect; + this.ssl = ssl; + } + + /** + * Constructs the MySQL database. + * + *

+ * + * @param host Host name + * @param port Port number + * @param database Database name + * @param username User name + * @param password User password + * @param reconnect {@code true} to auto reconnect + */ + public MySQL( + String host, + int port, + String database, + String username, + String password, + boolean reconnect) { + this(host, port, database, username, password, reconnect, true); + } + + /** + * Gets whether connected to MySQL. + * + *

+ * + * @return true if connected. + */ + @Override + public boolean isConnected() { + try { + return this.connection != null && !this.connection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + /** + * + * + *

Returns:

+ * + * + * + *

+ * + * @return Connection or null if not connected + * @throws SQLTimeoutException when the driver has determined that the timeout value has been + * exceeded and has at least tried to cancel the current database connection attempt. + * @throws IllegalStateException if the JDBC drivers is unavailable + * @throws SQLException if a database access error occurs. + */ + @Override + public Connection getConnection() + throws SQLTimeoutException, IllegalStateException, SQLException { + if (!isConnected() && reconnect) { + this.lost_connections++; + this.connect(); + } + return this.isConnected() ? this.connection : null; + } + + /** + * The times the connection was lost. + * + *

+ * + * @return Times the connection was lost, or {@code -1} if the + * auto-reconnection is disabled. + */ + @Override + public int getLostConnections() { + return reconnect ? lost_connections : -1; + } + + /** + * Starts the connection with MySQL. + * + *

+ * + * @throws IllegalStateException if the JDBC drivers is unavailable. + * @throws SQLException if a database access error occurs. + * @throws SQLTimeoutException when the driver has determined that the timeout has been exceeded + * and has at least tried to cancel the current database connection attempt. + */ + @Override + public synchronized void connect() + throws IllegalStateException, SQLException, SQLTimeoutException { + try { + Class.forName(DRIVER_CLASS); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Could not connect to MySQL! The JDBC driver is unavailable!"); + } + + this.connection = + DriverManager.getConnection( + String.format( + URL_FORMAT, + host, + port, + database, + reconnect, + ssl), + username, + password); + } + + /** + * Closes the connection with MySQL. + * + *

+ * + * @throws IllegalStateException if currently not connected, the connection should be checked + * before calling this: {@link #isConnected()}. + * @throws SQLException if a database access error occurs. + */ + @Override + public void disconnect() throws SQLException { + if (!isConnected()) { + throw new IllegalStateException("Not connected!"); + } + + this.connection.close(); + this.connection = null; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/postgresql/PostGreSQL.java b/database/src/main/java/com/pepedevs/corelib/database/sql/postgresql/PostGreSQL.java new file mode 100644 index 0000000..f48099e --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/postgresql/PostGreSQL.java @@ -0,0 +1,221 @@ +package com.pepedevs.corelib.database.sql.postgresql; + +import com.pepedevs.corelib.database.DatabaseType; +import com.pepedevs.corelib.database.sql.SQLDatabase; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; + +public class PostGreSQL extends SQLDatabase { + + /** Connection URL format. */ + private static final String URL_FORMAT = + "jdbc:postgresql://" + + "%s" // host + + ":" + + "%d" // port + + "/" + + "%s" // database + + "?autoReconnect=" + + "%s" // auto reconnect + + "&" + + "useSSL=" + + "%s" // use ssl + ; + + private static final String DRIVER_CLASS = "org.postgresql.Driver"; + + private final String host; + private final int port; + private final String database; + private final String username; + private final String password; + private final boolean reconnect; + private final boolean ssl; + + private Connection connection; + private int lost_connections; + + /** + * Constructs the PostGreSQL database. + * + *

+ * + * @param host Host name + * @param port Port number + * @param database Database name + * @param username User name + * @param password User password + * @param reconnect {@code true} to auto reconnect + * @param ssl {@code true} to use SSL + */ + public PostGreSQL( + String host, + int port, + String database, + String username, + String password, + boolean reconnect, + boolean ssl) { + super(DatabaseType.PostGreSQL); + + Validate.isTrue(!StringUtils.isEmpty(host), "The host cannot be null or empty!"); + Validate.isTrue(!StringUtils.isEmpty(database), "The database cannot be null or empty!"); + Validate.notNull(username, "The username cannot be null!"); + Validate.notNull(password, "The password cannot be null!"); + + this.host = host; + this.port = port; + this.database = database; + this.username = username; + this.password = password; + this.reconnect = reconnect; + this.ssl = ssl; + } + + /** + * Constructs the PostGreSQL database. + * + *

+ * + * @param host Host name + * @param port Port number + * @param database Database name + * @param username User name + * @param password User password + * @param reconnect {@code true} to auto reconnect + */ + public PostGreSQL( + String host, + int port, + String database, + String username, + String password, + boolean reconnect) { + this(host, port, database, username, password, reconnect, true); + } + + /** + * Gets whether connected to PostGreSQL. + * + *

+ * + * @return true if connected. + */ + @Override + public boolean isConnected() { + try { + return this.connection != null && !this.connection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + /** + * Starts the connection with PostGreSQL. + * + *

+ * + * @throws IllegalStateException if the JDBC drivers is unavailable. + * @throws SQLException if a database access error occurs. + * @throws SQLTimeoutException when the driver has determined that the timeout has been exceeded + * and has at least tried to cancel the current database connection attempt. + */ + @Override + public synchronized void connect() + throws IllegalStateException, SQLException, SQLTimeoutException { + try { + Class.forName(DRIVER_CLASS); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Could not connect to PostGreSQL! The JDBC driver is unavailable!"); + } + + this.connection = + DriverManager.getConnection( + String.format( + URL_FORMAT, + host, + port, + database, + String.valueOf(reconnect), + String.valueOf(ssl)), + username, + password); + } + + /** + * Closes the connection with PostGreSQL. + * + *

+ * + * @throws IllegalStateException if currently not connected, the connection should be checked + * before calling this: {@link #isConnected()}. + * @throws SQLException if a database access error occurs. + */ + @Override + public void disconnect() throws SQLException { + if (!this.isConnected()) { + throw new IllegalStateException("Not connected!"); + } + + this.connection.close(); + this.connection = null; + } + + /** + * + * + *

Returns:

+ * + * + * + *

+ * + * @return Connection or null if not connected + * @throws SQLTimeoutException when the driver has determined that the timeout value has been + * exceeded and has at least tried to cancel the current database connection attempt. + * @throws IllegalStateException if the JDBC drivers is unavailable + * @throws SQLException if a database access error occurs. + */ + @Override + public Connection getConnection() + throws SQLTimeoutException, IllegalStateException, SQLException { + if (!isConnected() && reconnect) { + this.lost_connections++; + this.connect(); + } + return this.isConnected() ? this.connection : null; + } + + /** + * The times the connection was lost. + * + *

+ * + * @return Times the connection was lost, or {@code -1} if the + * auto-reconnection is disabled. + */ + @Override + public int getLostConnections() { + return reconnect ? lost_connections : -1; + } +} diff --git a/database/src/main/java/com/pepedevs/corelib/database/sql/sqlite/SQLite.java b/database/src/main/java/com/pepedevs/corelib/database/sql/sqlite/SQLite.java new file mode 100644 index 0000000..d87e4cd --- /dev/null +++ b/database/src/main/java/com/pepedevs/corelib/database/sql/sqlite/SQLite.java @@ -0,0 +1,175 @@ +package com.pepedevs.corelib.database.sql.sqlite; + +import com.pepedevs.corelib.database.DatabaseType; +import com.pepedevs.corelib.database.sql.SQLDatabase; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; + +/** Class for interacting with a SQLite database. */ +public class SQLite extends SQLDatabase { + + /** The JDBC driver class. */ + private static final String DRIVER_CLASS = "org.sqlite.JDBC"; + + private final File db; + private final boolean reconnect; + + private Connection connection; + private int lost_connections; + + /** + * Constructs the SQLite database. + * + *

Note: Database file should end with {@code .db} extension. + * + *

+ * + * @param db Database file + * @param reconnect {@code true} to auto reconnect + */ + public SQLite(File db, boolean reconnect) { + super(DatabaseType.SQLite); + + if (db == null) + throw new IllegalArgumentException("The database file cannot be null!"); + + this.db = db; + this.reconnect = reconnect; + } + + /** + * Constructs the SQLite database. Auto-reconnect is set {@code true}. + * + *

Note: Database file should end with {@code .db} extension. + * + *

+ * + * @param db Database file + */ + public SQLite(File db) { + this(db, true); + } + + /** + * Gets whether connected to SQLite. + * + *

+ * + * @return true if connected. + */ + @Override + public boolean isConnected() { + try { + return this.connection != null && !this.connection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + /** + * + * + *

Returns:

+ * + * + * + *

+ * + * @return Connection or null if not connected. + * @throws IOException when the given database file cannot be created + * @throws SQLTimeoutException when the driver has determined that the timeout value has been + * exceeded and has at least tried to cancel the current database connection attempt. + * @throws IllegalStateException if the JDBC drivers is unavailable or the file is not s sqlite + * database file + * @throws SQLException if a database access error occurs. + */ + @Override + public Connection getConnection() + throws IOException, SQLTimeoutException, IllegalStateException, SQLException { + if (!isConnected() && reconnect) { + this.lost_connections++; + this.connect(); + } + return this.isConnected() ? this.connection : null; + } + + /** + * The times the connection was lost. + * + *

+ * + * @return Times the connection was lost, or {@code -1} if the + * auto-reconnection is disabled. + */ + @Override + public int getLostConnections() { + return reconnect ? lost_connections : -1; + } + + /** + * Starts the connection with SQLite. + * + *

+ * + * @throws IOException when the given database file cannot be created + * @throws IllegalStateException if the JDBC drivers is unavailable or the file is not s sqlite + * database file + * @throws SQLException if a database access error occurs. + * @throws SQLTimeoutException when the driver has determined that the timeout has been exceeded + * and has at least tried to cancel the current database connection attempt. + */ + @Override + public synchronized void connect() + throws IOException, IllegalStateException, SQLException, SQLTimeoutException { + if (!this.db.getName().endsWith(".db")) + throw new IllegalStateException("The database file should have '.db' extension."); + + if (!this.db.getParentFile().exists()) this.db.getParentFile().mkdirs(); + + if (!this.db.exists()) this.db.createNewFile(); + try { + Class.forName(DRIVER_CLASS); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Could not connect to SQLite! The JDBC driver is unavailable!"); + } + this.connection = DriverManager.getConnection("jdbc:sqlite:" + this.db.getAbsolutePath()); + } + + /** + * Closes the connection with SQLite. + * + *

+ * + * @throws IllegalStateException if currently not connected, the connection should be checked + * before calling this: {@link #isConnected()}. + * @throws SQLException if a database access error occurs. + */ + @Override + public void disconnect() throws SQLException { + if (!isConnected()) { + throw new IllegalStateException("Not connected!"); + } + + this.connection.close(); + this.connection = null; + } +} diff --git a/event-utils/pom.xml b/event-utils/pom.xml new file mode 100644 index 0000000..75d0b6a --- /dev/null +++ b/event-utils/pom.xml @@ -0,0 +1,27 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + event-utils + + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/CustomEvent.java b/event-utils/src/main/java/com/pepedevs/corelib/events/CustomEvent.java new file mode 100644 index 0000000..460a941 --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/CustomEvent.java @@ -0,0 +1,50 @@ +package com.pepedevs.corelib.events; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; + +/** Simple implementation of {@link Event} that can call itself. */ +public abstract class CustomEvent extends Event { + + /** + * The default constructor is defined for cleaner code. This constructor assumes the event is + * synchronous. + */ + public CustomEvent() { + super(); + } + + /** + * This constructor is used to explicitly declare an event as synchronous or asynchronous. + * + *

+ * + * @param async true indicates the event will fire asynchronously, false by default from default + * constructor + */ + public CustomEvent(boolean async) { + super(async); + } + + /** + * Calls this event. + * + *

This is the same as: + * + *


+     * Bukkit.getPluginManager().callEvent(this);
+     * 
+ * + *

+ * + * @return This Object, for chaining. + * @throws IllegalStateException thrown when an asynchronous event is fired from synchronous + * code. + *

Note: This is best-effort basis, and should not be used to test synchronized state. + * This is an indicator for flawed flow logic. + */ + public CustomEvent call() throws IllegalStateException { + Bukkit.getPluginManager().callEvent(this); + return this; + } +} diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/CustomEventCancellable.java b/event-utils/src/main/java/com/pepedevs/corelib/events/CustomEventCancellable.java new file mode 100644 index 0000000..9cda84d --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/CustomEventCancellable.java @@ -0,0 +1,21 @@ +package com.pepedevs.corelib.events; + +import org.bukkit.event.Cancellable; + +/** An event that implements {@link Cancellable}, allowing its cancellation. */ +public abstract class CustomEventCancellable extends CustomEvent implements Cancellable { + + /** Whether this event is cancelled or not. */ + protected boolean cancelled; + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + +} diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/EventHandler.java b/event-utils/src/main/java/com/pepedevs/corelib/events/EventHandler.java new file mode 100644 index 0000000..2befe1c --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/EventHandler.java @@ -0,0 +1,33 @@ +package com.pepedevs.corelib.events; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.Plugin; + +import java.util.function.Consumer; + +public interface EventHandler extends Listener, EventExecutor { + + static EventHandler listen(Plugin plugin, Class type, Consumer listener) { + return listen(plugin, type, EventPriority.NORMAL, listener); + } + + static EventHandler listen(Plugin plugin, Class type, EventPriority priority, Consumer listener) { + return listen(plugin, type, priority, listener, false); + } + + static EventHandler listen(Plugin plugin, Class type, EventPriority priority, Consumer listener, boolean ignoredCancelled) { + final EventHandler events = ($, event) -> listener.accept(type.cast(event)); + Bukkit.getPluginManager().registerEvent(type, events, priority, events, plugin, ignoredCancelled); + return events; + } + + default void unregister() { + HandlerList.unregisterAll(this); + } + +} diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/EventUtils.java b/event-utils/src/main/java/com/pepedevs/corelib/events/EventUtils.java new file mode 100644 index 0000000..46bc869 --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/EventUtils.java @@ -0,0 +1,59 @@ +package com.pepedevs.corelib.events; + +import org.bukkit.event.block.Action; + +/** Class for dealing with events */ +public class EventUtils { + + /** + * Check for Right click + * + *

+ * + * @param action {@link Action} + * @return true if right click else false + * @see Action + */ + public static boolean isRightClick(Action action) { + return action != null && (action.name().contains("RIGHT_")); + } + + /** + * Check for Left click + * + *

+ * + * @param action {@link Action} + * @return true if left click else false + * @see Action + */ + public static boolean isLeftClick(Action action) { + return action != null && (action.name().contains("LEFT_")); + } + + /** + * Check for Clicking Block + * + *

+ * + * @param action {@link Action} + * @return true if clicking block else false + * @see Action + */ + public static boolean isClickingBlock(Action action) { + return action != null && (action.name().contains("_BLOCK")); + } + + /** + * Check for Clicking Air + * + *

+ * + * @param action {@link Action} + * @return true if clicking air else false + * @see Action + */ + public static boolean isClickingAir(Action action) { + return action != null && (action.name().contains("_AIR")); + } +} diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/PluginLoadEvent.java b/event-utils/src/main/java/com/pepedevs/corelib/events/PluginLoadEvent.java new file mode 100644 index 0000000..e4dcc92 --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/PluginLoadEvent.java @@ -0,0 +1,96 @@ +package com.pepedevs.corelib.events; + +import com.pepedevs.corelib.utils.Pair; +import com.pepedevs.corelib.utils.server.ServerLifePhase; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** Reference to a utility class for plugin. */ +public class PluginLoadEvent { + + private PluginLoadEvent() {} + + /** + * Triggers the consumer function on the given plugin loads. + * + *

+ * + * @param pluginName Plugin to check for loading + * @param callback Consumer to trigger when the plugin loads + */ + public static void onPluginLoaded(Plugin handlingPlugin, String pluginName, Consumer callback, Runnable notFoundCallback) { + if (Bukkit.getPluginManager().isPluginEnabled(pluginName)) { + callback.accept(Bukkit.getPluginManager().getPlugin(pluginName)); + } else { + Bukkit.getPluginManager() + .registerEvents( + new PluginLoadedListener(handlingPlugin, pluginName, callback, notFoundCallback), + handlingPlugin); + } + } + + private static class PluginLoadedListener implements Listener { + + private static final Map>> INSTANCES = new ConcurrentHashMap<>(); + + private final Plugin handlingPlugin; + private final String pluginName; + private final Consumer callback; + private final Runnable notFoundCallback; + + private PluginLoadedListener(Plugin handlingPlugin, String pluginName, Consumer callback, Runnable notFoundCallback) { + this.handlingPlugin = handlingPlugin; + this.pluginName = pluginName; + this.callback = callback; + this.notFoundCallback = notFoundCallback; + Pair> instance = INSTANCES.getOrDefault(this.handlingPlugin, new Pair<>(null, new ArrayList<>())); + if (!INSTANCES.containsKey(this.handlingPlugin)) + INSTANCES.put(this.handlingPlugin, instance); + instance.getValue().add(this); + if (instance.getKey() == null) { + instance.setKey(Bukkit.getScheduler().runTaskTimerAsynchronously(this.handlingPlugin, () -> { + if (ServerLifePhase.getLifePhase() == ServerLifePhase.RUNNING) { + Exception ex = null; + for (PluginLoadedListener listener : new ArrayList<>(instance.getValue())) { + HandlerList.unregisterAll(listener); + try { + listener.notFoundCallback.run(); + } catch (Exception e) { + if (ex == null) + ex = e; + else + ex.addSuppressed(e); + } + } + INSTANCES.remove(this.handlingPlugin); + if (ex != null) + ex.printStackTrace(); + if (!instance.getKey().isCancelled()) + instance.getKey().cancel(); + } + }, 20L, 20L)); + } + } + + @EventHandler + public void handlePluginEnable(PluginEnableEvent e) { + if (!e.getPlugin().getName().equals(pluginName)) return; + + HandlerList.unregisterAll(this); + + callback.accept(e.getPlugin()); + INSTANCES.get(this.handlingPlugin).getValue().remove(this); + } + } +} diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/player/CustomPlayerEvent.java b/event-utils/src/main/java/com/pepedevs/corelib/events/player/CustomPlayerEvent.java new file mode 100644 index 0000000..8fdcb79 --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/player/CustomPlayerEvent.java @@ -0,0 +1,47 @@ +package com.pepedevs.corelib.events.player; + +import com.pepedevs.corelib.events.CustomEvent; +import org.bukkit.entity.Player; + +/** A player related {@link CustomEvent} */ +public abstract class CustomPlayerEvent extends CustomEvent { + + /** The player involved in this event. */ + protected final Player player; + + /** + * Constructing a player event. + * + *

+ * + * @param player Player involved in this event. + * @param async true indicates the event will fire asynchronously, false by default from default + * constructor + */ + public CustomPlayerEvent(Player player, boolean async) { + super(async); + this.player = player; + } + + /** + * Constructing a synchronous player event. + * + *

+ * + * @param player Player involved in this event. + */ + public CustomPlayerEvent(final Player player) { + this(player, false); + } + + /** + * Gets the player involved in this event. + * + *

+ * + * @return Player who is involved in this event. + */ + public Player getPlayer() { + return player; + } +} diff --git a/event-utils/src/main/java/com/pepedevs/corelib/events/player/CustomPlayerEventCancellable.java b/event-utils/src/main/java/com/pepedevs/corelib/events/player/CustomPlayerEventCancellable.java new file mode 100644 index 0000000..57b09f8 --- /dev/null +++ b/event-utils/src/main/java/com/pepedevs/corelib/events/player/CustomPlayerEventCancellable.java @@ -0,0 +1,46 @@ +package com.pepedevs.corelib.events.player; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; + +/** A player related event that implements {@link Cancellable}, allowing its cancellation. */ +public abstract class CustomPlayerEventCancellable extends CustomPlayerEvent + implements Cancellable { + + /** Whether the event is cancelled or not */ + protected boolean cancelled; + + /** + * Constructing a player event. + * + *

+ * + * @param player Player involved in this event. + * @param async true indicates the event will fire asynchronously, false by default from default + * constructor + */ + public CustomPlayerEventCancellable(Player player, boolean async) { + super(player, async); + } + + /** + * Constructing a synchronous player event. + * + *

+ * + * @param player Player involved in this event. + */ + public CustomPlayerEventCancellable(final Player player) { + super(player); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } +} diff --git a/inventory-gui/pom.xml b/inventory-gui/pom.xml new file mode 100644 index 0000000..aa8e775 --- /dev/null +++ b/inventory-gui/pom.xml @@ -0,0 +1,32 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + inventory-gui + + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + com.pepedevs + reflection-utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/AnvilItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/AnvilItem.java new file mode 100644 index 0000000..027b736 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/AnvilItem.java @@ -0,0 +1,185 @@ +package com.pepedevs.corelib.gui.anvil; + +import com.pepedevs.corelib.gui.anvil.action.AnvilItemClickAction; +import com.pepedevs.corelib.utils.StringUtils; +import com.pepedevs.corelib.utils.itemstack.ItemMetaBuilder; +import com.pepedevs.corelib.utils.itemstack.ItemStackUtils; +import com.pepedevs.corelib.utils.material.MaterialUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** Class for creating custom Anvil Inventory items. */ +public abstract class AnvilItem { + + protected String name; + protected ItemStack icon; + protected List lore; + protected AnvilMenu menu; + + /** + * Constructs the AnvilItem. + * + *

+ * + * @param name Name of the AnvilItem + * @param icon ItemStack icon for the AnvilItem + * @param lore Lore of the AnvilItem + */ + public AnvilItem(String name, ItemStack icon, Collection lore) { + Validate.notNull(icon, "The icon cannot be null!"); + this.name = name == null ? "" : name; + this.icon = icon; + this.lore = new ArrayList(lore); + } + + /** + * Constructs the AnvilItem. + * + *

+ * + * @param name Name of the AnvilItem + * @param icon ItemStack icon for the AnvilItem + * @param lore Lore of the AnvilItem + */ + public AnvilItem(String name, ItemStack icon, String... lore) { + this(name, icon, Arrays.asList(lore)); + } + + /** + * Constructs the AnvilItem. + * + *

+ * + * @param icon ItemStack icon for the AnvilItem + */ + public AnvilItem(ItemStack icon) { + this( + StringUtils.defaultIfBlank( + icon.getItemMeta() != null ? icon.getItemMeta().getDisplayName() : null, + "null name"), + icon, + (String[]) + ItemStackUtils.extractLore(icon, false) + .toArray( + new String + [ItemStackUtils.extractLore(icon, false).size()])); + } + + /** + * Returns the name of the AnvilItem. + * + *

+ * + * @return Name of the AnvilItem + */ + public String getName() { + return name; + } + + /** + * Sets the name of the AnvilItem + * + *

+ * + * @param name Name for the AnvilItem + * @return This Object, for chaining + */ + public AnvilItem setName(String name) { + this.name = name; + return this; + } + + /** + * Returns the icon of the AnvilItem. + * + *

+ * + * @return Icon of the AnvilItem + */ + public ItemStack getIcon() { + return icon; + } + + /** + * Sets the icon of the AnvilItem + * + *

+ * + * @param icon ItemStack icon for the AnvilItem + * @return This Object, for chaining + */ + public AnvilItem setIcon(ItemStack icon) { + Validate.notNull(icon, "The icon cannot be null!"); + this.icon = icon; + return this; + } + + /** + * Returns the display icon of the AnvilItem. + * + *

+ * + * @return Display icon of the AnvilItem + */ + public ItemStack getDisplayIcon() { + return this.getIcon().getType() == Material.AIR + ? this.icon + : (new ItemMetaBuilder(MaterialUtils.getRightMaterial(this.getIcon()))) + .withDisplayName(StringUtils.translateAlternateColorCodes(this.getName())) + .withLore(this.getLore()) + .applyTo(this.getIcon().clone()); + } + + /** + * Returns the lore of the AnvilItem. + * + *

+ * + * @return Lore of the AnvilItem + */ + public List getLore() { + return lore; + } + + /** + * Sets the name of the AnvilItem + * + *

+ * + * @param lore Lore for the AnvilItem + * @return This Object, for chaining + */ + public AnvilItem setLore(List lore) { + this.lore = lore != null ? lore : new ArrayList<>(); + return this; + } + + /** + * Gets the menu this item belongs. + * + *

Note that this will return null if this item has never been set in a + * menu. + * + *

+ * + * @return The menu this item belongs + */ + public AnvilMenu getMenu() { + return menu; + } + + /** + * Click trigger for this AnvilItem. + * + *

+ * + * @param action {@link AnvilItemClickAction} for the AnvilItem + */ + public abstract void onClick(AnvilItemClickAction action); +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/AnvilMenu.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/AnvilMenu.java new file mode 100644 index 0000000..b873404 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/AnvilMenu.java @@ -0,0 +1,582 @@ +package com.pepedevs.corelib.gui.anvil; + +import com.pepedevs.corelib.gui.anvil.action.AnvilItemClickAction; +import com.pepedevs.corelib.gui.anvil.action.AnvilMenuClickAction; +import com.pepedevs.corelib.gui.anvil.handler.AnvilMenuHandler; +import com.pepedevs.corelib.utils.reflection.bukkit.BukkitReflection; +import com.pepedevs.corelib.utils.reflection.bukkit.PlayerReflection; +import com.pepedevs.corelib.utils.reflection.general.ClassReflection; +import com.pepedevs.corelib.utils.reflection.general.ConstructorReflection; +import com.pepedevs.corelib.utils.reflection.general.FieldReflection; +import com.pepedevs.corelib.utils.reflection.general.MethodReflection; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** Class for creating custom Anvil Inventory menus. */ +public class AnvilMenu { + + private final Set openMenus; + + protected AnvilItem leftItem; + protected AnvilItem rightItem; + protected ClickAction outputAction; + private AnvilMenuHandler handler; + + /** + * Constructs the AnvilMenu. + * + *

+ * + * @param leftItem Item for the left slot + * @param rightItem Item for the right slot + */ + public AnvilMenu(AnvilItem leftItem, AnvilItem rightItem) { + this.leftItem = leftItem; + this.rightItem = rightItem; + if (this.leftItem != null) this.leftItem.menu = this; + if (this.rightItem != null) this.rightItem.menu = this; + this.openMenus = new HashSet<>(); + } + + /** Constructs the AnvilMenu. */ + public AnvilMenu() { + this(null, null); + } + + /** + * Returns the Left slot AnvilItem. + * + *

+ * + * @return Left slot AnvilItem + */ + public AnvilItem getLeftItem() { + return leftItem; + } + + /** + * Sets the Left slot AnvilItem. + * + *

+ * + * @param leftItem AnvilItem to set + * @return This Object, for chaining + */ + public AnvilMenu setLeftItem(AnvilItem leftItem) { + this.leftItem = leftItem; + if (this.leftItem != null) this.leftItem.menu = this; + return this; + } + + /** + * Returns the Right slot AnvilItem. + * + *

+ * + * @return Right slot AnvilItem + */ + public AnvilItem getRightItem() { + return rightItem; + } + + /** + * Sets the Right slot AnvilItem. + * + *

+ * + * @param rightItem AnvilItem to set + * @return This Object, for chaining + */ + public AnvilMenu setRightItem(AnvilItem rightItem) { + this.rightItem = rightItem; + if (this.rightItem != null) this.rightItem.menu = this; + return this; + } + + /** + * Returns the AnvilMenuHandler for this AnvilMenu. + * + *

+ * + * @return {@link AnvilMenuHandler} + */ + public AnvilMenuHandler getHandler() { + return handler; + } + + /** + * Sets the output slot click action. + * + *

+ * + * @param outputAction Click action for output slot of the Anvil + * @return This Object, for chaining + */ + public AnvilMenu setOutputAction(ClickAction outputAction) { + this.outputAction = outputAction; + return this; + } + + /** + * Return all open menus. + * + *

+ * + * @return All {@link OpenAnvilGui} + */ + public Set getOpenMenus() { + return openMenus; + } + + /** + * Checks if this menu is open for the given player. + * + *

+ * + * @param player Player to check for open gui + * @return true if the gui is open, false otherwise + */ + public boolean isMenuOpen(Player player) { + return player.getOpenInventory() != null + && player.getOpenInventory().getTopInventory() != null + && player.getOpenInventory().getTopInventory().getType() == InventoryType.ANVIL + && this.getOpenGuiByPlayer(player) != null; + } + + /** + * Checks if the given inventory is of this menu. + * + *

+ * + * @param inventory Inventory to check + * @return true if the inventory is of this gui, false otherwise + */ + public boolean isThisMenu(Inventory inventory) { + return inventory.getType() == InventoryType.ANVIL + && this.getOpenGuiByInventory(inventory) != null; + } + + /** + * Initializes the {@link AnvilMenuHandler} of this. + * + *

+ * + * @param plugin The plugin owner of the listener. + * @return true if not already registered. + */ + public boolean registerListener(Plugin plugin) { + if (this.handler == null) { + Bukkit.getPluginManager() + .registerEvents(this.handler = new AnvilMenuHandler(this, plugin), plugin); + return true; + } else { + return false; + } + } + + /** + * Stop handling this. + * + *

+ * + * @return false if not already registered + */ + public boolean unregisterListener() { + if (this.handler != null) { + this.handler.unregisterListener(); + this.handler = null; + return true; + } else { + return false; + } + } + + /** + * Opens a new {@link Inventory} with the contents of this {@link AnvilMenu}. + * + *

+ * + * @param player The player viewer + * @return The opened inventory + */ + public Inventory open(Player player) { + Object container = this.newContainerAnvil(player); + Inventory inventory = this.toBukkitInventory(container); + if (leftItem != null) { + inventory.setItem(Slot.INPUT_LEFT, leftItem.getDisplayIcon()); + } + if (rightItem != null) { + inventory.setItem(Slot.INPUT_RIGHT, rightItem.getDisplayIcon()); + } + + int containerId = this.getNextContainer(player); + this.sendOpenWindowPacket(player, containerId); + this.setActiveContainer(player, container); + this.setActiveContainerId(container, containerId); + this.addActiveContainerSlotListener(container, player); + this.openMenus.add(new OpenAnvilGui(player, inventory, containerId)); + return inventory; + } + + /** + * Closes the viewing inventory of the given {@link Player} only if it is equals this. + * + *

+ * + * @param player Player to close inventory + * @return true if was closed. + */ + public boolean close(Player player) { + if (this.isMenuOpen(player)) { + OpenAnvilGui gui = this.getOpenGuiByPlayer(player); + this.handleInventoryClose(player); + this.setActiveContainerDefault(player); + this.sendCloseWindowPacket(player, gui.getContainerId()); + gui.getInventory().clear(); + this.openMenus.remove(gui); + return true; + } else { + return false; + } + } + + /** + * Closes the viewing inventory of each online player only if it is equals this. + * + *

+ * + * @return This Object, for chaining + */ + public AnvilMenu closeOnlinePlayers() { + Bukkit.getOnlinePlayers().forEach(this::close); + return this; + } + + /** + * {@link AnvilMenuClickAction} for handling click in the menu. + * + *

+ * + * @param action {@link AnvilMenuClickAction} to handle for clicks in the menu + * @return This Object, for chaining + */ + public AnvilMenu onClick(AnvilMenuClickAction action) { + if (this.getHandler() == null) { + throw new UnsupportedOperationException("This menu has never been registered!"); + } else if (!action.isRightClick() && !action.isLeftClick()) { + return this; + } else if (action.getRaw_slot() == Slot.OUTPUT) { + AnvilItemClickAction clickAction = + new AnvilItemClickAction( + action.getMenu(), + action.getInventoryView(), + action.getClickType(), + action.getAction(), + action.getSlot_type(), + action.getRaw_slot(), + action.getCurrent(), + action.getHotbarKey(), + false); + this.outputAction.onClick(clickAction); + } else if (action.getRaw_slot() == Slot.INPUT_LEFT) { + AnvilItemClickAction clickAction = + new AnvilItemClickAction( + action.getMenu(), + action.getInventoryView(), + action.getClickType(), + action.getAction(), + action.getSlot_type(), + action.getRaw_slot(), + action.getCurrent(), + action.getHotbarKey(), + false); + this.leftItem.onClick(clickAction); + } else if (action.getRaw_slot() == Slot.INPUT_RIGHT) { + AnvilItemClickAction clickAction = + new AnvilItemClickAction( + action.getMenu(), + action.getInventoryView(), + action.getClickType(), + action.getAction(), + action.getSlot_type(), + action.getRaw_slot(), + action.getCurrent(), + action.getHotbarKey(), + false); + this.rightItem.onClick(clickAction); + } + return this; + } + + private OpenAnvilGui getOpenGuiByPlayer(Player player) { + for (OpenAnvilGui gui : this.openMenus) { + if (gui.getPlayer().equals(player)) return gui; + } + + return null; + } + + private OpenAnvilGui getOpenGuiByInventory(Inventory inventory) { + for (OpenAnvilGui gui : this.openMenus) { + if (gui.getInventory().equals(inventory)) return gui; + } + + return null; + } + + private int getNextContainer(Player player) { + try { + return (int) + MethodReflection.invoke( + PlayerReflection.getHandle(player), "nextContainerCounter"); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + } + return -1; + } + + private void handleInventoryClose(Player player) { + try { + Object ePlayer = PlayerReflection.getHandle(player); + Class craftEventFactory = + ClassReflection.getCraftClass("CraftEventFactory", "event"); + Class entityHuman = + ClassReflection.getNmsClass("EntityHuman", "world.entity.player"); + MethodReflection.get(craftEventFactory, "handleInventoryCloseEvent", entityHuman) + .invoke(craftEventFactory, ePlayer); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + } + } + + private void sendOpenWindowPacket(Player player, int containerId) { + try { + Class iChatComponent = + ClassReflection.getNmsClass("IChatBaseComponent", "network.chat"); + Class packetPlayOutOpenWindow = + ClassReflection.getNmsClass("PacketPlayOutOpenWindow", "network.protocol.game"); + Class chatMessage = ClassReflection.getNmsClass("ChatMessage", "network.chat"); + Class block = ClassReflection.getNmsClass("Blocks", "world.level.block"); + Object blockAnvil = FieldReflection.get(block, "ANVIL").get(null); + String blockAnvilA = (String) MethodReflection.invoke(blockAnvil, "a"); + + Object packet = + ConstructorReflection.newInstance( + packetPlayOutOpenWindow, + new Class[] {int.class, String.class, iChatComponent}, + containerId, + "minecraft:anvil", + ConstructorReflection.newInstance( + chatMessage, + new Class[] {String.class, Object[].class}, + blockAnvilA + ".name", + new Object[0])); + + BukkitReflection.sendPacket(player, packet); + } catch (IllegalAccessException + | InvocationTargetException + | NoSuchMethodException + | InstantiationException + | NoSuchFieldException e) { + e.printStackTrace(); + } + } + + private void sendCloseWindowPacket(Player player, int containerId) { + try { + Class packetPlayOutCloseWindow = + ClassReflection.getNmsClass( + "PacketPlayOutCloseWindow", "network.protocol.game"); + Object packet = + ConstructorReflection.newInstance( + packetPlayOutCloseWindow, new Class[] {int.class}, containerId); + BukkitReflection.sendPacket(player, packet); + } catch (InvocationTargetException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void setActiveContainerDefault(Player player) { + try { + Object ePlayer = PlayerReflection.getHandle(player); + FieldReflection.setValue( + ePlayer, + "activeContainer", + FieldReflection.getValue(ePlayer, "defaultContainer")); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + } + } + + private void setActiveContainer(Player player, Object container) { + try { + FieldReflection.setValue( + PlayerReflection.getHandle(player), "activeContainer", container); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void setActiveContainerId(Object container, int containerId) { + try { + FieldReflection.setValue(container, "windowId", containerId); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void addActiveContainerSlotListener(Object container, Player player) { + try { + Class iCrafting = ClassReflection.getNmsClass("ICrafting", "world.inventory"); + MethodReflection.invoke( + container, + "addSlotListener", + new Class[] {iCrafting}, + PlayerReflection.getHandle(player)); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + e.printStackTrace(); + } + } + + private Inventory toBukkitInventory(Object container) { + try { + return ((InventoryView) MethodReflection.invoke(container, "getBukkitView")) + .getTopInventory(); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + e.printStackTrace(); + } + return null; + } + + private Object newContainerAnvil(Player player) { + try { + Object ePlayer = PlayerReflection.getHandle(player); + Class containerAnvil = + ClassReflection.getNmsClass("ContainerAnvil", "world.inventory"); + Class playerInventory = + ClassReflection.getNmsClass("PlayerInventory", "world.entity.player"); + Class world = ClassReflection.getNmsClass("World", "world.level"); + Class blockPosition = ClassReflection.getNmsClass("BlockPosition", "core"); + Class entityHuman = + ClassReflection.getNmsClass("EntityHuman", "world.entity.player"); + Object anvil = + ConstructorReflection.newInstance( + containerAnvil, + new Class[] {playerInventory, world, blockPosition, entityHuman}, + FieldReflection.getValue(ePlayer, "inventory"), + FieldReflection.getValue(ePlayer, "world"), + ConstructorReflection.newInstance( + blockPosition, + new Class[] {int.class, int.class, int.class}, + 0, + 0, + 0), + ePlayer); + FieldReflection.setValue(anvil, "checkReachable", false); + return anvil; + } catch (InvocationTargetException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException + | NoSuchFieldException e) { + e.printStackTrace(); + } + + return null; + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (this.getClass() != obj.getClass()) { + return false; + } else { + AnvilMenu other = (AnvilMenu) obj; + if (!Objects.equals(this.leftItem, other.leftItem) + || !Objects.equals(this.rightItem, other.rightItem)) { + return false; + } else { + if (this.handler == null) { + if (other.handler != null) { + return false; + } + } else if (!this.handler.equals(other.handler)) { + return false; + } + + return true; + } + } + } + + public static class OpenAnvilGui { + + private Player player; + private Inventory inventory; + private int containerId; + + protected OpenAnvilGui(Player player, Inventory inventory, int containerId) { + this.player = player; + this.inventory = inventory; + this.containerId = containerId; + } + + public Player getPlayer() { + return player; + } + + public Inventory getInventory() { + return inventory; + } + + public int getContainerId() { + return containerId; + } + } + + public static class Slot { + + /** + * The slot on the far left, where the first input is inserted. An {@link ItemStack} is + * always inserted here to be renamed + */ + public static final int INPUT_LEFT = 0; + /** + * Not used, but in a real anvil you are able to put the second item you want to combine + * here + */ + public static final int INPUT_RIGHT = 1; + /** + * The output slot, where an item is put when two items are combined from {@link + * #INPUT_LEFT} and {@link #INPUT_RIGHT} or {@link #INPUT_LEFT} is renamed + */ + public static final int OUTPUT = 2; + + private static final int[] values = + new int[] {Slot.INPUT_LEFT, Slot.INPUT_RIGHT, Slot.OUTPUT}; + + /** + * Get all anvil slot values + * + * @return The array containing all possible anvil slots + */ + public static int[] values() { + return values; + } + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/ClickAction.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/ClickAction.java new file mode 100644 index 0000000..cdfd864 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/ClickAction.java @@ -0,0 +1,15 @@ +package com.pepedevs.corelib.gui.anvil; + +import com.pepedevs.corelib.gui.anvil.action.AnvilItemClickAction; + +public interface ClickAction { + + /** + * Action triggered when a gui slot is clicked. + * + *

+ * + * @param action {@link AnvilItemClickAction} + */ + void onClick(AnvilItemClickAction action); +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/action/AnvilItemClickAction.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/action/AnvilItemClickAction.java new file mode 100644 index 0000000..2c48642 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/action/AnvilItemClickAction.java @@ -0,0 +1,115 @@ +package com.pepedevs.corelib.gui.anvil.action; + +import com.pepedevs.corelib.gui.anvil.AnvilMenu; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +/** Class for managing AnvilItem click action in menu. */ +public class AnvilItemClickAction { + + protected final AnvilMenu menu; + protected final InventoryView inventory_view; + protected final ClickType click_type; + protected final InventoryAction action; + protected final InventoryType.SlotType slot_type; + protected final int raw_slot; + protected final ItemStack clicked; + protected int hotbar_key; + protected boolean update; + + /** + * Constructs the Item Click Action. + * + *

+ * + * @param menu AnvilItem to bind action + * @param event {@link InventoryClickEvent} + */ + public AnvilItemClickAction(AnvilMenu menu, InventoryClickEvent event) { + this( + menu, + event.getView(), + event.getClick(), + event.getAction(), + event.getSlotType(), + event.getRawSlot(), + event.getCurrentItem(), + event.getHotbarButton(), + false); + } + + public AnvilItemClickAction( + AnvilMenu menu, + InventoryView view, + ClickType click_type, + InventoryAction action, + InventoryType.SlotType slot_type, + int raw_slot, + ItemStack clicked, + int hotbar_key, + boolean update) { + this.menu = menu; + this.inventory_view = view; + this.click_type = click_type; + this.action = action; + this.slot_type = slot_type; + this.raw_slot = raw_slot; + this.clicked = clicked; + this.hotbar_key = hotbar_key; + this.update = update; + } + + public AnvilMenu getMenu() { + return menu; + } + + public Inventory getInventory() { + return this.getInventoryView().getTopInventory(); + } + + public InventoryView getInventoryView() { + return this.inventory_view; + } + + public Player getPlayer() { + return (Player) this.getInventoryView().getPlayer(); + } + + public ClickType getClickType() { + return this.click_type; + } + + public InventoryAction getInventoryAction() { + return this.action; + } + + public InventoryType.SlotType getSlotType() { + return this.slot_type; + } + + public int getRawSlot() { + return this.raw_slot; + } + + public ItemStack getClickedItem() { + return this.clicked; + } + + public int getHotbarKey() { + return this.hotbar_key; + } + + public boolean isWillUpdate() { + return this.update; + } + + public void setUpdate(boolean update) { + this.update = update; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/action/AnvilMenuClickAction.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/action/AnvilMenuClickAction.java new file mode 100644 index 0000000..58c5ff8 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/action/AnvilMenuClickAction.java @@ -0,0 +1,100 @@ +package com.pepedevs.corelib.gui.anvil.action; + +import com.pepedevs.corelib.gui.anvil.AnvilMenu; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +public class AnvilMenuClickAction { + + protected final AnvilMenu menu; + protected final InventoryView inventory_view; + protected final ClickType click_type; + protected final InventoryAction action; + protected final InventoryType.SlotType slot_type; + protected final int raw_slot; + protected ItemStack current; + protected int hotbar_key; + + public AnvilMenuClickAction(AnvilMenu menu, InventoryClickEvent event) { + this( + menu, + event.getView(), + event.getClick(), + event.getAction(), + event.getSlotType(), + event.getCurrentItem(), + event.getRawSlot(), + event.getHotbarButton()); + } + + public AnvilMenuClickAction( + AnvilMenu menu, + InventoryView view, + ClickType click_type, + InventoryAction action, + InventoryType.SlotType slot_type, + ItemStack current, + int raw_slot, + int hotbar_key) { + this.menu = menu; + this.inventory_view = view; + this.click_type = click_type; + this.action = action; + this.slot_type = slot_type; + this.current = current; + this.raw_slot = raw_slot; + this.hotbar_key = hotbar_key; + } + + public AnvilMenu getMenu() { + return menu; + } + + public InventoryView getInventoryView() { + return inventory_view; + } + + public ClickType getClickType() { + return click_type; + } + + public InventoryAction getAction() { + return action; + } + + public InventoryType.SlotType getSlot_type() { + return slot_type; + } + + public ItemStack getCursor() { + return this.getInventoryView().getCursor(); + } + + public ItemStack getCurrent() { + return current; + } + + public int getRaw_slot() { + return raw_slot; + } + + public int getHotbarKey() { + return hotbar_key; + } + + public boolean isRightClick() { + return this.getClickType().isRightClick(); + } + + public boolean isLeftClick() { + return this.getClickType().isLeftClick(); + } + + public boolean isShiftClick() { + return this.getClickType().isShiftClick(); + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/handler/AnvilMenuHandler.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/handler/AnvilMenuHandler.java new file mode 100644 index 0000000..c9b7a94 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/anvil/handler/AnvilMenuHandler.java @@ -0,0 +1,71 @@ +package com.pepedevs.corelib.gui.anvil.handler; + +import com.pepedevs.corelib.gui.anvil.AnvilMenu; +import com.pepedevs.corelib.gui.anvil.action.AnvilMenuClickAction; +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.inventory.InventoryType; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.plugin.Plugin; + +public class AnvilMenuHandler implements Listener { + + protected final AnvilMenu menu; + protected final Plugin plugin; + + public AnvilMenuHandler(AnvilMenu menu, Plugin plugin) { + this.menu = menu; + this.plugin = plugin; + } + + public Plugin getPlugin() { + return plugin; + } + + /** Unregisters this listener. */ + public void unregisterListener() { + HandlerList.unregisterAll(this); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onInventoryClick(InventoryClickEvent event) { + if (event.getInventory() != null + && event.getInventory().getType() == InventoryType.ANVIL + && event.getWhoClicked() instanceof Player + && this.menu.isMenuOpen((Player) event.getWhoClicked()) + && this.menu.isThisMenu(event.getInventory())) { + AnvilMenuClickAction action = new AnvilMenuClickAction(this.menu, event); + this.menu.onClick(action); + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW) + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getInventory() != null + && event.getInventory().getType() == InventoryType.ANVIL + && event.getPlayer() instanceof Player + && this.menu.isMenuOpen((Player) event.getPlayer()) + && this.menu.isThisMenu(event.getInventory())) { + for (AnvilMenu.OpenAnvilGui gui : this.menu.getOpenMenus()) { + if (gui.getPlayer().equals(event.getPlayer())) { + this.menu.getOpenMenus().remove(gui); + break; + } + } + event.getInventory().clear(); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPluginDisable(PluginDisableEvent event) { + if (event.getPlugin().equals(this.plugin)) { + this.menu.closeOnlinePlayers(); + } + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/Item.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/Item.java new file mode 100644 index 0000000..0ff1ca2 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/Item.java @@ -0,0 +1,210 @@ +package com.pepedevs.corelib.gui.inventory; + +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.utils.StringUtils; +import com.pepedevs.corelib.utils.itemstack.ItemMetaBuilder; +import com.pepedevs.corelib.utils.itemstack.ItemStackUtils; +import com.pepedevs.corelib.utils.material.MaterialUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** Class for creating custom Inventory items. */ +public abstract class Item { + + protected String name; + protected ItemStack icon; + protected List lore; + protected ItemMenu menu; + + /** + * Constructs the Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon for the Item + * @param lore Lore of the Item + */ + public Item(String name, ItemStack icon, Collection lore) { + Validate.notNull(icon, "The icon cannot be null!"); + this.name = name == null ? "" : name; + this.icon = icon; + this.lore = new ArrayList<>(lore); + } + + /** + * Constructs the Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon for the Item + * @param lore Lore of the Item + */ + public Item(String name, ItemStack icon, String... lore) { + this(name, icon, Arrays.asList(lore)); + } + + /** + * Constructs the Item. + * + *

+ * + * @param icon ItemStack icon for the Item + */ + public Item(ItemStack icon) { + this( + StringUtils.defaultIfBlank( + (icon.getItemMeta() != null ? icon.getItemMeta().getDisplayName() : null), + "null name"), + icon, + (ItemStackUtils.extractLore(icon, false) + .toArray(new String[ItemStackUtils.extractLore(icon, false).size()]))); + } + + /** + * Returns the name of the Item. + * + *

+ * + * @return Name of the Item + */ + public String getName() { + return name; + } + + /** + * Sets the name of the Item + * + *

+ * + * @param name Name for the Item + * @return This Object, for chaining + */ + public Item setName(String name) { + this.name = name; + return this; + } + + /** + * Returns the icon of the Item. + * + *

+ * + * @return Icon of the Item + */ + public ItemStack getIcon() { + return icon; + } + + /** + * Sets the icon of the Item + * + *

+ * + * @param icon ItemStack icon for the Item + * @return This Object, for chaining + */ + public Item setIcon(ItemStack icon) { + Validate.notNull(icon, "The icon cannot be null!"); + this.icon = icon; + return this; + } + + /** + * Returns the display icon of the Item. + * + *

+ * + * @return Display icon of the Item + */ + public ItemStack getDisplayIcon() { + return (getIcon().getType() == Material.AIR + ? icon + : new ItemMetaBuilder(MaterialUtils.getRightMaterial(getIcon())) + .withDisplayName(StringUtils.translateAlternateColorCodes(getName())) + .withLore(getLore()) + .applyTo(getIcon().clone())); + } + + /** + * Returns the lore of the Item. + * + *

+ * + * @return Lore of the Item + */ + public List getLore() { + return lore; + } + + /** + * Sets the name of the Item + * + *

+ * + * @param lore Lore for the Item + * @return This Object, for chaining + */ + public Item setLore(List lore) { + this.lore = lore != null ? lore : new ArrayList(); + return this; + } + + /** + * Gets the menu this item belongs. + * + *

Note that this will return null if this item has never been set in a + * menu. + * + *

+ * + * @return The menu this item belongs + */ + public ItemMenu getMenu() { + return menu; + } + + /** + * Click trigger for this Item. + * + *

+ * + * @param action {@link ItemClickAction} for the Item + */ + public abstract void onClick(final ItemClickAction action); + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((icon == null) ? 0 : icon.hashCode()); + result = prime * result + ((lore == null) ? 0 : lore.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Item other = (Item) obj; + if (icon == null) { + if (other.icon != null) return false; + } else if (!icon.equals(other.icon)) return false; + if (lore == null) { + if (other.lore != null) return false; + } else if (!lore.equals(other.lore)) return false; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + return true; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/ItemMenu.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/ItemMenu.java new file mode 100644 index 0000000..5dbabcb --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/ItemMenu.java @@ -0,0 +1,698 @@ +package com.pepedevs.corelib.gui.inventory; + +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.gui.inventory.action.ItemMenuClickAction; +import com.pepedevs.corelib.gui.inventory.handler.ItemMenuHandler; +import com.pepedevs.corelib.gui.inventory.holder.ItemMenuHolder; +import com.pepedevs.corelib.gui.inventory.size.ItemMenuSize; +import com.pepedevs.corelib.utils.StringUtils; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.plugin.Plugin; + +import java.util.Arrays; +import java.util.List; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Class for creating custom Inventory menus. */ +public class ItemMenu { + + /* + Default title + */ + public static final String DEFAULT_TITLE = "Empty Title"; + protected final Item[] contents; + protected String title; + protected ItemMenuSize size; + protected ItemMenu parent; + protected ItemMenuHandler handler; + + /** + * Constructs the ItemMenu. + * + *

+ * + * @param title Title of the ItemMenu + * @param size Size of ItemMenu + * @param parent Parent ItemMenu if any, else null + * @param contents Contents of the ItemMenu + */ + public ItemMenu(String title, ItemMenuSize size, ItemMenu parent, Item... contents) { + Validate.notNull(size, "The size cannot be null!"); + this.title = StringUtils.defaultIfBlank(title, DEFAULT_TITLE); + this.size = size; + this.parent = parent; + this.contents = new Item[size.getSize()]; + fill(contents); + } + + /** + * Returns the title of the ItemMenu. + * + *

+ * + * @return Title of the ItemMenu + */ + public String getTitle() { + return title; + } + + /** + * Sets the title of this menu. + * + *

Note that all the Bukkit inventories created from this, must be re-opened to see the + * changes. + * + *

+ * + * @param title New title + * @return This Object, for chaining + */ + public ItemMenu setTitle(String title) { + this.title = StringUtils.defaultIfBlank(title, DEFAULT_TITLE); + return this; + } + + /** + * Returns the size of the ItemMenu. + * + *

+ * + * @return Size of the ItemMenu + */ + public ItemMenuSize getSize() { + return size; + } + + /** + * Returns the contents of the ItemMenu. + * + *

+ * + * @return Contents of the ItemMenu + */ + public Item[] getContents() { + return contents; + } + + /** + * Adds the contents to this menu. + * + *

Note that all the Bukkit inventories created from this, must be re-opened to see the + * changes. + * + *

+ * + * @param contents Contents for this menu + * @return This Object, for chaining + */ + public ItemMenu setContents(Item[] contents) { + fill(contents); + return this; + } + + /** + * Returns the contents of the ItemMenu as stream. + * + *

+ * + * @return Stream of Contents of the ItemMenu + */ + public Stream getContentsStream() { + return Arrays.stream(getContents()); + } + + /** + * Returns the filtered contents of ItemMenu with a Predicate filter. + * + *

+ * + * @param predicate_filter Filter for the contents of ItemMenu + * @return Filtered contents + */ + public Item[] getContents(Predicate predicate_filter) { + List filtered = + getContentsStream().filter(predicate_filter).collect(Collectors.toList()); + return filtered.toArray(new Item[filtered.size()]); + } + + /** + * Returns the parent ItemMenu for this ItemMenu. + * + *

+ * + * @return Parent ItemMenu for this ItemMenu + */ + public ItemMenu getParent() { + return parent; + } + + /** + * Sets the parent of this menu. + * + *

Note that all the Bukkit inventories created from this, must be re-opened to see the + * changes. + * + *

+ * + * @param parent New parent menu + * @return This Object, for chaining + */ + public ItemMenu setParent(ItemMenu parent) { + this.parent = parent; + return this; + } + + /** + * Checks of this ItemMenu has a parent ItemMenu. + * + *

+ * + * @return {@code true} if it has a parent ItemMenu, else false + */ + public boolean hasParent() { + return getParent() != null; + } + + /** + * Returns the ItemMenuHandler for this ItemMenu. + * + *

+ * + * @return {@link ItemMenuHandler} + */ + public ItemMenuHandler getHandler() { + return this.handler; + } + + /** + * Returns the Item on the given index. + * + *

+ * + * @param index Index of the Item + * @return {@link Item} at the given index + */ + public Item getItem(int index) { + rangeCheck(index, index); + return this.contents[index]; + } + + /** + * Returns an array of index of the Items with the given Predicate filter. + * + *

+ * + * @param predicate_filter Predicate filter for the Items + * @return Array of index of the Items + */ + public Integer[] getIndexes(Predicate predicate_filter) { + TreeSet set = new TreeSet<>(); + for (int i = 0; i < getContents().length; i++) { + if (predicate_filter == null || predicate_filter.test(getItem(i))) { + set.add(i); + } + } + return set.toArray(new Integer[set.size()]); + } + + /** + * Returns the first empty slot. + * + *

+ * + * @return First empty slot found, or -1 if no empty slots. + */ + public int getFirstEmpty() { + return getEmptyIndexes().length > 0 ? getEmptyIndexes()[0] : -1; + } + + /** + * Returns the empty slot indexes. + * + *

+ * + * @return Empty slot indexes found. + */ + public Integer[] getEmptyIndexes() { + return getIndexes( + content -> + content == null + || content.getDisplayIcon() == null + || content.getDisplayIcon().getType() == Material.AIR); + } + + /** + * Checks if the ItemMenu if full or has no empty slots. + * + *

+ * + * @return {@code true} if its full, else false + */ + public boolean isFull() { + return getEmptyIndexes().length == 0; + } + + /** + * Checks if the ItemMenu is empty or has no content. + * + *

+ * + * @return {@code true} if its empty, else false + */ + public boolean isEmpty() { + return getEmptyIndexes().length == this.getContents().length; + } + + // public ItemMenu setSize(ItemMenuSize size) { + // Validate.notNull(size, "The size cannot be null!"); + // this.size = size; + // return this; + // } + + /** + * Returns true if the opened inventory that the given {@link Player} has is equals this. + * + *

+ * + * @param player Player with the opened inventory + * @return true if the opened inventory that the given {@link Player} has is this + */ + public boolean isMenuOpen(Player player) { + if (player.getOpenInventory() == null + || player.getOpenInventory().getTopInventory() == null + || player.getOpenInventory().getTopInventory().getType() != InventoryType.CHEST + || !(player.getOpenInventory().getTopInventory().getHolder() + instanceof ItemMenuHolder)) { + return false; + } + return this.equals( + ((ItemMenuHolder) player.getOpenInventory().getTopInventory().getHolder()) + .getItemMenu()); + } + + /** + * Gets whether the provided {@link Inventory} is an instance of this {@link ItemMenu}.
+ * + * @param inventory The inventory to check + * @return true if the provided {@link Inventory} is an instance of this {@link ItemMenu} + */ + public boolean isThisMenu(Inventory inventory) { + if (inventory.getType() == InventoryType.CHEST + && inventory.getHolder() instanceof ItemMenuHolder) { + return equals(((ItemMenuHolder) inventory.getHolder()).getItemMenu()); + } else { + return false; + } + } + + /** + * Sets the item to the given slot in this menu. + * + *

Note that all the Bukkit inventories created from this, must be re-opened to see the + * changes. + * + *

+ * + * @param slot Index of slot to set item to + * @param content Item to set to the menu + * @return This Object, for chaining + */ + public ItemMenu setItem(int slot, Item content) { + setItemIf(slot, content, null); + return this; + } + + /** + * Sets the item on the given slot. + * + *

+ * + * @param slot Index of slot where the given item will be stored + * @param item Item to store + * @param predicate Your predicate expression + * @return true if was set. + */ + public boolean setItemIf(int slot, Item item, Predicate predicate) { + rangeCheck(slot, slot); + if (predicate == null || predicate.test(this.getItem(slot))) { + this.contents[slot] = item; + + if (item != null) { + item.menu = this; + } + return true; + } + return false; + } + + /** + * Stores the given {@link Item} only if not full. + * + *

+ * + * @param item Item to add + * @return true if could be added + */ + public boolean addItem(Item item) { + if (!isFull()) { + this.setItem(getFirstEmpty(), item); + return true; + } + return false; + } + + /** + * Clear the item stored on the given slot. + * + *

+ * + * @param slot Index of slot where the item is stored + * @return true if was clear + */ + public boolean clearItem(int slot) { + return clearItemIf(slot, null); + } + + /** + * Clear the item stored on the given slot. + * + *

+ * + * @param slot Index of slot where the item is stored + * @param predicate Your predicate expression + * @return true if was clear. + */ + public boolean clearItemIf(int slot, Predicate predicate) { + return setItemIf(slot, null, predicate); + } + + /** + * Fill the items to the menu. + * + *

+ * + * @param contents Items to store + * @return This Object, for chaining + */ + public ItemMenu fill(Item... contents) { + fill(0, contents); + return this; + } + + /** + * Fill all slots with the given Item. + * + *

+ * + * @param content Item to store + * @return This Object, for chaining + */ + public ItemMenu fillToAll(Item content) { + return fillToAllIf(content, null); + } + + /** + * Fill all slots with the given Item according to the predicate. + * + *

+ * + * @param content Item to store + * @param predicate Your predicate expression + * @return This Object, for chaining + */ + public ItemMenu fillToAllIf(Item content, Predicate predicate) { + for (int i = 0; i < this.getContents().length; i++) { + if (predicate == null || predicate.test(this.contents[i])) { + this.setItem(i, content); + } + } + return this; + } + + /** + * Fill the Items to the menu from the given index. + * + *

+ * + * @param from_index Index from which to start + * @param contents Items to store + * @return This Object, for chaining + */ + public ItemMenu fill(int from_index, Item... contents) { + rangeCheck(from_index, from_index); + for (int i = from_index; i < this.getContents().length; i++) { + if (contents == null || i >= contents.length) { + break; + } + this.setItem(i, contents[i]); + } + return this; + } + + /** + * Clear all the Items in the menu. + * + *

+ * + * @return This Object, for chaining + */ + public ItemMenu clear() { + return fillToAll(null); + } + + public Inventory apply(Inventory inventory) { + for (int i = 0; i < getContents().length; i++) { + if (i >= inventory.getSize()) { + break; + } + + if (getItem(i) != null) { + inventory.setItem(i, getItem(i).getDisplayIcon()); + } + } + return inventory; + } + + /** + * Initializes the {@link ItemMenuHandler} of this. + * + *

+ * + * @param plugin The plugin owner of the listener. + * @return true if not already registered. + */ + public boolean registerListener(Plugin plugin) { + if (this.handler == null) { + Bukkit.getPluginManager() + .registerEvents((this.handler = new ItemMenuHandler(this, plugin)), plugin); + return true; + } + return false; + } + + /** + * Stop handling this. + * + *

+ * + * @return false if not already registered + */ + public boolean unregisterListener() { + if (this.handler != null) { + this.handler.unregisterListener(); + this.handler = null; + return true; + } + return false; + } + + /** + * Opens a new {@link Inventory} with the contents of this {@link ItemMenu}. + * + *

+ * + * @param player The player viewer + * @return The opened inventory + */ + public Inventory open(Player player) { + Inventory inventory = + apply( + Bukkit.createInventory( + new ItemMenuHolder( + this, Bukkit.createInventory(player, size.getSize())), + size.getSize(), + StringUtils.limit( + StringUtils.translateAlternateColorCodes(getTitle()), 32))); + player.closeInventory(); + player.openInventory(inventory); + return inventory; + } + + /** + * Updates the viewing inventory of the given {@link Player} only if it is equals this. + * + *

+ * + * @param player Player to update inventory view + * @return true if was updated + */ + public boolean update(Player player) { + if (isMenuOpen(player)) { + apply(player.getOpenInventory().getTopInventory()); + player.updateInventory(); + return true; + } + return false; + } + + /** + * Updates the viewing inventory of each online player only if it is equals this. + * + *

+ * + * @return This Object, for chaining + */ + public ItemMenu updateOnlinePlayers() { + Bukkit.getOnlinePlayers().forEach(player -> update(player)); + return this; + } + + /** + * Closes the viewing inventory of the given {@link Player} only if it is equals this. + * + *

+ * + * @param player Player to close inventory + * @return true if was closed. + */ + public boolean close(Player player) { + if (isMenuOpen(player)) { + player.closeInventory(); + return true; + } + return false; + } + + /** + * Closes the viewing inventory of each online player only if it is equals this. + * + *

+ * + * @return This Object, for chaining + */ + public ItemMenu closeOnlinePlayers() { + Bukkit.getOnlinePlayers().forEach(player -> close(player)); + return this; + } + + /** + * {@link ItemMenuClickAction} for handling click in the menu. + * + *

+ * + * @param action {@link ItemMenuClickAction} to handle for clicks in the menu + * @return This Object, for chaining + */ + public ItemMenu onClick(final ItemMenuClickAction action) { + if (this.getHandler() == null) { + throw new UnsupportedOperationException("This menu has never been registered!"); + } + + if (!action.isRightClick() && !action.isLeftClick()) { + return this; + } + + if (action.getSlot() < 0 + || action.getSlot() >= getContents().length + || this.getItem(action.getSlot()) == null) { + return this; + } + + ItemClickAction sub_action = + new ItemClickAction( + this, + action.getInventoryView(), + action.getClickType(), + action.getInventoryAction(), + action.getSlotType(), + action.getSlot(), + action.getRawSlot(), + action.getCurrentItem(), + action.getHotbarKey(), + false, + false, + false); + + /* calling click method on the content class */ + getItem(action.getSlot()).onClick(sub_action); + + /* just update the view */ + if (sub_action.isWillUpdate()) { + getHandler().delayedUpdate(action.getPlayer(), 1); + // update(action.getPlayer()); + } else if (sub_action.isWillClose() + || sub_action.isWillGoBack()) { // closing or getting back + getHandler().delayedClose(action.getPlayer(), 1); + + /* opening parent */ + if (sub_action.isWillGoBack() && hasParent()) { + getParent().getHandler().delayedOpen(action.getPlayer(), 3); + } + } + return this; + } + + protected void rangeCheck(int from, int to) { + if (from > to) { + throw new IllegalArgumentException("from(" + from + ") > to(" + to + ")!"); + } + + if (from < 0) { + throw new ArrayIndexOutOfBoundsException(from); + } + + if (to >= getContents().length) { + throw new ArrayIndexOutOfBoundsException(to); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(contents); + result = prime * result + ((handler == null) ? 0 : handler.hashCode()); + result = prime * result + ((parent == null) ? 0 : parent.hashCode()); + result = prime * result + ((size == null) ? 0 : size.hashCode()); + result = prime * result + ((title == null) ? 0 : title.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ItemMenu other = (ItemMenu) obj; + if (!Arrays.equals(contents, other.contents)) return false; + if (handler == null) { + if (other.handler != null) return false; + } else if (!handler.equals(other.handler)) return false; + if (parent == null) { + if (other.parent != null) return false; + } else if (!parent.equals(other.parent)) return false; + if (size != other.size) return false; + if (title == null) { + if (other.title != null) return false; + } else if (!title.equals(other.title)) return false; + return true; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/action/ItemClickAction.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/action/ItemClickAction.java new file mode 100644 index 0000000..e30d20d --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/action/ItemClickAction.java @@ -0,0 +1,296 @@ +package com.pepedevs.corelib.gui.inventory.action; + +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType.SlotType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +/** Class for managing Item click action in menu. */ +public class ItemClickAction { + + protected final ItemMenu menu; + protected final InventoryView inventory_view; + protected final ClickType click_type; + protected final InventoryAction action; + protected final SlotType slot_type; + protected final int slot; + protected final int raw_slot; + protected final ItemStack clicked; + protected int hotbar_key; + protected boolean go_back; + protected boolean close; + protected boolean update; + + /** + * Constructs the Item Click Action. + * + *

+ * + * @param menu Item to bind action + * @param event {@link InventoryClickEvent} + */ + public ItemClickAction(ItemMenu menu, InventoryClickEvent event) { + this( + menu, + event.getView(), + event.getClick(), + event.getAction(), + event.getSlotType(), + event.getSlot(), + event.getRawSlot(), + event.getCurrentItem(), + event.getHotbarButton(), + false, + false, + false); + } + + /** + * Constructs the Item Click Action. + * + *

+ * + * @param menu ItemMenu to bind action + * @param inventory_view Inventory View of the open inventory + * @param click_type Click type + * @param action InventoryAction type + * @param slot_type {@link SlotType} + * @param slot Slot index + * @param raw_slot Raw slot index + * @param clicked Clicked ItemStack + * @param hotbar_key Hotbar Key + * @param go_back Go back + * @param close Close + * @param update Update + */ + public ItemClickAction( + ItemMenu menu, + InventoryView inventory_view, + ClickType click_type, + InventoryAction action, + SlotType slot_type, + int slot, + int raw_slot, + ItemStack clicked, + int hotbar_key, + boolean go_back, + boolean close, + boolean update) { + this.menu = menu; + this.inventory_view = inventory_view; + this.click_type = click_type; + this.action = action; + this.slot_type = slot_type; + this.slot = slot; + this.raw_slot = raw_slot; + this.clicked = clicked; + this.hotbar_key = hotbar_key; + this.go_back = go_back; + this.close = close; + this.update = update; + } + + /** + * Returns the menu. + * + *

+ * + * @return ItemMenu + */ + public ItemMenu getMenu() { + return menu; + } + + /** + * Returns the Inventory. + * + *

+ * + * @return Inventory + */ + public Inventory getInventory() { + return this.getInventoryView().getTopInventory(); + } + + /** + * Returns the InventoryView. + * + *

+ * + * @return InventoryView + */ + public InventoryView getInventoryView() { + return inventory_view; + } + + /** + * Returns the Player who clicked. + * + *

+ * + * @return Player who clicked + */ + public Player getPlayer() { + return (Player) this.getInventoryView().getPlayer(); + } + + /** + * Returns the ClickType. + * + *

+ * + * @return ClickType + */ + public ClickType getClickType() { + return click_type; + } + + /** + * Returns the InventoryAction. + * + *

+ * + * @return InventoryAction + */ + public InventoryAction getInventoryAction() { + return action; + } + + /** + * Returns the SlotType. + * + *

+ * + * @return SlotType + */ + public SlotType getSlotType() { + return slot_type; + } + + /** + * Returns the slot index. + * + *

+ * + * @return Slot index + */ + public int getSlot() { + return slot; + } + + /** + * Returns the raw slot index. + * + *

+ * + * @return Raw slot index + */ + public int getRawSlot() { + return raw_slot; + } + + /** + * Returns the clicked ItemStack. + * + *

+ * + * @return Clicked ItemStack + */ + public ItemStack getClickedItem() { + return clicked; + } + + /** + * Returns the Hotbar key. + * + *

+ * + * @return Hotbar Key + */ + public int getHotbarKey() { + return hotbar_key; + } + + /** + * Returns the Go back boolean. + * + *

+ * + * @return if it will go back + */ + public boolean isWillGoBack() { + return go_back; + } + + /** + * Returns the Close boolean. + * + *

+ * + * @return if it will close + */ + public boolean isWillClose() { + return close; + } + + /** + * Returns the Update boolean. + * + *

+ * + * @return if it will update + */ + public boolean isWillUpdate() { + return update; + } + + /** + * Sets the Go back boolean. + * + *

+ * + * @param go_back Boolean value + */ + public void setGoBack(boolean go_back) { + this.go_back = go_back; + if (go_back) { + this.close = false; + this.update = false; + } + } + + /** + * Sets the Close boolean. + * + *

+ * + * @param close Boolean value + */ + public void setClose(boolean close) { + this.close = close; + if (close) { + this.go_back = false; + this.update = false; + } + } + + /** + * Sets the Update boolean. + * + *

+ * + * @param update Boolean value + */ + public void setUpdate(boolean update) { + this.update = update; + if (update) { + this.go_back = false; + this.close = false; + } + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/action/ItemMenuClickAction.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/action/ItemMenuClickAction.java new file mode 100644 index 0000000..e032687 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/action/ItemMenuClickAction.java @@ -0,0 +1,270 @@ +package com.pepedevs.corelib.gui.inventory.action; + +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType.SlotType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +/** Class for managing ItemMenu click action in menu. */ +public class ItemMenuClickAction { + + protected final ItemMenu menu; + protected final InventoryView inventory_view; + protected final ClickType click_type; + protected final InventoryAction action; + protected final SlotType slot_type; + protected final int slot; + protected final int raw_slot; + protected ItemStack current; + protected int hotbar_key; + + /** + * Constructs the Item Menu Click Action. + * + *

+ * + * @param menu ItemMenu to bind action + * @param event {@link InventoryClickEvent} + */ + public ItemMenuClickAction(ItemMenu menu, InventoryClickEvent event) { + this( + menu, + event.getView(), + event.getClick(), + event.getAction(), + event.getSlotType(), + event.getSlot(), + event.getRawSlot(), + event.getCurrentItem(), + event.getHotbarButton()); + } + + /** + * Constructs the Item Menu Click Action. + * + *

+ * + * @param menu ItemMenu to bind action + * @param inventory_view Inventory View of the open inventory + * @param click_type Click type + * @param action InventoryAction type + * @param slot_type {@link SlotType} + * @param slot Slot index + * @param raw_slot Raw slot index + * @param current Current clicked ItemStack + * @param hotbar_key Hotbar Key + */ + public ItemMenuClickAction( + ItemMenu menu, + InventoryView inventory_view, + ClickType click_type, + InventoryAction action, + SlotType slot_type, + int slot, + int raw_slot, + ItemStack current, + int hotbar_key) { + this.menu = menu; + this.inventory_view = inventory_view; + this.click_type = click_type; + this.action = action; + this.slot_type = slot_type; + this.slot = slot; + this.raw_slot = raw_slot; + this.current = current; + this.hotbar_key = hotbar_key; + } + + /** + * Returns the current menu. + * + *

+ * + * @return ItemMenu + */ + public ItemMenu getMenu() { + return menu; + } + + /** + * Returns the current inventory. + * + *

+ * + * @return Inventory + */ + public Inventory getInventory() { + return this.getInventoryView().getTopInventory(); + } + + /** + * Returns the current InventoryView. + * + *

+ * + * @return InventoryView + */ + public InventoryView getInventoryView() { + return inventory_view; + } + + /** + * Returns the Player who clicked. + * + *

+ * + * @return Player who clicked + */ + public Player getPlayer() { + return (Player) this.getInventoryView().getPlayer(); + } + + /** + * Returns the ClickType. + * + *

+ * + * @return ClickType + */ + public ClickType getClickType() { + return click_type; + } + + /** + * Returns the {@link InventoryAction}. + * + *

+ * + * @return {@link InventoryAction} + */ + public InventoryAction getInventoryAction() { + return action; + } + + /** + * Returns the {@link SlotType}. + * + *

+ * + * @return {@link SlotType} + */ + public SlotType getSlotType() { + return slot_type; + } + + /** + * Returns the slot index. + * + *

+ * + * @return Slot index + */ + public int getSlot() { + return slot; + } + + /** + * Returns the raw slot index. + * + *

+ * + * @return Raw slot index + */ + public int getRawSlot() { + return raw_slot; + } + + /** + * Returns the current ItemStack on the cursor. + * + *

+ * + * @return Cursor ItemStack + */ + public ItemStack getCursor() { + return getInventoryView().getCursor(); + } + + /** + * Sets the item on the cursor. + * + *

+ * + * @param stack New cursor item + * @deprecated This changes the ItemStack in their hand before any calculations are applied to + * the Inventory, which has a tendency to create inconsistencies between the Player and the + * server, and to make unexpected changes in the behavior of the clicked Inventory. + */ + @Deprecated + public void setCursor(ItemStack stack) { + getInventoryView().setCursor(stack); + } + + /** + * Returns the ItemStack currently in the clicked slot. + * + *

+ * + * @return Item in the clicked + */ + public ItemStack getCurrentItem() { + return current; + } + + /** + * Sets the ItemStack currently in the clicked slot. + * + *

+ * + * @param current Item to be placed in the current slot + */ + public void setCurrentItem(ItemStack current) { + this.current = current; + } + + public int getHotbarKey() { + return hotbar_key; + } + + /** + * Returns whether or not the ClickType for this event represents a right click. + * + *

+ * + * @return true if the ClickType uses the right mouse button + * @see ClickType#isRightClick() + */ + public boolean isRightClick() { + return this.getClickType().isRightClick(); + } + + /** + * Returns whether or not the ClickType for this event represents a left click. + * + *

+ * + * @return true if the ClickType uses the left mouse button + * @see ClickType#isLeftClick() + */ + public boolean isLeftClick() { + return this.getClickType().isLeftClick(); + } + + /** + * Returns whether the ClickType for this event indicates that the key was pressed down when the + * click was made. + * + *

+ * + * @return true if the ClickType uses Shift or Ctrl + * @see ClickType#isShiftClick() + */ + public boolean isShiftClick() { + return this.getClickType().isShiftClick(); + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/BookItemMenu.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/BookItemMenu.java new file mode 100644 index 0000000..c6f0122 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/BookItemMenu.java @@ -0,0 +1,580 @@ +package com.pepedevs.corelib.gui.inventory.custom.book; + +import com.pepedevs.corelib.gui.inventory.Item; +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import com.pepedevs.corelib.gui.inventory.action.ItemMenuClickAction; +import com.pepedevs.corelib.gui.inventory.size.ItemMenuSize; +import org.apache.commons.lang.Validate; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.plugin.Plugin; + +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** Implementation of {@link ItemMenu} interacting with paginated menu. */ +public class BookItemMenu extends ItemMenu { + + /** Default page button bar size. */ + public static final ItemMenuSize DEFAULT_BUTTONS_BAR_SIZE = ItemMenuSize.ONE_LINE; + + private final ItemMenuSize buttons_bar_size; + private final Item[] bar_buttons; + private BookPageItemMenu[] pages; + + /** + * Constructs the Book menu. + * + *

+ * + * @param title Title of the menu + * @param pages_size Page size of the menu + * @param buttons_bar_size Page button bar size of menu + * @param parent Parent menu if any, else null + * @param contents Contents of the menu + */ + public BookItemMenu( + String title, + ItemMenuSize pages_size, + ItemMenuSize buttons_bar_size, + ItemMenu parent, + Item... contents) { + super(title, pages_size, parent, contents); + Validate.notNull(pages_size, "The pages size cannot be null!"); + Validate.notNull(buttons_bar_size, "The buttons bar size cannot be null!"); + Validate.isTrue( + pages_size.isHigherThan(ItemMenuSize.ONE_LINE), + "The book pages size must be higher than ItemMenuSize.ONE_LINE!"); + Validate.isTrue( + pages_size.isHigherThan(buttons_bar_size), + "The pages size must be higher than the buttons bar size"); + + this.buttons_bar_size = buttons_bar_size; + this.bar_buttons = new Item[buttons_bar_size.getSize()]; + int pages_amount = 1; + if (contents != null) { + int contents_length = contents.length; + pages_amount = getPagesAmount(contents_length, pages_size, buttons_bar_size); + } + + this.pages = new BookPageItemMenu[pages_amount]; + for (int i = 0; i < this.pages.length; i++) { // default pages. + this.pages[i] = new BookPageItemMenu(this, new Item[pages_size.getSize()]); + this.pages[i].setPageNumber(i); + } + + this.addItems(contents); + } + + /** + * Constructs the Book menu. + * + *

+ * + * @param title Title of the menu + * @param pages_size Page size of the menu + * @param parent Parent menu if any, else null + * @param contents Contents of the menu + */ + public BookItemMenu(String title, ItemMenuSize pages_size, ItemMenu parent, Item... contents) { + this(title, pages_size, DEFAULT_BUTTONS_BAR_SIZE, parent, contents); + } + + /** + * Returns the per page menu size. + * + *

+ * + * @return Size of the menu + */ + public ItemMenuSize getPagesSize() { + return super.getSize(); + } + + /** + * Returns the button bar size size. + * + *

+ * + * @return Size of button bar + */ + public ItemMenuSize getButtonsBarSize() { + return this.buttons_bar_size; + } + + /** + * Returns the per page menu size. + * + *

+ * + * @return Size of the menu + */ + @Override + @Deprecated + public final ItemMenuSize getSize() { + return super.getSize(); + } + + /** + * Returns the all the pages of the book menu. + * + *

+ * + * @return Array of pages + */ + public BookPageItemMenu[] getPages() { + return Arrays.copyOf(pages, pages.length); + } + + /** + * Returns the page of the menu from index. + * + *

+ * + * @param page_index Index of page + * @return Page of menu + */ + public BookPageItemMenu getPage(int page_index) { + pagesRangeCheck(page_index, page_index); + return pages[page_index]; + } + + /** + * Returns the button-bar buttons of the menu. + * + *

+ * + * @return Array of button-bar buttons. + */ + public Item[] getBarButtons() { + return Arrays.copyOf(bar_buttons, bar_buttons.length); + } + + /** + * Returns the button-bar button at the index. + * + *

+ * + * @param button_index Index of button-bar + * @return Button-bar button + */ + public Item getBarButton(int button_index) { + buttonsRangeCheck(button_index, button_index); + return bar_buttons[button_index]; + } + + @Override + @Deprecated + public final Item[] getContents() { + return null; + } + + @Override + @Deprecated + public final Stream getContentsStream() { + return null; + } + + @Override + @Deprecated + public final Item[] getContents(Predicate predicate_filter) { + return null; + } + + @Override + @Deprecated + public final Item getItem(int index) { + return null; + } + + @Override + @Deprecated + public final Integer[] getIndexes(Predicate predicate_filter) { + return null; + } + + @Override + @Deprecated + public final int getFirstEmpty() { + return -1; + } + + @Override + @Deprecated + public final Integer[] getEmptyIndexes() { + return null; + } + + @Override + public boolean isFull() { + for (int i = 0; i < getPages().length; i++) { + if (!getPage(i).isFull()) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + for (int i = 0; i < getPages().length; i++) { + if (!getPage(i).isEmpty()) { + return false; + } + } + return true; + } + + @Override + public boolean isMenuOpen(Player player) { + return getOpenPageNumber(player) != -1; + } + + /** + * Returns the number of the page of this book that the given player is viewing, or -1 if the + * player is not viewing any page of this book. + * + *

+ * + * @param player Player viewer + * @return Number of the page of this book that the given player is viewing, or -1 if the player + * is not viewing any page of this book + */ + public int getOpenPageNumber(Player player) { + for (int i = 0; i < getPages().length; i++) { + if (getPage(i).isMenuOpen(player)) { + return getPage(i).getPageNumber(); + } + } + return -1; + } + + // @Override @Deprecated + // public final ItemMenu setSize(ItemMenuSize size) { + // return null; + // } + + @Override + @Deprecated + public final ItemMenu setContents(Item[] contents) { + return null; + } + + @Override + @Deprecated + public final ItemMenu setItem(int index, Item content) { + return null; + } + + @Override + @Deprecated + public final boolean setItemIf(int index, Item content, Predicate predicate) { + return false; + } + + @Override + public boolean addItem(Item item) { + addItems(item); + return true; + } + + /** + * Stores the given items. + * + *

New pages will be created if required. + * + *

+ * + * @param items Items to add + * @return This Object, for chaining + */ + public BookItemMenu addItems(Item... items) { + final int count = addItemsNotFull(items); + if ((items.length - count) > 0) { // if there is no added items + int pages_amount = + getPagesAmount((items.length - count), getPagesSize(), getButtonsBarSize()); + this.pages = Arrays.copyOfRange(this.pages, 0, this.pages.length + pages_amount); + for (int i = 0; i < pages.length; i++) { + if (pages[i] == null) { + pages[i] = new BookPageItemMenu(this); + pages[i].setPageNumber(i); + + // here we're registering listener of the new page. + if (getHandler() != null) { + pages[i].registerListener(getHandler().getPlugin()); + } + } + } + addItemsNotFull(Arrays.copyOfRange(items, count, items.length)); + } + return this; + } + + /** + * Stores the given items only if the menu is not full at the moment of storing the current + * element of the iteration. + * + *

+ * + * @param items Items to store + * @return Amount of items that was stored successfully + */ + public int addItemsNotFull(Item... items) { + int added_count = 0; + for (int i = 0; i < items.length; i++) { + for (int j = 0; j < getPages().length; j++) { + if (!getPage(j).isFull()) { + getPage(j).addItem(items[i]); + added_count++; + break; + } + } + } + return added_count; + } + + /** + * Stores the given bar button on the given slot. + * + *

+ * + * @param index Index of Slot + * @param button Button to store + * @return This Object, for chaining + */ + public BookItemMenu setBarButton(int index, Item button) { + this.buttonsRangeCheck(index, index); + this.bar_buttons[index] = button; + return this; + } + + /** + * Stores the given bar button item on the next empty slot of the bar. + * + *

+ * + * @param buttons Button items to store + * @return true if the button could be stored successfully + */ + public boolean addBarButton(Item... buttons) { + for (int i = 0; i < buttons.length; i++) { + for (int j = 0; j < this.getButtonsBarSize().getSize(); j++) { + if (this.bar_buttons[j] == null) { + this.bar_buttons[j] = buttons[i]; + return true; + } + } + } + return false; + } + + @Override + @Deprecated + public final boolean clearItem(int slot) { + return false; + } + + @Override + @Deprecated + public final boolean clearItemIf(int slot, Predicate predicate) { + return false; + } + + @Override + @Deprecated + public final ItemMenu fill(Item... contents) { + return null; + } + + @Override + @Deprecated + public final ItemMenu fill(int from_index, Item... contents) { + return null; + } + + /** + * Clears all the items (including button-bar buttons and content) in the book menu. + * + *

+ * + * @return ItemMenu Object + * @see #clearContents() + * @see #clearBarButtons() + */ + public ItemMenu clear() { + clearContents(); + clearBarButtons(); + return this; + } + + /** + * Clears all the content in the book menu. + * + *

+ * + * @return ItemMenu Object + */ + public ItemMenu clearContents() { + for (int i = 0; i < this.getPages().length; i++) { // clear + BookPageItemMenu page = getPage(i); + for (int j = 0; j < page.getContents().length; j++) { + page.fillToAll(null); + } + } + return this; + } + + /** + * Clears all the button-bar buttons in the book menu. + * + *

+ * + * @return ItemMenu Object + */ + public ItemMenu clearBarButtons() { + for (int i = 0; i < this.getButtonsBarSize().getSize(); i++) { // clear + this.bar_buttons[i] = null; + } + return this; + } + + @Override + @Deprecated + public final Inventory apply(Inventory inventory) { + return null; + } + + /** + * Applies button-bar button to the given inventory. + * + *

+ * + * @param inventory Inventory to apply button-bar + * @return Inventory instance + */ + public Inventory applyBarButtons(Inventory inventory) { + int from_index = this.getPagesSize().getSize() - this.getButtonsBarSize().getSize(); + for (int i = 0; i < getButtonsBarSize().getSize(); i++) { + Item button = this.bar_buttons[i]; + if (button != null) { + inventory.setItem(from_index + i, button.getDisplayIcon()); + } + } + return inventory; + } + + @Override + public boolean registerListener(Plugin plugin) { + for (int i = 0; i < this.getPages().length; i++) { + if (!this.getPage(i).registerListener(plugin)) { + return false; + } + } + return super.registerListener(plugin); + } + + @Override + public boolean unregisterListener() { + for (int i = 0; i < this.getPages().length; i++) { + if (!this.getPage(i).unregisterListener()) { + return false; + } + } + return this.unregisterListener(); + } + + @Override + public Inventory open(Player player) { + return open(0, player); + } + + /** + * Opens a new {@link Inventory} with the contents of the given page. + * + *

+ * + * @param page_number the page number. + * @param player the player viewer. + * @return the opened inventory. + */ + public Inventory open(int page_number, Player player) { + this.pagesRangeCheck(page_number, page_number); + return getPage(page_number).open(player); // apply page contents and bar buttons + } + + @Override + public boolean update(Player player) { + for (int i = 0; i < this.getPages().length; i++) { + BookPageItemMenu page = this.getPage(i); + if (page.update(player)) { + return true; + } + } + return false; + } + + @Override + @Deprecated + public ItemMenu onClick(final ItemMenuClickAction action) { + return this; + } + + protected void bookRangeCheck(int from, int to) { + rangeCheck(getPagesSize().getSize() * pages.length, from, to); + } + + protected void pagesRangeCheck(int from, int to) { + rangeCheck(this.getPages().length, from, to); + } + + protected void buttonsRangeCheck(int from, int to) { + rangeCheck(this.getButtonsBarSize().getSize(), from, to); + } + + protected void rangeCheck(int array_length, int from, int to) { + if (from > to) { + throw new IllegalArgumentException("from(" + from + ") > to(" + to + ")!"); + } + + if (from < 0) { + throw new ArrayIndexOutOfBoundsException(from); + } + + if (to >= array_length) { + throw new ArrayIndexOutOfBoundsException(to - 1); + } + } + + @Override + @Deprecated + protected final void rangeCheck(int from, int to) { + /* do nothing because is deprecated! */ + } + + protected int getPagesAmount( + int contents_length, ItemMenuSize pages_size, ItemMenuSize buttons_bar_size) { + Validate.isTrue( + pages_size.isHigherThan(ItemMenuSize.ONE_LINE), + "The book pages size must be higher than ItemMenuSize.ONE_LINE!"); + Validate.isTrue( + pages_size.isHigherThan(buttons_bar_size), + "The pages size must be higher the buttons bar size"); + int pages_amount = 1; + if (contents != null) { + int pages_fit_size = + pages_size.getSize() + - buttons_bar_size + .getSize(); // pages_fit_size = the pages size - (buttons bar + // size). + int length = contents_length; + if (length > pages_fit_size) { + while (length - pages_fit_size > 0) { + length -= pages_fit_size; + pages_amount++; + } + } + } + return pages_amount; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/BookPageItemMenu.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/BookPageItemMenu.java new file mode 100644 index 0000000..03cfdc5 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/BookPageItemMenu.java @@ -0,0 +1,193 @@ +package com.pepedevs.corelib.gui.inventory.custom.book; + +import com.pepedevs.corelib.gui.inventory.Item; +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.gui.inventory.action.ItemMenuClickAction; +import com.pepedevs.corelib.gui.inventory.size.ItemMenuSize; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +import java.util.Arrays; + +/** Implementation of {@link ItemMenu} for creating pages of {@link BookItemMenu}. */ +public class BookPageItemMenu extends ItemMenu { + + private int page_number; + + /** + * Constructs the Book Page menu. + * + *

+ * + * @param book Book menu of which this page menu is a child of + * @param contents Contents of this page menu + */ + public BookPageItemMenu(BookItemMenu book, Item... contents) { + super(book.getTitle(), book.getPagesSize(), book, contents); + } + + @Override + public final String getTitle() { + return super.getTitle(); + } + + @Override + public final BookPageItemMenu setTitle(String title) { + this.getBookMenu().setTitle(title); + return this; + } + + public final ItemMenuSize getPageSize() { + return this.getBookMenu().getPagesSize(); + } + + @Override + @Deprecated + public final ItemMenuSize getSize() { + return getPageSize(); + } + + @Override + public final Item[] getContents() { // don't return the buttons + return Arrays.copyOfRange( + super.getContents(), 0, ItemMenuSize.beforeTo(getPageSize()).getSize()); + } + + /** + * Returns the {@link BookItemMenu} owner of this. + * + *

+ * + * @return Parent of this book menu page + */ + public final BookItemMenu getBookMenu() { + return (BookItemMenu) super.getParent(); + } + + /** + * Returns the page next to this page on the book, or null if this is the last page. + * + *

+ * + * @return {@link BookPageItemMenu} next to this page on the book, or null if this is the last + * page. + */ + public final BookPageItemMenu getNextBookPage() { + return (page_number + 1) < getBookMenu().getPages().length + ? getBookMenu().getPage(page_number + 1) + : null; + } + + /** + * Returns the page before this page on the book, or null if this is the first page. + * + *

+ * + * @return {@link BookPageItemMenu} before this page on the book, or null if this is the first + * page. + */ + public final BookPageItemMenu getBeforeBookPage() { + return (page_number - 1) >= 0 ? getBookMenu().getPage(page_number - 1) : null; + } + + /** + * Returns the number of this page on the book. + * + *

+ * + * @return Number of this {@link BookPageItemMenu} on the book. + */ + public final int getPageNumber() { + return page_number; + } + + /** + * Sets the page number of the this page menu. + * + *

+ * + * @param number Page number + * @return This Object, for chaining + */ + final BookPageItemMenu setPageNumber(int number) { + this.page_number = number; + return this; + } + + /** Returns the {@link BookItemMenu} owner of this. */ + @Override + @Deprecated + public final BookItemMenu getParent() { + return getBookMenu(); + } + + // @Override @Deprecated + // public final BookPageItemMenu setSize(ItemMenuSize size) { + // return this; + // } + + @Override + public boolean hasParent() { + return true; + } + + @Override + public Inventory open(Player player) { + return this.getBookMenu().applyBarButtons(super.open(player)); + } + + public ItemMenu onClick(final ItemMenuClickAction action) { + if (this.getHandler() == null) { + throw new UnsupportedOperationException("This menu has never been registered!"); + } + + if (!action.isRightClick() && !action.isLeftClick()) { + return this; + } + + if (action.getSlot() < 0) { + return this; + } + + ItemClickAction sub_action = + new ItemClickAction( + this, + action.getInventoryView(), + action.getClickType(), + action.getInventoryAction(), + action.getSlotType(), + action.getSlot(), + action.getRawSlot(), + action.getCurrentItem(), + action.getHotbarKey(), + false, + false, + false); + + /* calling click method of the clicked item */ + if (action.getSlot() >= getContents().length) { + int bar_button_slot = action.getSlot() - getContents().length; + if (bar_button_slot < getBookMenu().getBarButtons().length) { // clicking a bar button + Item bar_button = this.getBookMenu().getBarButton(bar_button_slot); + if (bar_button == null) { + return this; + } + bar_button.onClick(sub_action); + } + } else { + Item item = getItem(action.getSlot()); + + if (item != null) { + item.onClick(sub_action); + } + } + + if (sub_action.isWillUpdate()) { + update(action.getPlayer()); + } else if (sub_action.isWillClose() || sub_action.isWillGoBack()) { + getHandler().delayedClose(action.getPlayer(), 1); + } + return this; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/item/AlternateBookPageActionItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/item/AlternateBookPageActionItem.java new file mode 100644 index 0000000..855046b --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/book/item/AlternateBookPageActionItem.java @@ -0,0 +1,96 @@ +package com.pepedevs.corelib.gui.inventory.custom.book.item; + +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.gui.inventory.custom.book.BookItemMenu; +import com.pepedevs.corelib.gui.inventory.custom.book.BookPageItemMenu; +import com.pepedevs.corelib.gui.inventory.item.action.ActionItem; +import com.pepedevs.corelib.gui.inventory.item.action.ItemAction; +import com.pepedevs.corelib.gui.inventory.item.action.ItemActionPriority; +import org.bukkit.inventory.ItemStack; + +/** Implementation of {@link ActionItem} handling page change button item in book menu. */ +public class AlternateBookPageActionItem extends ActionItem { + + protected BookItemMenu book; + protected boolean go_next; + + /** + * Constructs the book page change action item. + * + *

+ * + * @param name Name of the item + * @param icon ItemStack icon of the item + * @param lore Lore of the item + */ + public AlternateBookPageActionItem(String name, ItemStack icon, String... lore) { + super(name, icon, lore); + this.go_next = true; + super.addAction( + new ItemAction() { + + @Override + public ItemActionPriority getPriority() { + return ItemActionPriority.LOW; + } + + @Override + public void onClick(ItemClickAction action) { + if (!(action.getMenu() instanceof BookPageItemMenu)) { + return; + } + + if (!((BookPageItemMenu) action.getMenu()).getBookMenu().equals(book)) { + return; + } + + BookPageItemMenu current = + book.getPage(book.getOpenPageNumber(action.getPlayer())); + BookPageItemMenu to = + go_next ? current.getNextBookPage() : current.getBeforeBookPage(); + if (to != null) { + book.getHandler().delayedClose(action.getPlayer(), 1); + to.getHandler().delayedOpen(action.getPlayer(), 2); + } + } + }); + } + + /** + * Sets the book menu owner of the pages that the player can alternate. + * + *

+ * + * @param book {@link BookItemMenu} owner of the pages that the player can alternate + * @return This Object, for chaining + */ + public AlternateBookPageActionItem setBookMenu(BookItemMenu book) { + this.book = book; + return this; + } + + /** + * If true: The next page will be opened. If false: The page before the current will be opened. + * + *

+ * + * @param flag true == next page. false == page before + * @return This Object, for chaining + */ + public AlternateBookPageActionItem setGoNext(boolean flag) { + this.go_next = flag; + return this; + } + + @Deprecated + @Override + public final ActionItem addAction(ItemAction action) { + return this; + } + + @Deprecated + @Override + public final ActionItem removeAction(ItemAction action) { + return this; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/updating/UpdatingItemMenu.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/updating/UpdatingItemMenu.java new file mode 100644 index 0000000..8f35026 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/updating/UpdatingItemMenu.java @@ -0,0 +1,42 @@ +package com.pepedevs.corelib.gui.inventory.custom.updating; + +import com.pepedevs.corelib.gui.inventory.Item; +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import com.pepedevs.corelib.gui.inventory.custom.updating.handler.UpdatingItemMenuHandler; +import com.pepedevs.corelib.gui.inventory.size.ItemMenuSize; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +/** Implementation of {@link ItemMenu} handling updatable content item menu. */ +public class UpdatingItemMenu extends ItemMenu { + + /** + * Constructs the Updatable menu. + * + *

+ * + * @param title Title of the menu + * @param size Size of the menu + * @param parent Parent of the menu if any. or null + * @param contents Content of the menu + */ + public UpdatingItemMenu(String title, ItemMenuSize size, ItemMenu parent, Item... contents) { + super(title, size, parent, contents); + } + + @Override + public UpdatingItemMenuHandler getHandler() { + return (UpdatingItemMenuHandler) this.handler; + } + + @Override + public boolean registerListener(Plugin plugin) { + if (this.handler == null) { + Bukkit.getPluginManager() + .registerEvents( + (this.handler = new UpdatingItemMenuHandler(this, plugin)), plugin); + return true; + } + return false; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/updating/handler/UpdatingItemMenuHandler.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/updating/handler/UpdatingItemMenuHandler.java new file mode 100644 index 0000000..5888182 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/custom/updating/handler/UpdatingItemMenuHandler.java @@ -0,0 +1,70 @@ +package com.pepedevs.corelib.gui.inventory.custom.updating.handler; + +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import com.pepedevs.corelib.gui.inventory.handler.ItemMenuHandler; +import com.pepedevs.corelib.utils.scheduler.SchedulerUtils; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +/** Implementation of {@link ItemMenuHandler} for handling updatable menus. */ +public class UpdatingItemMenuHandler extends ItemMenuHandler { + + protected BukkitTask updater_task; + + /** + * Constructs the updating menu handler. + * + *

+ * + * @param menu Updating item menu + * @param plugin Plugin instance for the menu + */ + public UpdatingItemMenuHandler(ItemMenu menu, Plugin plugin) { + super(menu, plugin); + } + + /** + * Checks if the menu is updating. + * + *

+ * + * @return true if updating, else false + */ + public boolean isUpdating() { + return updater_task != null + && Bukkit.getScheduler().isCurrentlyRunning(updater_task.getTaskId()); + } + + @Override + public void unregisterListener() { + super.unregisterListener(); + stopUpdating(); + } + + /** + * Starts updating the menu. + * + *

+ * + * @param start_delay Start delay before updating menu + * @param ticks Ticks invterval between updating menu + */ + public void startUpdating(int start_delay, int ticks) { + stopUpdating(); + this.updater_task = + SchedulerUtils.runTaskTimer( + this.menu::updateOnlinePlayers, + start_delay, + ticks, + plugin); + } + + /** Stops updating menu */ + public void stopUpdating() { + if (updater_task != null) { + updater_task.cancel(); + updater_task = null; + } + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/handler/ItemMenuHandler.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/handler/ItemMenuHandler.java new file mode 100644 index 0000000..4d94f60 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/handler/ItemMenuHandler.java @@ -0,0 +1,132 @@ +package com.pepedevs.corelib.gui.inventory.handler; + +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import com.pepedevs.corelib.gui.inventory.action.ItemMenuClickAction; +import com.pepedevs.corelib.gui.inventory.holder.ItemMenuHolder; +import com.pepedevs.corelib.utils.scheduler.SchedulerUtils; +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.InventoryType; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.plugin.Plugin; + +/** Handler for handling Item Menu. */ +public class ItemMenuHandler implements Listener { + + protected final ItemMenu menu; + protected final Plugin plugin; + + /** + * Constructs the ItemMenu Handler. + * + *

+ * + * @param menu ItemMenu + * @param plugin Plugin to register handler + */ + public ItemMenuHandler(ItemMenu menu, Plugin plugin) { + this.menu = menu; + this.plugin = plugin; + } + + /** + * Returns the plugin instance. + * + *

+ * + * @return Plugin instance + */ + public Plugin getPlugin() { + return plugin; + } + + /** + * Add delay to menu open. + * + *

+ * + * @param player Player to open menu + * @param delay Delay in milliseconds + */ + public void delayedOpen(Player player, int delay) { + delayed( + () -> { + menu.open(player); + }, + delay); + } + + /** + * Add delay to menu update. + * + *

+ * + * @param player Player to open menu + * @param delay Delay in milliseconds + */ + public void delayedUpdate(Player player, int delay) { + delayed( + () -> { + menu.update(player); + }, + delay); + } + + /** + * Add delay to menu close. + * + *

+ * + * @param player Player to open menu + * @param delay Delay in milliseconds + */ + public void delayedClose(Player player, int delay) { + delayed( + () -> { + menu.close(player); + }, + delay); + } + + /** Unregister the handler. */ + public void unregisterListener() { + HandlerList.unregisterAll(this); + } + + protected void delayed(Runnable runnable, int delay) { + SchedulerUtils.scheduleSyncDelayedTask(runnable, delay, plugin); + } + + @SuppressWarnings("deprecation") + @EventHandler(priority = EventPriority.LOWEST) + public void onInventoryClick(InventoryClickEvent event) { + if (event.getInventory() == null + || event.getInventory().getType() != InventoryType.CHEST + || !(event.getWhoClicked() instanceof Player) + || !(event.getInventory().getHolder() instanceof ItemMenuHolder)) { + return; + } + + ItemMenuHolder holder = (ItemMenuHolder) event.getInventory().getHolder(); + if (!menu.equals(holder.getItemMenu())) { + return; + } + + final ItemMenuClickAction action = new ItemMenuClickAction(menu, event); + ((ItemMenuHolder) event.getInventory().getHolder()).getItemMenu().onClick(action); + event.setCurrentItem(action.getCurrentItem()); + event.setCursor(action.getCursor()); + event.setCancelled(true); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPluginDisable(PluginDisableEvent event) { + if (event.getPlugin().equals(plugin)) { + this.menu.closeOnlinePlayers(); + } + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/holder/ItemMenuHolder.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/holder/ItemMenuHolder.java new file mode 100644 index 0000000..a7ffb5b --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/holder/ItemMenuHolder.java @@ -0,0 +1,48 @@ +package com.pepedevs.corelib.gui.inventory.holder; + +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +/** Implementation of {@link InventoryHolder} interacting with ItemMenu. */ +public class ItemMenuHolder implements InventoryHolder { + + private final ItemMenu menu; + private final Inventory inventory; + + /** + * Constructs the ItemMenuHolder. + * + *

+ * + * @param menu ItemMenu + * @param inventory Inventory + */ + public ItemMenuHolder(ItemMenu menu, Inventory inventory) { + this.menu = menu; + this.inventory = inventory; + } + + /** + * Returns the ItemMenu. + * + *

+ * + * @return ItemMenu + */ + public ItemMenu getItemMenu() { + return menu; + } + + /** + * Returns the Inventory. + * + *

+ * + * @return Inventory + */ + @Override + public Inventory getInventory() { + return inventory; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ActionItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ActionItem.java new file mode 100644 index 0000000..0dc9d67 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ActionItem.java @@ -0,0 +1,97 @@ +package com.pepedevs.corelib.gui.inventory.item.action; + +import com.pepedevs.corelib.gui.inventory.Item; +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import org.apache.commons.lang.Validate; +import org.bukkit.inventory.ItemStack; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Implementation of {@link Item}. */ +public class ActionItem extends Item { + + private final Set actions; + + /** + * Constructs the menu Action Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon of the Item + * @param lore Lore of the Item + */ + public ActionItem(String name, ItemStack icon, String... lore) { + super(name, icon, lore); + this.actions = new HashSet<>(); + } + + /** + * Constructs the menu Action Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon of the Item + * @param lore Lore of the Item + */ + public ActionItem(String name, ItemStack icon, List lore) { + super(name, icon, lore); + + this.actions = new HashSet<>(); + } + + /** + * Constructs the menu Action Item. + * + *

+ * + * @param icon ItemStack icon of the Item + */ + public ActionItem(ItemStack icon) { + super(icon); + this.actions = new HashSet<>(); + } + + /** + * Add action to the Action Item. + * + *

+ * + * @param action Item Action + * @return This Object, for chaining + */ + public ActionItem addAction(ItemAction action) { + Validate.notNull(action, "The action cannot be null!"); + Validate.notNull(action.getPriority(), "The action priority cannot be null!"); + actions.add(action); + return this; + } + + /** + * Remove action from the Action Item. + * + *

+ * + * @param action Item Action + * @return This Object, for chaining + */ + public ActionItem removeAction(ItemAction action) { + actions.remove(action); + return this; + } + + @Override + public void onClick(ItemClickAction action) { + for (int i = ItemActionPriority.values().length - 1; + i >= 0; + i--) { // call in order, from high to low + ItemActionPriority priority = ItemActionPriority.values()[i]; + actions.stream() + .filter(act -> act != null && priority.equals(act.getPriority())) + .forEach(act -> act.onClick(action)); + } + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ItemAction.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ItemAction.java new file mode 100644 index 0000000..0013833 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ItemAction.java @@ -0,0 +1,25 @@ +package com.pepedevs.corelib.gui.inventory.item.action; + +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; + +/** Class to classify menu item action. */ +public interface ItemAction { + + /** + * Returns the priority of the action. + * + *

+ * + * @return {@link ItemActionPriority} + */ + public ItemActionPriority getPriority(); + + /** + * Click action on the item. + * + *

+ * + * @param action {@link ItemClickAction} on the Item + */ + public void onClick(ItemClickAction action); +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ItemActionPriority.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ItemActionPriority.java new file mode 100644 index 0000000..385423b --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/ItemActionPriority.java @@ -0,0 +1,13 @@ +package com.pepedevs.corelib.gui.inventory.item.action; + +public enum ItemActionPriority { + + /** Should run at last. */ + LOW, + + /** Should run after the high priority and before low priority. */ + NORMAL, + + /** Should run first. */ + HIGH; +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/back/BackActionItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/back/BackActionItem.java new file mode 100644 index 0000000..247b1b6 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/back/BackActionItem.java @@ -0,0 +1,37 @@ +package com.pepedevs.corelib.gui.inventory.item.action.back; + +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.gui.inventory.item.action.ActionItem; +import com.pepedevs.corelib.gui.inventory.item.action.ItemAction; +import com.pepedevs.corelib.gui.inventory.item.action.ItemActionPriority; +import org.bukkit.inventory.ItemStack; + +/** Implementation of {@link ActionItem} handling back button. */ +public class BackActionItem extends ActionItem { + + /** + * Constructs the Back Action Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon of the Item + * @param lore Lore of the Item + */ + public BackActionItem(String name, ItemStack icon, String[] lore) { + super(name, icon, lore); + addAction( + new ItemAction() { + + @Override + public ItemActionPriority getPriority() { + return ItemActionPriority.LOW; + } + + @Override + public void onClick(ItemClickAction action) { + action.setGoBack(true); + } + }); + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/close/CloseMenuActionItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/close/CloseMenuActionItem.java new file mode 100644 index 0000000..5b5995c --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/close/CloseMenuActionItem.java @@ -0,0 +1,72 @@ +package com.pepedevs.corelib.gui.inventory.item.action.close; + +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.gui.inventory.item.action.ActionItem; +import com.pepedevs.corelib.gui.inventory.item.action.ItemAction; +import com.pepedevs.corelib.gui.inventory.item.action.ItemActionPriority; +import com.pepedevs.corelib.utils.itemstack.safe.SafeItemStack; +import com.pepedevs.corelib.utils.itemstack.stainedglass.StainedGlassColor; +import com.pepedevs.corelib.utils.itemstack.stainedglass.StainedGlassItemStack; +import com.pepedevs.corelib.utils.reflection.general.EnumReflection; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +/** Implementation of {@link ActionItem} handling the close menu option. */ +public class CloseMenuActionItem extends ActionItem { + + @Deprecated // Compatibility with server versions <= 1.8 + public static final ItemStack DEFAULT_ICON = + EnumReflection.getEnumConstant(Material.class, "BARRIER") != null + ? new SafeItemStack(Material.BARRIER) + : new StainedGlassItemStack(StainedGlassColor.RED, true); + + /** + * Constructs the Close Menu Action Item. + * + *

+ * + * @param lore Lore of the Item + */ + public CloseMenuActionItem(String... lore) { + this(ChatColor.RED + "Close", lore); + } + + /** + * Constructs the Close Menu Action Item. + * + *

+ * + * @param name Name of the Item + * @param lore Lore of the Item + */ + public CloseMenuActionItem(String name, String... lore) { + this(name, DEFAULT_ICON, lore); + } + + /** + * Constructs the Close Menu Action Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon of the Item + * @param lore Lore of the Item + */ + public CloseMenuActionItem(String name, ItemStack icon, String... lore) { + super(name, icon, lore); + addAction( + new ItemAction() { + + @Override + public ItemActionPriority getPriority() { + return ItemActionPriority.LOW; + } + + @Override + public void onClick(ItemClickAction action) { + action.setClose(true); + } + }); + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/open/OpenMenuActionItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/open/OpenMenuActionItem.java new file mode 100644 index 0000000..1ad5f4d --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/action/open/OpenMenuActionItem.java @@ -0,0 +1,71 @@ +package com.pepedevs.corelib.gui.inventory.item.action.open; + +import com.pepedevs.corelib.gui.inventory.ItemMenu; +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import com.pepedevs.corelib.gui.inventory.item.action.ActionItem; +import com.pepedevs.corelib.gui.inventory.item.action.ItemAction; +import com.pepedevs.corelib.gui.inventory.item.action.ItemActionPriority; +import org.bukkit.inventory.ItemStack; + +/** Implementation of {@link ActionItem} handling with open menu option. */ +public class OpenMenuActionItem extends ActionItem { + + protected ItemMenu menu; + + /** + * Constructs the Open Menu Action Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack icon of the Item + * @param lore Lore of the Item + */ + public OpenMenuActionItem(String name, ItemStack icon, String... lore) { + super(name, icon, lore); + addDefaultAction(); + } + + /** + * Constructs the Open Menu Action Item. + * + *

+ * + * @param icon ItemStack icon of the Item + */ + public OpenMenuActionItem(ItemStack icon) { + super(icon); + addDefaultAction(); + } + + /** Adds the default action of opening other menu. */ + protected void addDefaultAction() { + addAction( + new ItemAction() { + + @Override + public ItemActionPriority getPriority() { + return ItemActionPriority.LOW; + } + + @Override + public void onClick(ItemClickAction action) { + action.getMenu().getHandler().delayedClose(action.getPlayer(), 1); + menu.getHandler().delayedOpen(action.getPlayer(), 3); + } + }); + } + + /** + * Sets the {@link ItemMenu} that will be opened when clicking this. + * + *

+ * + * @param menu Menu to open when this clicked + * @return This Object, for chaining + */ + public OpenMenuActionItem setMenu(ItemMenu menu) { + this.menu = menu; + return this; + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/voidaction/VoidActionItem.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/voidaction/VoidActionItem.java new file mode 100644 index 0000000..ed51b02 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/item/voidaction/VoidActionItem.java @@ -0,0 +1,34 @@ +package com.pepedevs.corelib.gui.inventory.item.voidaction; + +import com.pepedevs.corelib.gui.inventory.Item; +import com.pepedevs.corelib.gui.inventory.action.ItemClickAction; +import org.bukkit.inventory.ItemStack; + +/** Class for Filler/Void Items in ItemMenu. */ +public class VoidActionItem extends Item { + + /** + * Constructs the Void Action Item. + * + *

+ * + * @param name Name of the Item + * @param icon ItemStack Icon of the Item + * @param lore Lore of the Item + */ + public VoidActionItem(String name, ItemStack icon, String... lore) { + super(name, icon, lore); + } + + /** + * Click action for Filler/Void Item. + * + *

+ * + * @param action {@link ItemClickAction} for the Item + */ + @Override + public final void onClick(ItemClickAction action) { + /* do nothing */ + } +} diff --git a/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/size/ItemMenuSize.java b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/size/ItemMenuSize.java new file mode 100644 index 0000000..ed921b2 --- /dev/null +++ b/inventory-gui/src/main/java/com/pepedevs/corelib/gui/inventory/size/ItemMenuSize.java @@ -0,0 +1,152 @@ +package com.pepedevs.corelib.gui.inventory.size; + +import java.util.Arrays; + +/** Enumeration for ItemMenu size. */ +public enum ItemMenuSize { + ONE_LINE(9), + TWO_LINE(18), + THREE_LINE(27), + FOUR_LINE(36), + FIVE_LINE(45), + SIX_LINE(54); + + private final int size; + + ItemMenuSize(int size) { + this.size = size; + } + + /** + * Returns the {@link ItemMenuSize} which fits the given size. + * + *

+ * + * @param size Size to fit in ItemMenu + * @return ItemMenuSize instance with the given size + */ + public static ItemMenuSize fitOf(int size) { + int difference = -1; + for (ItemMenuSize constant : ItemMenuSize.values()) { + difference = + ((difference == -1 || (Math.max(size - constant.getSize(), 0)) < difference) + ? (size - constant.getSize()) + : difference); + } + return Arrays.stream(ItemMenuSize.values()) + .filter(constant -> Math.max(size - constant.getSize(), 0) == 0) + .findAny() + .orElse(SIX_LINE); + } + + /** + * Returns the size enumeration next to this size. + * + *

+ * + * @param from Size to get next to + * @return The next size enumeration + */ + public static ItemMenuSize nextTo(ItemMenuSize from) { + return fitOf(from.getSize() + 1); + } + + /** + * Returns the size enumeration before this size. + * + *

+ * + * @param from Size to get before size + * @return The before size enumeration + */ + public static ItemMenuSize beforeTo(ItemMenuSize from) { + switch (from) { + case ONE_LINE: + + case TWO_LINE: + return ONE_LINE; + + case THREE_LINE: + return TWO_LINE; + + case FOUR_LINE: + return THREE_LINE; + + case FIVE_LINE: + return FOUR_LINE; + + case SIX_LINE: + return FIVE_LINE; + + default: + return null; + } + } + + /** + * Returns the size of menu. + * + *

+ * + * @return Size of menu + */ + public int getSize() { + return size; + } + + /** + * Checks if the given size is higher than this size. + * + *

+ * + * @param other Other size + * @return {@code true} if the given size is higher, else false + */ + public boolean isHigherThan(ItemMenuSize other) { + return other != null && getSize() > other.getSize(); + } + + /** + * Checks if the given size is higher than or equal to this size. + * + *

+ * + * @param other Other size + * @return {@code true} if the given size is higher or equal, else false + */ + public boolean isHigherEqualsThan(ItemMenuSize other) { + return other != null && getSize() >= other.getSize(); + } + + /** + * Checks if the given size is lower than this size. + * + *

+ * + * @param other Other size + * @return {@code true} if the given size is lower, else false + */ + public boolean isLowerThan(ItemMenuSize other) { + return other != null && getSize() < other.getSize(); + } + + /** + * Checks if the given size is lower than or equal to this size. + * + *

+ * + * @param other Other size + * @return {@code true} if the given size is lower or equal, else false + */ + public boolean isLowerEqualsThan(ItemMenuSize other) { + return other != null && getSize() <= other.getSize(); + } + + public int getFirstSlotTest() { + return beforeTo(this).getSize() + 1; + } + + public int getLastSlotTest() { + return getSize() - 1; + } +} diff --git a/minecraft-version/pom.xml b/minecraft-version/pom.xml new file mode 100644 index 0000000..0cbad2b --- /dev/null +++ b/minecraft-version/pom.xml @@ -0,0 +1,19 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + minecraft-version + + + 8 + 8 + + + \ No newline at end of file diff --git a/minecraft-version/src/main/java/com/pepedevs/corelib/utils/version/Version.java b/minecraft-version/src/main/java/com/pepedevs/corelib/utils/version/Version.java new file mode 100644 index 0000000..1c16da4 --- /dev/null +++ b/minecraft-version/src/main/java/com/pepedevs/corelib/utils/version/Version.java @@ -0,0 +1,166 @@ +package com.pepedevs.corelib.utils.version; + +import org.bukkit.Bukkit; + +/** An enumeration for most server versions, that implements some methods for comparing versions. */ +public enum Version { + + /* legacy versions */ + v1_8_R1(181), + v1_8_R2(182), + v1_8_R3(183), + v1_9_R1(191), + v1_9_R2(192), + v1_10_R1(1101), + v1_11_R1(1111), + v1_12_R1(1121), + + /* latest versions */ + v1_13_R1(1131), + v1_13_R2(1132), + v1_14_R1(1141), + v1_15_R1(1151), + v1_16_R1(1161), + v1_16_R2(1162), + v1_16_R3(1163), + v1_17_R1(1171), + v1_18_R1(1181), + ; + + public static final String CRAFT_CLASSES_PACKAGE = "org.bukkit.craftbukkit.%s"; + public static final String NMS_CLASSES_PACKAGE = "net.minecraft.server.%s"; + public static final Version SERVER_VERSION = Version.getServerVersion(); + + private final int id; + + Version(int id) { + this.id = id; + } + + /** + * Gets the version of the current running server. + * + *

Note that server versions older than {@link Version#v1_8_R1} are NOT supported. + * + *

+ * + * @return Version of this server + */ + public static Version getServerVersion() { + String packaje = Bukkit.getServer().getClass().getPackage().getName(); + String version = packaje.substring(packaje.lastIndexOf(".") + 1); + return valueOf(version); + } + + public String getNmsPackage() { + if (SERVER_VERSION.isNewerEquals(Version.v1_17_R1)) + return "net.minecraft"; + else + return String.format(NMS_CLASSES_PACKAGE, SERVER_VERSION.name()); + } + + public String getObcPackage() { + return String.format(CRAFT_CLASSES_PACKAGE, SERVER_VERSION.name()); + } + + /** + * Gets the version's id. + * + *

+ * + * @return Version's id + */ + public int getID() { + return this.id; + } + + /** + * Checks whether this version is older than the provided version. + * + *

+ * + * @param other Other version + * @return true if older + */ + public boolean isOlder(Version other) { + return (getID() < other.getID()); + } + + /** + * Checks whether this version is older than or equals to the provided version. + * + *

+ * + * @param other Other version + * @return true if older or equals + */ + public boolean isOlderEquals(Version other) { + return (getID() <= other.getID()); + } + + /** + * Checks whether this version is newer than the provided version. + * + *

+ * + * @param other Other version + * @return true if newer + */ + public boolean isNewer(Version other) { + return (getID() > other.getID()); + } + + /** + * Checks whether this version is newer than or equals to the provided version. + * + *

+ * + * @param other Other version + * @return true if newer or equals + */ + public boolean isNewerEquals(Version other) { + return (getID() >= other.getID()); + } + + /** + * Checks whether this has the same version as the provided version. + * + *

+ * + *


+     * Version.1_8_R1.equalsVersion (1_8_R3) = true
+     * Version.1_9_R1.equalsVersion (1_8_R1) = false
+     * 
+ * + *

+ * + * @param other Other version + * @return true if has the same version + */ + public boolean equalsVersion(Version other) { + String s0 = name().substring(0, name().indexOf("_R")); + String s1 = other.name().substring(0, other.name().indexOf("_R")); + return s0.equals(s1); + } + + /** + * Checks whether this has the same revision as the provided version. + * + *

+ * + *


+     * Version.1_8_R3.equalsRevision (1_9_R3) = true
+     * Version.1_8_R1.equalsRevision (1_8_R3) = false
+     * 
+ * + *

+ * + * @param other Other version + * @return true if has the same revision + */ + public boolean equalsRevision(Version other) { + String s0 = name().substring(name().indexOf("R") + 1); + String s1 = other.name().substring(other.name().indexOf("R") + 1); + return s0.equals(s1); + } +} diff --git a/packet-utils/pom.xml b/packet-utils/pom.xml new file mode 100644 index 0000000..b4f7d5d --- /dev/null +++ b/packet-utils/pom.xml @@ -0,0 +1,33 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + packet-utils + + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + io.netty + netty-transport + 5.0.0.Alpha2 + provided + + + + \ No newline at end of file diff --git a/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketAdapter.java b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketAdapter.java new file mode 100644 index 0000000..aa28808 --- /dev/null +++ b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketAdapter.java @@ -0,0 +1,19 @@ +package com.pepedevs.corelib.packets; + +/** + * Convenience implementation of {@link PacketListener}. Derive from this and only override the + * needed. + */ +public class PacketAdapter implements PacketListener { + + @Override + public void onReceiving(final PacketEvent event) { + /* to override */ + } + + @Override + public void onSending(final PacketEvent event) { + /* to override */ + } + +} diff --git a/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketChannelHandler.java b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketChannelHandler.java new file mode 100644 index 0000000..c5b526a --- /dev/null +++ b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketChannelHandler.java @@ -0,0 +1,218 @@ +package com.pepedevs.corelib.packets; + +import com.pepedevs.corelib.utils.PluginHandler; +import com.pepedevs.corelib.utils.reflection.bukkit.PlayerReflection; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.Plugin; + +import java.util.*; + +/** Simple player's channels handler, for handling the receiving-sending packets. */ +public final class PacketChannelHandler extends PluginHandler { + + /** All the packet listeners that will be run when a packet is being receive-send. */ + private static final Map> PACKET_LISTENERS = + new HashMap<>(); + + public PacketChannelHandler(Plugin plugin) { + super(plugin); + Bukkit.getPluginManager() + .registerEvents(new PlayerInjector(), this.getPlugin()); + } + + /** + * Registers the provided {@link PacketListener} that will be run when a packet is being sending + * or receiving ( packets which its name is the same as the provided ). + * + *

+ * + * @param packet_name Name of the packets this listener will manage. + * @param priority Priority of the listener. + * @param listener Listener to run. + */ + public void addPacketListener( + final String packet_name, final PacketListener.Priority priority, final PacketListener listener) { + Set by_priority = PACKET_LISTENERS.get(priority); + if (by_priority == null) { + PACKET_LISTENERS.put(priority, (by_priority = new HashSet<>())); + } + + by_priority.add(new RegisteredPacketListener(packet_name, listener)); + } + + /** + * Unregisters the provided {@link PacketListener}. + * + *

+ * + * @param listener Listener to unregister. + */ + public void removePacketListener(final PacketListener listener) { + Set containing = null; + RegisteredPacketListener removing = null; + for (Set by_priority : PACKET_LISTENERS.values()) { + for (RegisteredPacketListener registered : by_priority) { + if (registered.listener.equals(listener)) { + containing = by_priority; + removing = registered; + break; + } + } + + if (removing != null) { + break; + } + } + + containing.remove(removing); + } + + @Override + protected boolean isAllowMultipleInstances() { + return false; + } + + @Override + protected boolean isSingleInstanceForAllPlugin() { + return true; + } + + /** Class representing the registered version of a {@link PacketListener}. */ + private static class RegisteredPacketListener { + + private final String packet_name; + private final PacketListener listener; + + private RegisteredPacketListener(final String packet_name, final PacketListener listener) { + this.packet_name = packet_name; + this.listener = listener; + } + + } + + /** + * Listener that injects any player that joins the server. + * + *

Also this class injects the players that are already connected to the server when is + * constructed. + */ + private static class PlayerInjector implements Listener { + + private PlayerInjector() { + Bukkit.getOnlinePlayers().forEach(PlayerInjector::inject); + } + + private static void inject(final Player player) { + /* unject before injecting */ + PlayerInjector.unject(player); + + /* injecting */ + final Channel channel = PlayerReflection.getChannel(player); + final PlayerChannelHandler handler = new PlayerChannelHandler(player); + + channel.pipeline().addBefore("packet_handler", "PacketInjector", handler); + } + + private static void unject(final Player player) { + final Channel channel = PlayerReflection.getChannel(player); + if (channel.pipeline().get("PacketInjector") != null) { + channel.pipeline().remove("PacketInjector"); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void handleJoin(final PlayerJoinEvent event) { + PlayerInjector.inject(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void handleQuit(final PlayerQuitEvent event) { + PlayerInjector.unject(event.getPlayer()); + } + + } + + /** The duplex channel handler for handling the packets when receiving or sending. */ + private static class PlayerChannelHandler extends ChannelDuplexHandler { + + private final Player player; + + public PlayerChannelHandler(final Player player) { + this.player = player; + } + + @Override + public void channelRead(final ChannelHandlerContext context, final Object packet) + throws Exception { + final PacketEvent event = new PacketEvent(player, packet); + + synchronized (PACKET_LISTENERS) { + final List listeners = getListenersFor(packet); + for (PacketListener listener : listeners) { + // we're taking care of exceptions that can cause a kick. + try { + listener.onReceiving(event); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + } + + if (!event.isCancelled()) { + super.channelRead(context, packet); + } + } + + @Override + public void write( + final ChannelHandlerContext context, + final Object packet, + final ChannelPromise promise) + throws Exception { + final PacketEvent event = new PacketEvent(player, packet); + + synchronized (PACKET_LISTENERS) { + final List listeners = getListenersFor(packet); + for (PacketListener listener : listeners) { + // we're taking care of exceptions that can cause a kick. + try { + listener.onSending(event); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + } + + if (!event.isCancelled()) { + super.write(context, packet, promise); + } + } + + private List getListenersFor(final Object packet) { + final List listeners = new ArrayList<>(); + for (PacketListener.Priority priority : PacketListener.Priority.values()) { + final Set by_priority = PACKET_LISTENERS.get(priority); + if (by_priority == null) { + continue; + } + + for (RegisteredPacketListener listener : by_priority) { + if (listener.packet_name.equals(packet.getClass().getSimpleName())) { + listeners.add(listener.listener); + } + } + } + return listeners; + } + } +} diff --git a/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketEvent.java b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketEvent.java new file mode 100644 index 0000000..e80ee1a --- /dev/null +++ b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketEvent.java @@ -0,0 +1,80 @@ +package com.pepedevs.corelib.packets; + +import org.bukkit.entity.Player; + +/** Simple event called whenever a packet will be sent-received. */ +public class PacketEvent { + + protected final Player player; + protected final Object packet; + + protected boolean cancelled; + + /** + * Constructs the packet event. + * + *

+ * + * @param player the player sending-receiving the packet. + * @param packet the packet. + */ + public PacketEvent(final Player player, final Object packet) { + this.player = player; + this.packet = packet; + } + + /** + * Gets the player sending-receiving the packet. + * + *

+ * + * @return the player. + */ + public Player getPlayer() { + return player; + } + + /** + * The packet, that is an instance of a nms packet like "PacketPlayInSteerVehicle". + * + *

+ * + * @return the packet. + */ + public Object getPacket() { + return packet; + } + + /** + * Returns the name of the packet. + * + *

+ * + * @return Name of the packet + */ + public String getPacketName() { + return packet.getClass().getSimpleName(); + } + + /** + * Gets whether this event has been cancelled. + * + *

+ * + * @return true if cancelled. + */ + public boolean isCancelled() { + return cancelled; + } + + /** + * Canceling this event will avoid the packet to be sent-received. + * + *

+ * + * @param flag true to cancel. + */ + public void setCancelled(boolean flag) { + this.cancelled = flag; + } +} diff --git a/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketListener.java b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketListener.java new file mode 100644 index 0000000..c47385e --- /dev/null +++ b/packet-utils/src/main/java/com/pepedevs/corelib/packets/PacketListener.java @@ -0,0 +1,59 @@ +package com.pepedevs.corelib.packets; + +/** Simple interface called when receiving or sending packets. */ +public interface PacketListener { + + /** + * Called when receiving a packet. + * + *

+ * + * @param event the packet event. + */ + void onReceiving(final PacketEvent event); + + /** + * Called when sending a packet. + * + *

+ * + * @param event the packet event. + */ + void onSending(final PacketEvent event); + + /** Enumeration for Packet Event priority. */ + enum Priority { + + /** Very low importance and should be run first. */ + LOWEST(0), + + /** Low importance. */ + LOW(1), + + /** Neither important nor unimportant, and may be run normally. */ + NORMAL(2), + + /** High importance */ + HIGH(3), + + /** Critical and must have the final say in what happens. */ + HIGHEST(4), + + /** + * Listened to purely for monitoring the outcome of an event. + * + *

No modifications to the packet should be made under this priority + */ + MONITOR(5); + + private final int slot; + + Priority(final int slot) { + this.slot = slot; + } + + public int getSlot() { + return slot; + } + } +} diff --git a/placeholder-utils/pom.xml b/placeholder-utils/pom.xml new file mode 100644 index 0000000..4c0ffd5 --- /dev/null +++ b/placeholder-utils/pom.xml @@ -0,0 +1,54 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + placeholder-utils + + + 8 + 8 + + + + + + placeholderapi + https://repo.extendedclip.com/content/repositories/placeholderapi/ + + + + + + com.pepedevs + utils + ${project.parent.version} + + + com.pepedevs + vault-hook + ${project.parent.version} + + + + com.github.cryptomorin + XSeries + 8.4.0 + provided + + + + me.clip + placeholderapi + 2.10.10 + provided + + + + \ No newline at end of file diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/CorePlaceholderRegistry.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/CorePlaceholderRegistry.java new file mode 100644 index 0000000..9e7ae95 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/CorePlaceholderRegistry.java @@ -0,0 +1,61 @@ +package com.pepedevs.corelib.placeholders; + +import java.util.HashMap; +import java.util.Map; + +public class CorePlaceholderRegistry implements PlaceholderRegistry { + + private final Map placeholders; + private PlaceholderRegistry parent; + + public CorePlaceholderRegistry(Map handle) { + this.placeholders = handle; + this.parent = PlaceholderUtil.getRegistry(); + } + + public CorePlaceholderRegistry() { + this(new HashMap<>()); + } + + public CorePlaceholderRegistry(PlaceholderRegistry parent) { + this(new HashMap<>()); + this.parent = parent; + } + + @Override + public Placeholder getLocal(String key) { + return placeholders.get(key); + } + + @Override + public Placeholder get(String key) { + Placeholder p = placeholders.get(key); + return p != null ? p : parent != null ? parent.get(key) : null; + } + + @Override + public boolean hasLocal(String key) { + return placeholders.containsKey(key); + } + + @Override + public boolean has(String key) { + return placeholders.containsKey(key) || (parent != null && parent.has(key)); + } + + @Override + public CorePlaceholderRegistry set(Placeholder placeholder) { + placeholders.put(placeholder.getId(), placeholder); + return this; + } + + @Override + public PlaceholderRegistry getParent() { + return parent; + } + + @Override + public void setParent(PlaceholderRegistry parent) { + this.parent = parent; + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/Placeholder.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/Placeholder.java new file mode 100644 index 0000000..5d339f2 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/Placeholder.java @@ -0,0 +1,87 @@ +package com.pepedevs.corelib.placeholders; + +import org.bukkit.entity.Player; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Class for Placeholder resolving. */ +public interface Placeholder { + + static Placeholder of(String id, String obj) { + return new Placeholder() { + @Override + public String getId() { + return id; + } + + @Override + public String resolve(Player player, String arg) { + return obj; + } + }; + } + + static Placeholder of(String obj) { + return of(null, obj); + } + + static Placeholder of(String id, Supplier obj) { + return new Placeholder() { + @Override + public String getId() { + return id; + } + + @Override + public String resolve(Player player, String arg) { + return obj.get(); + } + }; + } + + static Placeholder of(Supplier obj) { + return of(null, obj); + } + + static Placeholder of(String id, Function obj) { + return new Placeholder() { + @Override + public String getId() { + return id; + } + + @Override + public String resolve(Player player, String arg) { + return obj.apply(player); + } + }; + } + + static Placeholder of(Function obj) { + return of(null, obj); + } + + static Placeholder of(String id, BiFunction obj) { + return new Placeholder() { + @Override + public String getId() { + return id; + } + + @Override + public String resolve(Player player, String arg) { + return obj.apply(player, arg); + } + }; + } + + static Placeholder of(BiFunction obj) { + return of(null, obj); + } + + String getId(); + + String resolve(Player player, String arg); +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderManager.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderManager.java new file mode 100644 index 0000000..91cb29f --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderManager.java @@ -0,0 +1,32 @@ +package com.pepedevs.corelib.placeholders; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public interface PlaceholderManager { + + /** + * Returns a read-only registry that can read the manager's placeholders + * + * @return the registry accessing the manager's placeholders + */ + PlaceholderRegistry getRegistry(); + + void register(Plugin plugin, Placeholder placeholder); + + boolean hasPlaceholders(String str); + + String apply(Player player, String text, PlaceholderRegistry registry); + + default String apply(Player player, String text) { + return apply(player, text, getRegistry()); + } + + default String single(Player player, String string) { + return apply(player, '%' + string + '%'); + } + + default String single(Player player, String string, PlaceholderRegistry registry) { + return apply(player, '%' + string + '%', registry); + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderRegistry.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderRegistry.java new file mode 100644 index 0000000..5174024 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderRegistry.java @@ -0,0 +1,117 @@ +package com.pepedevs.corelib.placeholders; + +import com.google.common.collect.ImmutableMap; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("unchecked") +public interface PlaceholderRegistry> { + + static CorePlaceholderRegistry wrap(Map handle) { + return new CorePlaceholderRegistry(handle); + } + + static CorePlaceholderRegistry wrap(String k1, String v1) { + return wrap(ImmutableMap.of(k1, Placeholder.of(v1))); + } + + static CorePlaceholderRegistry wrap(String k1, String v1, String k2, String v2) { + return wrap( + ImmutableMap.of( + k1, Placeholder.of(v1), + k2, Placeholder.of(v2))); + } + + static CorePlaceholderRegistry wrap( + String k1, String v1, String k2, String v2, String k3, String v3) { + return wrap( + ImmutableMap.of( + k1, Placeholder.of(v1), + k2, Placeholder.of(v2), + k3, Placeholder.of(v3))); + } + + static CorePlaceholderRegistry wrap( + String k1, + String v1, + String k2, + String v2, + String k3, + String v3, + String k4, + String v4) { + return wrap( + ImmutableMap.of( + k1, Placeholder.of(v1), + k2, Placeholder.of(v2), + k3, Placeholder.of(v3), + k4, Placeholder.of(v4))); + } + + static PlaceholderRegistry def() { + return PlaceholderUtil.getRegistry(); + } + + static CorePlaceholderRegistry create() { + return new CorePlaceholderRegistry(); + } + + static CorePlaceholderRegistry create(PlaceholderRegistry parent) { + return new CorePlaceholderRegistry(parent); + } + + PlaceholderRegistry getParent(); + + void setParent(PlaceholderRegistry parent); + + Placeholder getLocal(String key); + + default Placeholder get(String key) { + Placeholder p = getLocal(key); + if (p != null) return p; + else { + PlaceholderRegistry parent = getParent(); + return parent != null ? parent.get(key) : null; + } + } + + T set(Placeholder placeholder); + + default boolean has(String id) { + return get(id) != null; + } + + default boolean hasLocal(String id) { + return getLocal(id) != null; + } + + default T set(String id, Object obj) { + set(Placeholder.of(id, String.valueOf(obj))); + return (T) this; + } + + default T set(String id, Supplier obj) { + set(Placeholder.of(id, obj)); + return (T) this; + } + + default T set(String id, Function obj) { + set(Placeholder.of(id, obj)); + return (T) this; + } + + default T set(String id, BiFunction obj) { + set(Placeholder.of(id, obj)); + return (T) this; + } + + default T set(Map placeholders) { + for (Map.Entry p : placeholders.entrySet()) + set(p.getKey(), p.getValue()); + return (T) this; + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderUtil.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderUtil.java new file mode 100644 index 0000000..a4abc5a --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderUtil.java @@ -0,0 +1,110 @@ +package com.pepedevs.corelib.placeholders; + +import com.pepedevs.corelib.events.PluginLoadEvent; +import com.pepedevs.corelib.placeholders.events.PlaceholderManagerHookEvent; +import com.pepedevs.corelib.placeholders.managers.CustomPlaceholderManager; +import com.pepedevs.corelib.placeholders.managers.PAPIPlaceholderManager; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public final class PlaceholderUtil { + + private static PlaceholderManager manager = null; + + private PlaceholderUtil() {} + + public static PlaceholderValue parseByte(Object obj) { + if (obj instanceof Number) { + return PlaceholderValue.fake(((Number) obj).byteValue()); + } else if (obj instanceof String) return PlaceholderValue.byteValue((String) obj); + else return null; + } + + public static PlaceholderValue parseShort(Object obj) { + if (obj instanceof Number) { + return PlaceholderValue.fake(((Number) obj).shortValue()); + } else if (obj instanceof String) return PlaceholderValue.shortValue((String) obj); + else return null; + } + + public static PlaceholderValue parseInt(Object obj) { + if (obj instanceof Number) { + return PlaceholderValue.fake(((Number) obj).intValue()); + } else if (obj instanceof String) return PlaceholderValue.intValue((String) obj); + else return null; + } + + public static PlaceholderValue parseLong(Object obj) { + if (obj instanceof Number) { + return PlaceholderValue.fake(((Number) obj).longValue()); + } else if (obj instanceof String) return PlaceholderValue.longValue((String) obj); + else return null; + } + + public static PlaceholderValue parseFloat(Object obj) { + if (obj instanceof Number) { + return PlaceholderValue.fake(((Number) obj).floatValue()); + } else if (obj instanceof String) return PlaceholderValue.floatValue((String) obj); + else return null; + } + + public static PlaceholderValue parseDouble(Object obj) { + if (obj instanceof Number) { + return PlaceholderValue.fake(((Number) obj).doubleValue()); + } else if (obj instanceof String) return PlaceholderValue.doubleValue((String) obj); + else return null; + } + + public static void tryHook(Plugin plugin) { + PlaceholderManagerHookEvent event = new PlaceholderManagerHookEvent(); + PluginLoadEvent.onPluginLoaded(plugin, + "PlaceholderAPI", + pl -> { + Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getPluginManager().callEvent(event)); + manager = new PAPIPlaceholderManager(); + }, + () -> { + Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getPluginManager().callEvent(event)); + manager = event.getPlaceholderManager() != null ? event.getPlaceholderManager() : new CustomPlaceholderManager(plugin); + }); + } + + public static void register(Plugin plugin, Placeholder placeholder) { + manager.register(plugin, placeholder); + } + + public static PlaceholderValue process(String message) { + message = ChatColor.translateAlternateColorCodes('&', message); + return PlaceholderValue.stringValue(message); + } + + public static String resolve(Player player, String str) { + return manager.apply(player, str); + } + + public static String resolve(Player player, String str, PlaceholderRegistry local) { + return manager.apply(player, str, local); + } + + public static String placeholder(Player player, String str) { + return manager.apply(player, str); + } + + public static boolean hasPlaceholders(String str) { + return manager.hasPlaceholders(str); + } + + public static PlaceholderRegistry getRegistry() { + return manager.getRegistry(); + } + + public static PlaceholderManager getManager() { + return manager; + } + + public static void setManager(PlaceholderManager manager) { + PlaceholderUtil.manager = manager; + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderValue.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderValue.java new file mode 100644 index 0000000..750231f --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/PlaceholderValue.java @@ -0,0 +1,235 @@ +package com.pepedevs.corelib.placeholders; + +import com.cryptomorin.xseries.XSound; +import com.pepedevs.corelib.utils.ColorUtils; +import com.pepedevs.corelib.utils.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +public interface PlaceholderValue { + + static PlaceholderValue byteValue(String string) { + return value(string, Byte::parseByte, (byte) -1); + } + + static PlaceholderValue shortValue(String string) { + return value(string, Short::parseShort, (short) -1); + } + + static PlaceholderValue intValue(String string) { + return value(string, Integer::parseInt, -1); + } + + static PlaceholderValue longValue(String string) { + return value(string, Long::parseLong, -1L); + } + + static PlaceholderValue floatValue(String string) { + return value(string, Float::parseFloat, -1.0f); + } + + static PlaceholderValue doubleValue(String string) { + return value(string, Double::parseDouble, -1.0); + } + + static PlaceholderValue rawStringValue(String string) { + if (string == null) return null; + if (PlaceholderUtil.hasPlaceholders(string)) return new StringPlaceholderValue(string); + else return new FalsePlaceholderValue<>(string); + } + + static PlaceholderValue stringValue(String string) { + if (string == null) return null; + string = StringUtils.untranslateAlternateColorCodes(string); + if (PlaceholderUtil.hasPlaceholders(string)) return new StringPlaceholderValue(string); + else return new FalsePlaceholderValue<>(string); + } + + static PlaceholderValue colorValue(String string) { + return value(string, ColorUtils::parseToBukkitColor, Color.BLACK); + } + + static PlaceholderValue soundValue(String string) { + return value(string, (s) -> XSound.matchXSound(s).get().parseSound(), null); + } + + static PlaceholderValue fake(T value) { + return new FalsePlaceholderValue<>(value); + } + + static PlaceholderValue value(String string, Function parser, T onError) { + if (string == null) return null; + T parsed; + try { + parsed = parser.apply(string); + } catch (Exception e) { + if (!PlaceholderUtil.hasPlaceholders(string)) { + Bukkit.getLogger().severe("Invalid value: " + string); + return new FalsePlaceholderValue<>(onError); + } + return new SimplePlaceholderValue<>( + string, + parser, + (str, exc) -> + Bukkit.getLogger().severe("Cannot parse value: '" + str + "' (from '" + string + "')"), + onError); + } + return new FalsePlaceholderValue<>(parsed); + } + + /** + * Removes placeholders from the value and parses it using the local placeholders in addition to + * the normal ones. + * + *

+ * + * @param player Player that executes the placeholders + * @param local Placeholders to add to the default ones + * @return Parsed value without placeholders + */ + T resolve(Player player, PlaceholderRegistry local); + + /** + * Removes placeholders from the value and parses it. + * + *

+ * + * @param player Player that executes the placeholders + * @return Parsed value without placeholders + */ + default T resolve(Player player) { + return resolve(player, PlaceholderUtil.getRegistry()); + } + + /** + * @return true only if no real placeholders need to be resolved (a.k.a this is a fake + * PlaceholderValue) + */ + default boolean hasPlaceholders() { + return true; + } + + default PlaceholderValue map(Function mapper) { + return (p, l) -> mapper.apply(resolve(p, l)); + } + + String toString(); + + class FalsePlaceholderValue implements PlaceholderValue { + + private final T value; + + public FalsePlaceholderValue(T value) { + this.value = value; + } + + @Override + public T resolve(Player player, PlaceholderRegistry local) { + return value; + } + + @Override + public T resolve(Player player) { + return value; + } + + public String toString() { + return String.valueOf(value); + } + + @Override + public boolean hasPlaceholders() { + return false; + } + + @Override + public FalsePlaceholderValue map(Function mapper) { + return new FalsePlaceholderValue<>(mapper.apply(value)); + } + + public T getValue() { + return value; + } + } + + class SimplePlaceholderValue implements PlaceholderValue { + + private final String value; + + private final Function parser; + private final BiConsumer exceptionHandler; + private final T onError; + + public SimplePlaceholderValue( + String value, + Function parser, + BiConsumer exceptionHandler, + T onError) { + this.value = value; + this.parser = parser; + this.exceptionHandler = exceptionHandler; + this.onError = onError; + } + + @Override + public T resolve(Player player, PlaceholderRegistry local) { + return parse(PlaceholderUtil.resolve(player, value, local)); + } + + @Override + public T resolve(Player player) { + return parse(PlaceholderUtil.resolve(player, value)); + } + + protected T parse(String real) { + try { + return parser.apply(real); + } catch (Exception e) { + exceptionHandler.accept(real, e); + } + return onError; + } + + public String toString() { + return value; + } + + public String getValue() { + return value; + } + } + + class StringPlaceholderValue implements PlaceholderValue { + + private final String value; + + public StringPlaceholderValue(String value) { + this.value = value; + } + + @Override + public String resolve(Player player, PlaceholderRegistry local) { + return PlaceholderUtil.resolve(player, value, local); + } + + @Override + public String resolve(Player player) { + return PlaceholderUtil.resolve(player, value); + } + + public String toString() { + return value; + } + + public String getValue() { + return value; + } + + } + +} \ No newline at end of file diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/events/PlaceholderManagerHookEvent.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/events/PlaceholderManagerHookEvent.java new file mode 100644 index 0000000..0387f7c --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/events/PlaceholderManagerHookEvent.java @@ -0,0 +1,29 @@ +package com.pepedevs.corelib.placeholders.events; + +import com.pepedevs.corelib.placeholders.PlaceholderManager; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +public class PlaceholderManagerHookEvent extends Event { + + private static final HandlerList handlers = new HandlerList(); + + private PlaceholderManager placeholderManager = null; + + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public PlaceholderManager getPlaceholderManager() { + return placeholderManager; + } + + public void setPlaceholderManager(PlaceholderManager placeholderManager) { + this.placeholderManager = placeholderManager; + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/BasePlaceholderManager.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/BasePlaceholderManager.java new file mode 100644 index 0000000..48e1a94 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/BasePlaceholderManager.java @@ -0,0 +1,82 @@ +package com.pepedevs.corelib.placeholders.managers; + +import com.pepedevs.corelib.placeholders.Placeholder; +import com.pepedevs.corelib.placeholders.PlaceholderManager; +import com.pepedevs.corelib.placeholders.PlaceholderRegistry; +import org.bukkit.entity.Player; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class BasePlaceholderManager implements PlaceholderManager { + + public static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%"); + + public static String exec(Player player, String text, Function finder) { + Placeholder found = finder.apply(text); + + if (found != null) { + return found.resolve(player, ""); + } + + int index = text.lastIndexOf('_'); + + while (index >= 0) { + String id = text.substring(0, index); + String arg = text.substring(index + 1); + found = finder.apply(id); + if (found != null) { + try { + return found.resolve(player, arg); + } catch (Exception e) { + return null; + } + } + index = text.lastIndexOf('_', index - 1); + } + return null; + } + + public boolean hasPlaceholders(String string) { + return PLACEHOLDER_PATTERN.matcher(string).find(); + } + + @Override + public String apply(Player player, String text) { + return apply(player, text, this::find); + } + + @Override + public String apply(Player player, String text, PlaceholderRegistry local) { + return apply(player, text, local::get); + } + + @Override + public String single(Player player, String string) { + return exec(player, string, this::find); + } + + @Override + public String single(Player player, String text, PlaceholderRegistry local) { + return exec(player, text, this::find); + } + + public String apply(Player player, String text, Function finder) { + Matcher matcher = PLACEHOLDER_PATTERN.matcher(text); + StringBuffer res = new StringBuffer(); + + while (matcher.find()) { + String str = matcher.group(1); + String replacement = exec(player, str, finder); + if (replacement != null) { + matcher.appendReplacement(res, Matcher.quoteReplacement(replacement)); + } + } + matcher.appendTail(res); + + return res.toString(); + } + + protected abstract Placeholder find(String id); +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/CustomPlaceholderManager.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/CustomPlaceholderManager.java new file mode 100644 index 0000000..4032ea5 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/CustomPlaceholderManager.java @@ -0,0 +1,73 @@ +package com.pepedevs.corelib.placeholders.managers; + +import com.pepedevs.corelib.placeholders.Placeholder; +import com.pepedevs.corelib.placeholders.PlaceholderRegistry; +import com.pepedevs.corelib.placeholders.managers.customs.PlayerPlaceholder; +import com.pepedevs.corelib.placeholders.managers.customs.VaultPlaceholder; +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; +import java.util.Map; + +public class CustomPlaceholderManager extends BasePlaceholderManager { + + private final PlaceholderRegistry registry = new CustomPlaceholderRegistry(); + private Map placeholders = new HashMap<>(); + + public CustomPlaceholderManager(Plugin plugin) { + addDefaults(plugin); + } + + @Override + public void register(Plugin plugin, Placeholder placeholder) { + placeholders.put(placeholder.getId(), placeholder); + } + + @Override + protected Placeholder find(String id) { + return placeholders.get(id); + } + + public void addDefaults(Plugin plugin) { + register(plugin, new PlayerPlaceholder()); + register(plugin, new VaultPlaceholder()); + } + + @Override + public PlaceholderRegistry getRegistry() { + return registry; + } + + private class CustomPlaceholderRegistry + implements PlaceholderRegistry { + + public PlaceholderRegistry getParent() { + return null; + } + + public void setParent(PlaceholderRegistry parent) { + throw new UnsupportedOperationException(); + } + + public Placeholder getLocal(String key) { + return placeholders.get(key); + } + + public Placeholder get(String key) { + return placeholders.get(key); + } + + public CustomPlaceholderRegistry set(Placeholder placeholder) { + throw new UnsupportedOperationException( + "Use PlaceholderUtil.register o PlaceholderManager#register instead!"); + } + + public boolean has(String id) { + return placeholders.containsKey(id); + } + + public boolean hasLocal(String id) { + return placeholders.containsKey(id); + } + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/PAPIPlaceholderManager.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/PAPIPlaceholderManager.java new file mode 100644 index 0000000..eb531fa --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/PAPIPlaceholderManager.java @@ -0,0 +1,157 @@ +package com.pepedevs.corelib.placeholders.managers; + +import com.pepedevs.corelib.placeholders.Placeholder; +import com.pepedevs.corelib.placeholders.PlaceholderRegistry; +import me.clip.placeholderapi.PlaceholderAPI; +import me.clip.placeholderapi.PlaceholderHook; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.Map; + +/** PlaceholderManager that hooks into PlaceholderAPI (Papi) and works with it. */ +public class PAPIPlaceholderManager extends BasePlaceholderManager { + + private final PapiPlaceholderRegistry registry = new PapiPlaceholderRegistry(); + + @Override + public void register(Plugin plugin, Placeholder placeholder) { + new OfficialPlaceholderAdapter(plugin, placeholder).register(); + } + + @SuppressWarnings("deprecation") + public Map getPlaceholders() { + return PlaceholderAPI.getPlaceholders(); + } + + @Override + public PapiPlaceholderRegistry getRegistry() { + return registry; + } + + public Placeholder find(String id) { + PlaceholderHook hook = getPlaceholders().get(id); + return hook == null + ? null + : new Placeholder() { + @Override + public String getId() { + return id; + } + + @Override + public String resolve(Player player, String arg) { + return hook.onPlaceholderRequest(player, arg); + } + }; + } + + private static class OfficialPlaceholderAdapter extends PlaceholderExpansion { + + private final Placeholder placeholder; + private final Plugin plugin; + + public OfficialPlaceholderAdapter(Plugin plugin, Placeholder placeholder) { + this.plugin = plugin; + this.placeholder = placeholder; + } + + @Override + public String onPlaceholderRequest(Player player, String identifier) { + return placeholder.resolve(player, identifier); + } + + @Override + public String getIdentifier() { + return placeholder.getId(); + } + + @Override + public String getAuthor() { + return String.join(", ", plugin.getDescription().getAuthors()); + } + + @Override + public String getVersion() { + return plugin.getDescription().getVersion(); + } + + /** + * Because this is an internal class, you must override this method to let PlaceholderAPI + * know to not unregister your expansion class when PlaceholderAPI is reloaded + * + * @return true to persist through reloads + */ + @Override + public boolean persist() { + return true; + } + + /** + * Because this is a internal class, this check is not needed and we can simply return + * {@code true} + * + * @return Always true since it's an internal class. + */ + @Override + public boolean canRegister() { + return true; + } + } + + private static class PlaceholderHookWrapper implements Placeholder { + + private final String id; + private final PlaceholderHook hook; + + private PlaceholderHookWrapper(String id, PlaceholderHook hook) { + this.id = id; + this.hook = hook; + } + + @Override + public String resolve(Player player, String arg) { + return hook.onRequest(player, arg); + } + + @Override + public String getId() { + return id; + } + } + + private class PapiPlaceholderRegistry implements PlaceholderRegistry { + + public PlaceholderRegistry getParent() { + return null; + } + + public void setParent(PlaceholderRegistry parent) { + throw new UnsupportedOperationException(); + } + + public Placeholder getLocal(String key) { + PlaceholderHook hook = getPlaceholders().get(key); + return hook == null ? null : new PlaceholderHookWrapper(key, hook); + } + + public Placeholder get(String key) { + PlaceholderHook hook = getPlaceholders().get(key); + return hook == null ? null : new PlaceholderHookWrapper(key, hook); + } + + public PapiPlaceholderRegistry set(Placeholder placeholder) { + throw new UnsupportedOperationException( + "Use PlaceholderUtil.register or PlaceholderManager#register instead!"); + } + + public boolean has(String id) { + return getPlaceholders().containsKey(id); + } + + public boolean hasLocal(String id) { + return getPlaceholders().containsKey(id); + } + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/customs/PlayerPlaceholder.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/customs/PlayerPlaceholder.java new file mode 100644 index 0000000..1178eca --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/customs/PlayerPlaceholder.java @@ -0,0 +1,31 @@ +package com.pepedevs.corelib.placeholders.managers.customs; + +import com.pepedevs.corelib.placeholders.Placeholder; +import org.bukkit.entity.Player; + +public class PlayerPlaceholder implements Placeholder { + + @Override + public String getId() { + return "player"; + } + + @Override + public String resolve(Player player, String arg) { + switch (arg) { + case "display_name": + return player.getDisplayName(); + case "food_level": + return Integer.toString(player.getFoodLevel()); + case "health": + return Double.toString(player.getHealth()); + case "level": + return Integer.toString(player.getLevel()); + case "saturation": + return Float.toString(player.getSaturation()); + case "world": + return player.getWorld().getName(); + } + return null; + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/customs/VaultPlaceholder.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/customs/VaultPlaceholder.java new file mode 100644 index 0000000..0dea069 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/managers/customs/VaultPlaceholder.java @@ -0,0 +1,29 @@ +package com.pepedevs.corelib.placeholders.managers.customs; + +import com.pepedevs.corelib.economy.Balance; +import com.pepedevs.corelib.economy.EconomyManager; +import com.pepedevs.corelib.placeholders.Placeholder; +import org.bukkit.entity.Player; + +public class VaultPlaceholder implements Placeholder { + + @Override + public String getId() { + return "vault"; + } + + @Override + public String resolve(Player player, String id) { + Balance economy = EconomyManager.get(player); + double balance = economy == null ? 0 : economy.get(); + + switch (id) { + case "eco_balance": + return String.valueOf(balance); + case "eco_balance_fixed": + return String.valueOf((long) balance); + } + + return null; + } +} diff --git a/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/message/PlaceholderMessage.java b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/message/PlaceholderMessage.java new file mode 100644 index 0000000..fdd46a4 --- /dev/null +++ b/placeholder-utils/src/main/java/com/pepedevs/corelib/placeholders/message/PlaceholderMessage.java @@ -0,0 +1,315 @@ +package com.pepedevs.corelib.placeholders.message; + +import com.pepedevs.corelib.placeholders.PlaceholderRegistry; +import com.pepedevs.corelib.placeholders.PlaceholderValue; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.block.Sign; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class PlaceholderMessage { + + private final List> lines; + + public PlaceholderMessage(List> lines) { + this.lines = lines; + } + + public PlaceholderMessage(Node node) { + checkTag(node, Arrays.asList(Tag.STR, Tag.SEQ)); + if (node.getTag() == Tag.STR) { + String messageStr = ((ScalarNode) node).getValue(); + lines = Collections.singletonList(PlaceholderValue.stringValue(messageStr)); + } else { // node.getTag() == Tag.SEQ + lines = + ((SequenceNode) node) + .getValue().stream() + .map( + lineNode -> { + checkTag(lineNode, Tag.STR); + return PlaceholderValue.stringValue( + ((ScalarNode) lineNode).getValue()); + }) + .collect(Collectors.toList()); + } + } + + public static PlaceholderMessage fromConfig(Object obj) { + if (obj == null) return null; + if (obj instanceof Collection) { + return new PlaceholderMessage( + ((Collection) obj) + .stream() + .map(o -> PlaceholderValue.stringValue(o.toString())) + .collect(Collectors.toList())); + } else { + return new PlaceholderMessage( + Collections.singletonList(PlaceholderValue.stringValue(obj.toString()))); + } + } + + public static PlaceholderMessage fromText(List lines) { + return new PlaceholderMessage(lines.stream().map(PlaceholderValue::fake).collect(Collectors.toList())); + } + + public static PlaceholderMessage fromText(String... lines) { + return fromText(Arrays.asList(lines)); + } + + public static void checkTag(Node node, Tag expected) { + if (node.getTag() != expected) { + throw new IllegalStateException( + "Wrong type: found " + + node.getTag() + + ", expected: " + + Arrays.stream(new Tag[] {expected}) + .map(Tag::getValue) + .collect(Collectors.joining(", "))); + } + } + + public static void checkTag(Node node, Collection expected) { + if (!expected.contains(node.getTag())) { + throw new IllegalStateException( + "Wrong type: found " + + node.getTag() + + ", expected: " + + Arrays.stream(expected.toArray(new Tag[0])) + .map(Tag::getValue) + .collect(Collectors.joining(", "))); + } + } + + // --------------FILTER + + public List get(Player player) { + return lines.stream().map(p -> p.resolve(player)).collect(Collectors.toList()); + } + + public List get(Player player, PlaceholderRegistry placeholders) { + return lines.stream() + .map(p -> p.resolve(player, placeholders)) + .collect(Collectors.toList()); + } + + public List get(Player player, String k1, String v2) { + return get(player, PlaceholderRegistry.wrap(k1, v2)); + } + + public List get(Player player, String k1, String v1, String k2, String v2) { + return get( + player, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2)); + } + + // --------------SEND + + public List get( + Player player, String k1, String v1, String k2, String v2, String k3, String v3) { + return get( + player, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2, + k3, v3)); + } + + public PlaceholderMessage filter(PlaceholderRegistry reg) { + return new PlaceholderMessage( + lines.stream() + .map(p -> PlaceholderValue.rawStringValue(p.resolve(null, reg))) + .collect(Collectors.toList())); + } + + public PlaceholderMessage filter(String k1, String v1) { + return filter(PlaceholderRegistry.wrap(k1, v1)); + } + + public PlaceholderMessage filter(String k1, String v1, String k2, String v2) { + return filter( + PlaceholderRegistry.wrap( + k1, v1, + k2, v2)); + } + + public PlaceholderMessage filter(String k1, String v1, String k2, String v2, String k3, String v3) { + return filter( + PlaceholderRegistry.wrap( + k1, v1, + k2, v2, + k3, v3)); + } + + public void send(Player player) { + for (PlaceholderValue message : lines) + player.sendMessage(ChatColor.translateAlternateColorCodes('&', message.resolve(player))); + } + + public void send(Player player, PlaceholderRegistry placeholders) { + for (PlaceholderValue message : lines) + player.sendMessage( + ChatColor.translateAlternateColorCodes('&', + message.resolve(player, placeholders))); + } + + public void send(Player player, String k1, String v1) { + send(player, PlaceholderRegistry.wrap(k1, v1)); + } + + public void send(Player player, String k1, String v1, String k2, String v2) { + send( + player, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2)); + } + + public void send( + Player player, String k1, String v1, String k2, String v2, String k3, String v3) { + send( + player, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2, + k3, v3)); + } + + public void send(CommandSender sender) { + for (PlaceholderValue message : lines) + sender.sendMessage(ChatColor.translateAlternateColorCodes('&', message.resolve(null))); + } + + public void send(CommandSender sender, PlaceholderRegistry placeholders) { + for (PlaceholderValue message : lines) + sender.sendMessage( + ChatColor.translateAlternateColorCodes('&', message.resolve(null, placeholders))); + } + + public void send(CommandSender sender, String k1, String v1) { + send(sender, PlaceholderRegistry.wrap(k1, v1)); + } + + public void send(CommandSender sender, String k1, String v1, String k2, String v2) { + send( + sender, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2)); + } + + public void send( + CommandSender sender, + String k1, + String v1, + String k2, + String v2, + String k3, + String v3) { + send( + sender, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2, + k3, v3)); + } + + // --------------BROADCAST + public void broadcast(Iterable players) { + for (Player player : players) + for (PlaceholderValue message : lines) + player.sendMessage(message.resolve(player)); + } + + public void broadcast(Iterable players, PlaceholderRegistry placeholders) { + for (Player player : players) + for (PlaceholderValue message : lines) + player.sendMessage(message.resolve(player, placeholders)); + } + + public void broadcast(Iterable players, String k1, String v1) { + broadcast(players, PlaceholderRegistry.wrap(k1, v1)); + } + + public void broadcast( + Iterable players, String k1, String v1, String k2, String v2) { + broadcast( + players, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2)); + } + + public void broadcast( + Iterable players, + String k1, + String v1, + String k2, + String v2, + String k3, + String v3) { + broadcast( + players, + PlaceholderRegistry.wrap( + k1, v1, + k2, v2, + k3, v3)); + } + + public void broadcast() { + broadcast(Bukkit.getOnlinePlayers()); + } + + public void broadcast(PlaceholderRegistry placeholders) { + broadcast(Bukkit.getOnlinePlayers(), placeholders); + } + + public void broadcast(String k1, String v1) { + broadcast(Bukkit.getOnlinePlayers(), PlaceholderRegistry.wrap(k1, v1)); + } + + public void broadcast(String k1, String v1, String k2, String v2) { + broadcast( + Bukkit.getOnlinePlayers(), + PlaceholderRegistry.wrap( + k1, v1, + k2, v2)); + } + + public void broadcast(String k1, String v1, String k2, String v2, String k3, String v3) { + broadcast( + Bukkit.getOnlinePlayers(), + PlaceholderRegistry.wrap( + k1, v1, + k2, v2, + k3, v3)); + } + + // --------------SIGNS + public void setSign(Sign sign, Player player, PlaceholderRegistry placeholders) { + int i = 0; + for (; i < Math.min(4, lines.size()); i++) { + sign.setLine(i, lines.get(i).resolve(player, placeholders)); + } + for (; i < 4; i++) { + sign.setLine(i, ""); + } + sign.update(); + } + + public List> getLines() { + return lines; + } +} diff --git a/plugin-helper/pom.xml b/plugin-helper/pom.xml new file mode 100644 index 0000000..eac00d9 --- /dev/null +++ b/plugin-helper/pom.xml @@ -0,0 +1,26 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + plugin-helper + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/Plugin.java b/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/Plugin.java new file mode 100644 index 0000000..9b7af8a --- /dev/null +++ b/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/Plugin.java @@ -0,0 +1,221 @@ +package com.pepedevs.corelib.plugin; + +import com.pepedevs.corelib.utils.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; + +/** An implementation of {@link JavaPlugin} that adds some useful utilities. */ +public abstract class Plugin extends JavaPlugin { + + protected String compilation_id; + + @Override + public final void onEnable() { + /* checking the plugin dependencies */ + if (getDependences() != null && getDependences().length > 0) { + for (PluginDependence dependence : getDependences()) { + final org.bukkit.plugin.Plugin plugin = + Bukkit.getPluginManager().getPlugin(dependence.getName()); + final Boolean result = dependence.apply(plugin); + + if (result == null || !result) { + Bukkit.getPluginManager().disablePlugin(this); + return; + } + } + } + + /* plugin setup */ + if (!setUp() || !isEnabled()) { + Bukkit.getPluginManager().disablePlugin(this); + return; + } + + /* finalizing plugin setup */ + try { + this.setUpConfig(); + this.setUpHandlers(); + this.setUpCommands(); + this.setUpListeners(); + } catch (Throwable ex) { + // any exception will disable the plugin + ex.printStackTrace(); + Bukkit.getPluginManager().disablePlugin(this); + } + } + + /** + * Setups this plugin. This methods will be called for the initialization of this plugin after + * checking the required core version, and the dependences. + * + *

Also this methods should return {@code true} if the initialization was + * successfully, and {@code false} if not. Returning {@code + * false} from this method means that the initialization was unsuccessfully, then the + * plugin will be disabled automatically. + * + *

+ * + * @return true if the initialization was successfully. + */ + protected abstract boolean setUp(); + + /** + * Gets the plugins on which this plugin depends. + * + *

This methods might return an empty array or {@code null} if this plugin + * doesn't depend on another. + * + *

+ * + * @return The dependencies or null if this plugin doesn't depend on another. + * @see PluginDependence + */ + public abstract PluginDependence[] getDependences(); + + /** + * This method should setups the configuration. + * + *

Also this should return {@code true} if the configuration was loaded + * successfully, and {@code false} if not. + * + *

Note that if this methods returns {@code false}, then the plugin will be + * disabled automatically. + * + *

Note that this methods will be called after: + * + *

+ * + *

+ * + * @return whether the configuration was loaded successfully. + */ + protected abstract boolean setUpConfig(); + + /** + * This method should setups the plugin handlers. + * + *

Also this should return {@code true} if the initialization of the + * handlers was successfully, and {@code false} if not. + * + *

Note that if this methods returns {@code false}, then the plugin will be + * disabled automatically. + * + *

Note that this methods will be called after: + * + *

+ * + *

+ * + * @return Whether the initialization of the handlers was successfully. + */ + protected abstract boolean setUpHandlers(); + + /** + * This method should setups the commands of the plugin. + * + *

Also this should return {@code true} if the initialization of the + * commands was successfully, and {@code false} if not. + * + *

Note that if this methods returns {@code false}, then the plugin will be + * disabled automatically. + * + *

Note that this methods will be called after: + * + *

+ * + *

+ * + * @return Whether the initialization of the commands was successfully. + */ + protected abstract boolean setUpCommands(); + + /** + * This method should setups the listeners of the plugin. + * + *

Also this should return {@code true} if the initialization of the + * listeners was successfully, and {@code false} if not. + * + *

Note that if this methods returns {@code false}, then the plugin will be + * disabled automatically. + * + *

Note that this methods will be called after: + * + *

+ * + * @return Whether the initialization of the listeners was successfully. + */ + protected abstract boolean setUpListeners(); + + /** + * Saves the raw contents of any resource embedded with this plugin's .jar file assuming it can + * be found using {@link #getResource(String)}. + * + *

The resource is saved into the desired {@code out_directory}. + * + *

+ * + * @param resource_path The embedded resource path to look for within the plugin's .jar file. + * (No preceding slash). + * @param out_directory The directory into which the resource will be saved. + * @param replace If true, the embedded resource will overwrite the contents of an existing + * file. + */ + public void saveResource(String resource_path, File out_directory, boolean replace) { + URL url = this.getClassLoader().getResource(resource_path = resource_path.replace('\\', '/')); + if (url != null) { + File out = + new File( + out_directory, + resource_path.lastIndexOf('/') != -1 + ? resource_path.substring(resource_path.lastIndexOf('/') + 1) + : resource_path); + + if (!out.exists() || replace) { + try { + FileUtils.copyURLToFile(url, out); + } catch (IOException ex) { + this.getLogger().severe("Couldn't save resource " + resource_path + " to " + out); + ex.printStackTrace(); + } + } + } else { + throw new IllegalArgumentException( + "The embedded resource '" + + resource_path + + "' cannot be found in " + + getFile()); + } + } + + protected String getCompilationId() { + if (compilation_id == null) { + YamlConfiguration plugin = + YamlConfiguration.loadConfiguration( + new InputStreamReader(this.getResource("plugin.yml"))); + + compilation_id = plugin.getString("compid", String.valueOf(0)); + } + + return compilation_id; + } +} diff --git a/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/PluginAdapter.java b/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/PluginAdapter.java new file mode 100644 index 0000000..192bcf2 --- /dev/null +++ b/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/PluginAdapter.java @@ -0,0 +1,33 @@ +package com.pepedevs.corelib.plugin; + +/** + * An convenience implementation of {@link Plugin}. Derive from this and only override what you + * need. + */ +public abstract class PluginAdapter extends Plugin { + + @Override + public PluginDependence[] getDependences() { + return null; + } + + @Override + protected boolean setUpConfig() { + return true; + } + + @Override + protected boolean setUpHandlers() { + return true; + } + + @Override + protected boolean setUpCommands() { + return true; + } + + @Override + protected boolean setUpListeners() { + return true; + } +} diff --git a/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/PluginDependence.java b/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/PluginDependence.java new file mode 100644 index 0000000..d794911 --- /dev/null +++ b/plugin-helper/src/main/java/com/pepedevs/corelib/plugin/PluginDependence.java @@ -0,0 +1,79 @@ +package com.pepedevs.corelib.plugin; + +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.util.function.Function; + +/** + * Represents a possible plugin on which a {@link Plugin} may depend. + * + *

This class implements {@link Function} for receiving the plugin on which a {@link Plugin} + * depends, and producing a resultant {@link Boolean}, the function might receive {@code + * null} that means the plugin has never been loaded by Bukkit plugin manager, in that + * case, if the developer return {@code null} from this function, the plugin will + * be disabled automatically. + * + *

Also the developer can send messages to the console when checking the received plugin. + * + *

+ * + *

Implementation example:

+ * + *

+ * PluginDependence dependence = new PluginDependence("ProtocolLib") { 
+ * {@code @Override}
+ * public Boolean apply (org.bukkit.plugin.Plugin plugin) {
+ * if (plugin == null) {
+ * ConsoleUtil.sendPluginMessage(ChatColor.RED, "ProtocolLib couldn't be found!", MyPlugin.getInstance());
+ * } else {
+ * return true; // returning true will not disable the plugin.
+ * }
+ * return null; // returning null or false will disable the plugin automatically
+ * }
+ * };
+ *
+ */ +public abstract class PluginDependence implements Function { + + /** The name of the depending plugin */ + protected final String name; + + protected final boolean enabled; + + /** + * Construct the plugin dependence. Note the plugin {@code name} is case-sensitive. + * + *

+ * + * @param name Name of the depending plugin. + */ + public PluginDependence(final String name) { + Validate.notNull(name, "the name cannot be null!"); + this.name = name; + this.enabled = Bukkit.getPluginManager().isPluginEnabled(name); + } + + /** + * Gets the name of the depending plugin. + * + *

+ * + * @return Name of the depending plugin. + */ + public String getName() { + return name; + } + + /** + * Returns if the plugin found enable and hooked. + * + *

+ * + * @return {@code true} if the plugin is enabled, false otherwise + */ + public boolean isEnabled() { + return this.enabled; + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4813248 --- /dev/null +++ b/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + com.pepedevs + CoreLib + pom + 1.0 + + action-item + bungee-utils + configuration-utils + corelib + database + event-utils + inventory-gui + minecraft-version + packet-utils + placeholder-utils + plugin-helper + reflection-utils + scoreboard + task-utils + utils + vault-hook + + + + 8 + 8 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + org.bstats + com.pepedevs.corelib.libs.bstats + + + xyz.xenondevs.particle + com.pepedevs.corelib.particles + + + com.cryptomorin.xseries + com.pepedevs.corelib.utils.xseries + + + + + *:* + + com/cryptomorin/xseries/particles/* + com/cryptomorin/xseries/messages/* + + + + + + + package + + shade + + + + + + + + + + + + + + + + + + + + + + + spigotmc-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + sonatype + https://oss.sonatype.org/content/groups/public/ + + + + + + org.spigotmc + spigot-api + 1.16.3-R0.1-SNAPSHOT + provided + + + + \ No newline at end of file diff --git a/reflection-utils/pom.xml b/reflection-utils/pom.xml new file mode 100644 index 0000000..5aeed30 --- /dev/null +++ b/reflection-utils/pom.xml @@ -0,0 +1,34 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + reflection-utils + + + 8 + 8 + + + + + com.pepedevs + minecraft-version + ${project.parent.version} + + + + org.reflections + reflections + 0.10.2 + + + + + \ No newline at end of file diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/DataType.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/DataType.java new file mode 100644 index 0000000..fd2ed79 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/DataType.java @@ -0,0 +1,222 @@ +package com.pepedevs.corelib.utils.reflection; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents an enumeration of Java data types with corresponding classes + */ +public enum DataType { + + BYTE(byte.class, Byte.class), + SHORT(short.class, Short.class), + INTEGER(int.class, Integer.class), + LONG(long.class, Long.class), + CHARACTER(char.class, Character.class), + FLOAT(float.class, Float.class), + DOUBLE(double.class, Double.class), + BOOLEAN(boolean.class, Boolean.class); + + private static final Map, DataType> CLASS_MAP = new HashMap, DataType>(); + + // Initialize map for quick class lookup + static { + for (DataType type : values()) { + CLASS_MAP.put(type.primitive, type); + CLASS_MAP.put(type.reference, type); + } + } + + private final Class primitive; + private final Class reference; + + /** + * Construct a new data type + * + *

+ * + * @param primitive Primitive class of this data type + * @param reference Reference class of this data type + */ + DataType(Class primitive, Class reference) { + this.primitive = primitive; + this.reference = reference; + } + + /** + * Returns the data type with the given primitive/reference class + * + *

+ * + * @param clazz Primitive/Reference class of the data type + * @return The data type + */ + public static DataType fromClass(Class clazz) { + return CLASS_MAP.get(clazz); + } + + /** + * Returns the primitive class of the data type with the given reference class + * + *

+ * + * @param clazz Reference class of the data type + * @return The primitive class + */ + public static Class getPrimitive(Class clazz) { + DataType type = fromClass(clazz); + return type == null ? clazz : type.getPrimitive(); + } + + /** + * Returns the reference class of the data type with the given primitive class + * + *

+ * + * @param clazz Primitive class of the data type + * @return The reference class + */ + public static Class getReference(Class clazz) { + DataType type = fromClass(clazz); + return type == null ? clazz : type.getReference(); + } + + /** + * Returns the primitive class array of the given class array + * + *

+ * + * @param classes Given class array + * @return The primitive class array + */ + public static Class[] getPrimitive(Class[] classes) { + int length = classes == null ? 0 : classes.length; + Class[] types = new Class[length]; + for (int index = 0; index < length; index++) { + types[index] = getPrimitive(classes[index]); + } + return types; + } + + /** + * Returns the reference class array of the given class array + * + *

+ * + * @param classes Given class array + * @return The reference class array + */ + public static Class[] getReference(Class[] classes) { + int length = classes == null ? 0 : classes.length; + Class[] types = new Class[length]; + for (int index = 0; index < length; index++) { + types[index] = getReference(classes[index]); + } + return types; + } + + /** + * Returns the primitive class array of the given object array + * + *

+ * + * @param objects Given object array + * @return The primitive class array + */ + public static Class[] getPrimitive(Object[] objects) { + int length = objects == null ? 0 : objects.length; + Class[] types = new Class[length]; + for (int index = 0; index < length; index++) { + types[index] = getPrimitive(objects[index].getClass()); + } + return types; + } + + /** + * Returns the reference class array of the given object array + * + *

+ * + * @param objects Given object array + * @return The reference class array + */ + public static Class[] getReference(Object[] objects) { + int length = objects == null ? 0 : objects.length; + Class[] types = new Class[length]; + for (int index = 0; index < length; index++) { + types[index] = getReference(objects[index].getClass()); + } + return types; + } + + /** + * Compares two class arrays on equivalence + * + *

+ * + * @param primary Primary class array + * @param secondary Class array which is compared to the primary array + * @return Whether these arrays are equal or not + */ + public static boolean compare(Class[] primary, Class[] secondary) { + if (primary == null || secondary == null || primary.length != secondary.length) { + return false; + } + for (int index = 0; index < primary.length; index++) { + Class primaryClass = primary[index]; + Class secondaryClass = secondary[index]; + if (primaryClass.equals(secondaryClass) + || primaryClass.isAssignableFrom(secondaryClass)) { + continue; + } + return false; + } + return true; + } + + /** + * Returns the primitive class of this data type + * + *

+ * + * @return The primitive class + */ + public Class getPrimitive() { + return primitive; + } + + /** + * Returns the reference class of this data type + * + *

+ * + * @return The reference class + */ + public Class getReference() { + return reference; + } + + /** + * Returns true if the given Object is a valid instance of this data type. + * + *

+ * + * @param obj Object to check + * @return true if the given Object is a valid instance of this data type + */ + public boolean isInstance(Object obj) { + return obj != null + && (obj.getClass().equals(getPrimitive()) || obj.getClass().equals(getReference())); + } + + /** + * Gets whether this represents a number data type. + * + *

+ * + * @return Whether this represents a number data type + */ + public boolean isNumber() { + return Number.class.isAssignableFrom(reference); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/accessor/FieldAccessor.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/accessor/FieldAccessor.java new file mode 100644 index 0000000..9177d86 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/accessor/FieldAccessor.java @@ -0,0 +1,154 @@ +package com.pepedevs.corelib.utils.reflection.accessor; + +import sun.misc.Unsafe; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public class FieldAccessor { + + private final Field field; + + public FieldAccessor(Field field) { + this.field = field; + field.setAccessible(true); + } + + private static void setField(Object object, Object value, Field foundField) { + boolean isStatic = (foundField.getModifiers() & Modifier.STATIC) == Modifier.STATIC; + if (isStatic) { + setStaticFieldUsingUnsafe(foundField, value); + } else { + setFieldUsingUnsafe(foundField, object, value); + } + } + + private static void setStaticFieldUsingUnsafe(final Field field, final Object newValue) { + try { + field.setAccessible(true); + int fieldModifiersMask = field.getModifiers(); + boolean isFinalModifierPresent = + (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL; + if (isFinalModifierPresent) { + AccessController.doPrivileged( + (PrivilegedAction) + () -> { + try { + Unsafe unsafe = getUnsafe(); + long offset = unsafe.staticFieldOffset(field); + Object base = unsafe.staticFieldBase(field); + setFieldUsingUnsafe( + base, field.getType(), offset, newValue, unsafe); + return null; + } catch (Throwable t) { + throw new RuntimeException(t); + } + }); + } else { + field.set(null, newValue); + } + } catch (SecurityException | IllegalAccessException | IllegalArgumentException ex) { + throw new RuntimeException(ex); + } + } + + private static void setFieldUsingUnsafe( + final Field field, final Object object, final Object newValue) { + try { + field.setAccessible(true); + int fieldModifiersMask = field.getModifiers(); + boolean isFinalModifierPresent = + (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL; + if (isFinalModifierPresent) { + AccessController.doPrivileged( + (PrivilegedAction) + () -> { + try { + Unsafe unsafe = getUnsafe(); + long offset = unsafe.objectFieldOffset(field); + setFieldUsingUnsafe( + object, field.getType(), offset, newValue, unsafe); + return null; + } catch (Throwable t) { + throw new RuntimeException(t); + } + }); + } else { + try { + field.set(object, newValue); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + } catch (SecurityException ex) { + throw new RuntimeException(ex); + } + } + + private static Unsafe getUnsafe() + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, + SecurityException { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } + + private static void setFieldUsingUnsafe( + Object base, Class type, long offset, Object newValue, Unsafe unsafe) { + if (type == Integer.TYPE) { + unsafe.putInt(base, offset, ((Integer) newValue)); + } else if (type == Short.TYPE) { + unsafe.putShort(base, offset, ((Short) newValue)); + } else if (type == Long.TYPE) { + unsafe.putLong(base, offset, ((Long) newValue)); + } else if (type == Byte.TYPE) { + unsafe.putByte(base, offset, ((Byte) newValue)); + } else if (type == Boolean.TYPE) { + unsafe.putBoolean(base, offset, ((Boolean) newValue)); + } else if (type == Float.TYPE) { + unsafe.putFloat(base, offset, ((Float) newValue)); + } else if (type == Double.TYPE) { + unsafe.putDouble(base, offset, ((Double) newValue)); + } else if (type == Character.TYPE) { + unsafe.putChar(base, offset, ((Character) newValue)); + } else { + unsafe.putObject(base, offset, newValue); + } + } + + public boolean isStatic() { + return Modifier.isStatic(field.getModifiers()); + } + + public boolean isPublic() { + return Modifier.isPublic(field.getModifiers()); + } + + public boolean isFinal() { + return Modifier.isFinal(field.getModifiers()); + } + + public T get(Object obj) { + try { + //noinspection unchecked + return (T) field.get(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void set(Object obj, T value) { + setField(obj, value, field); + } + + public Class getType() { + return this.field.getType(); + } + + public Field getField() { + return this.field; + } + +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Class.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Class.java new file mode 100644 index 0000000..4e1a85c --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Class.java @@ -0,0 +1,39 @@ +package com.pepedevs.corelib.utils.reflection.annotation; + +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; +import com.pepedevs.corelib.utils.version.Version; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Resolves the annotated {@link ClassWrapper} or {@link java.lang.Class} field to the first matching class name. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Class { + + /** + * Name of the class. Use {nms}.MyClass for NMS classes, or {obc}.MyClass for OBC classes. + * Use > or < as a name prefix in combination with {@link #versions()} to specify versions newer- or older-than. + * + * @return the class name + */ + String[] value(); + + /** + * Specific versions for the names. + * + * @return Array of versions for the class names + */ + Version[] versions() default {}; + + /** + * Whether to ignore any com.pepedevs.corelib.utils.reflection exceptions thrown. Defaults to true + * + * @return whether to ignore exceptions + */ + boolean ignoreExceptions() default true; +} \ No newline at end of file diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Field.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Field.java new file mode 100644 index 0000000..086d1b4 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Field.java @@ -0,0 +1,45 @@ +package com.pepedevs.corelib.utils.reflection.annotation; + +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.FieldWrapper; +import com.pepedevs.corelib.utils.version.Version; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Resolves the annotated {@link FieldWrapper} or {@link java.lang.reflect.Field} field to the first matching field name. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Field { + + /** + * Name of the class to load this field from + * + * @return name of the class + */ + String className(); + + /** + * Possible names of the field. Use > or < as a name prefix in combination with {@link #versions()} to specify versions newer- or older-than. + * + * @return names of the field + */ + String[] value(); + + /** + * Specific versions for the names. + * + * @return Array of versions for the class names + */ + Version[] versions() default {}; + + /** + * Whether to ignore any com.pepedevs.corelib.utils.reflection exceptions thrown. Defaults to true + * + * @return whether to ignore exceptions + */ + boolean ignoreExceptions() default true; +} \ No newline at end of file diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Method.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Method.java new file mode 100644 index 0000000..1c0e333 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/Method.java @@ -0,0 +1,45 @@ +package com.pepedevs.corelib.utils.reflection.annotation; + +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import com.pepedevs.corelib.utils.version.Version; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Resolves the annotated {@link MethodWrapper} or {@link java.lang.reflect.Method} field to the first matching method name. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Method { + + /** + * Name of the class to load this method from + * + * @return name of the class + */ + String className(); + + /** + * Possible names of the method. Use > or < as a name prefix in combination with {@link #versions()} to specify versions newer- or older-than. + * + * @return method names + */ + String[] value(); + + /** + * Specific versions for the names. + * + * @return Array of versions for the class names + */ + Version[] versions() default {}; + + /** + * Whether to ignore any com.pepedevs.corelib.utils.reflection exceptions thrown. Defaults to true + * + * @return whether to ignore exceptions + */ + boolean ignoreExceptions() default true; +} \ No newline at end of file diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/ReflectionAnnotations.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/ReflectionAnnotations.java new file mode 100644 index 0000000..ffd1e54 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/annotation/ReflectionAnnotations.java @@ -0,0 +1,199 @@ +package com.pepedevs.corelib.utils.reflection.annotation; + +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.resolver.ClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.FieldResolver; +import com.pepedevs.corelib.utils.reflection.resolver.MethodResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.FieldWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import com.pepedevs.corelib.utils.version.Version; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ReflectionAnnotations { + + public static final ReflectionAnnotations INSTANCE = new ReflectionAnnotations(); + + static final Pattern classRefPattern = Pattern.compile("@Class\\((.*)\\)"); + + private ReflectionAnnotations() { + } + + public void load(Object toLoad) { + if (toLoad == null) + throw new IllegalArgumentException("toLoad cannot be null"); + + ClassResolver classResolver = new ClassResolver(); + + for (java.lang.reflect.Field field : toLoad.getClass().getDeclaredFields()) { + Class classAnnotation = field.getAnnotation(Class.class); + Field fieldAnnotation = field.getAnnotation(Field.class); + Method methodAnnotation = field.getAnnotation(Method.class); + + if (classAnnotation == null && fieldAnnotation == null && methodAnnotation == null) { + continue; + } else { + field.setAccessible(true); + } + + if (classAnnotation != null) { + List nameList = this.parseAnnotationVersions(Class.class, classAnnotation); + if (nameList.isEmpty()) throw new IllegalArgumentException("@Class names cannot be empty"); + String[] names = nameList.toArray(new String[0]); + for (int i = 0; i < names.length; i++) { + names[i] = names[i] + .replace("{nms}", Version.SERVER_VERSION.getNmsPackage()) + .replace("{obc}", Version.SERVER_VERSION.getObcPackage()); + } + try { + if (ClassWrapper.class.isAssignableFrom(field.getType())) { + field.set(toLoad, classResolver.resolveWrapper(names)); + } else if (java.lang.Class.class.isAssignableFrom(field.getType())) { + field.set(toLoad, classResolver.resolve(names)); + } else { + this.throwInvalidFieldType(field, toLoad, "Class or ClassWrapper"); + return; + } + } catch (ReflectiveOperationException e) { + if (!classAnnotation.ignoreExceptions()) { + this.throwReflectionException("@Class", field, toLoad, e); + return; + } + } + } else if (fieldAnnotation != null) { + List nameList = this.parseAnnotationVersions(Field.class, fieldAnnotation); + if (nameList.isEmpty()) throw new IllegalArgumentException("@Field names cannot be empty"); + String[] names = nameList.toArray(new String[0]); + try { + FieldResolver fieldResolver = new FieldResolver(this.parseClass(Field.class, fieldAnnotation, toLoad)); + if (FieldAccessor.class.isAssignableFrom(field.getType())) { + field.set(toLoad, fieldResolver.resolveAccessor(names)); + } else if (FieldWrapper.class.isAssignableFrom(field.getType())) { + field.set(toLoad, fieldResolver.resolveWrapper(names)); + } else if (java.lang.reflect.Field.class.isAssignableFrom(field.getType())) { + field.set(toLoad, fieldResolver.resolve(names)); + } else { + this.throwInvalidFieldType(field, toLoad, "Field, FieldWrapper, or FieldAccessor"); + return; + } + } catch (ReflectiveOperationException e) { + if (!fieldAnnotation.ignoreExceptions()) { + this.throwReflectionException("@Field", field, toLoad, e); + return; + } + } + } else if (methodAnnotation != null) { + List nameList = this.parseAnnotationVersions(Method.class, methodAnnotation); + if (nameList.isEmpty()) throw new IllegalArgumentException("@Method names cannot be empty"); + String[] names = nameList.toArray(new String[0]); + + boolean isSignature = names[0].contains(" ");// Only signatures can contain spaces (e.g. "void aMethod()") + for (String s : names) { + if (s.contains(" ") != isSignature) { + throw new IllegalArgumentException("Inconsistent method names: Cannot have mixed signatures/names"); + } + } + + try { + MethodResolver methodResolver = new MethodResolver(this.parseClass(Method.class, methodAnnotation, toLoad)); + if (MethodWrapper.class.isAssignableFrom(field.getType())) { + if (isSignature) { + field.set(toLoad, methodResolver.resolveSignatureWrapper(names)); + } else { + field.set(toLoad, methodResolver.resolveWrapper(names)); + } + } else if (java.lang.reflect.Method.class.isAssignableFrom(field.getType())) { + if (isSignature) { + field.set(toLoad, methodResolver.resolveSignature(names)); + } else { + field.set(toLoad, methodResolver.resolve(names)); + } + } else { + this.throwInvalidFieldType(field, toLoad, "Method or MethodWrapper"); + return; + } + } catch (ReflectiveOperationException e) { + if (!methodAnnotation.ignoreExceptions()) { + this.throwReflectionException("@Method", field, toLoad, e); + return; + } + } + } + } + } + + /** + * Parses an annotation to the current server version. Removes all entries that don't match the version, but keeps the original order for matching names. + * + * @param clazz Class of the annotation + * @param annotation annotation + * @param annotation type + * @return a list of matching names + */ + List parseAnnotationVersions(java.lang.Class clazz, A annotation) { + List list = new ArrayList<>(); + + try { + String[] names = (String[]) clazz.getMethod("value").invoke(annotation); + Version[] versions = (Version[]) clazz.getMethod("versions").invoke(annotation); + + if (versions.length == 0) {// No versions specified -> directly use the names + Collections.addAll(list, names); + } else { + if (versions.length > names.length) { + throw new RuntimeException("versions array cannot have more elements than the names (" + clazz + ")"); + } + for (int i = 0; i < versions.length; i++) { + if (Version.SERVER_VERSION == versions[i]) {// Wohoo, perfect match! + list.add(names[i]); + } else { + if (names[i].startsWith(">") && Version.SERVER_VERSION.isNewer(versions[i])) {// Match if the current version is newer + list.add(names[i].substring(1)); + } else if (names[i].startsWith("<") && Version.SERVER_VERSION.isOlder(versions[i])) {// Match if the current version is older + list.add(names[i].substring(1)); + } + } + } + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + return list; + } + + String parseClass(java.lang.Class clazz, A annotation, Object toLoad) { + try { + String className = (String) clazz.getMethod("className").invoke(annotation); + Matcher matcher = classRefPattern.matcher(className); + while (matcher.find()) { + if (matcher.groupCount() != 1) continue; + String fieldName = matcher.group(1);// It's a reference to a previously loaded class + java.lang.reflect.Field field = toLoad.getClass().getField(fieldName); + if (ClassWrapper.class.isAssignableFrom(field.getType())) { + return ((ClassWrapper) field.get(toLoad)).getName(); + } else if (java.lang.Class.class.isAssignableFrom(field.getType())) { + return ((java.lang.Class) field.get(toLoad)).getName(); + } + } + return className; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + void throwInvalidFieldType(java.lang.reflect.Field field, Object toLoad, String expected) { + throw new IllegalArgumentException("Field " + field.getName() + " in " + toLoad.getClass() + " is not of type " + expected + ", it's " + field.getType()); + } + + void throwReflectionException(String annotation, java.lang.reflect.Field field, Object toLoad, ReflectiveOperationException exception) { + throw new RuntimeException("Failed to set " + annotation + " field " + field.getName() + " in " + toLoad.getClass(), exception); + } + +} \ No newline at end of file diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/ClassReflection.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/ClassReflection.java new file mode 100644 index 0000000..d0450e3 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/ClassReflection.java @@ -0,0 +1,270 @@ +package com.pepedevs.corelib.utils.reflection.general; + +import com.pepedevs.corelib.utils.version.Version; +import org.apache.commons.lang.StringUtils; +import org.reflections.Reflections; + +import java.io.File; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** Class for reflecting classes */ +public class ClassReflection { + + private static final Map> CACHED_CLASSES = new ConcurrentHashMap<>(); + + /** + * Gets the member sub class with the provided name that is hold by the provided {@code root} + * class. + * + *

+ * + * @param root Class that holds the sub class + * @param name Name of the sub class + * @param declared Whether or not the sub class is declared + * @return Member sub class + * @throws ClassNotFoundException if the sub class doesn't exist at the {@code root} class + */ + public static Class getSubClass(Class root, String name, boolean declared) + throws ClassNotFoundException { + for (Class clazz : declared ? root.getDeclaredClasses() : root.getClasses()) { + if (clazz.getSimpleName().equals(name)) { + return clazz; + } + } + throw new ClassNotFoundException("The sub class " + name + " doesn't exist!"); + } + + /** + * Gets the member sub class with the provided name that is hold by the provided {@code root} + * class. (No matter if the class is declared or not) + * + *

+ * + * @param root Class that holds the sub class + * @param name Name of the sub class + * @return Member sub class + * @throws ClassNotFoundException if the sub class doesn't exist at the {@code root} class + */ + public static Class getSubClass(Class root, String name) throws ClassNotFoundException { + try { + return getSubClass(root, name, true); + } catch (ClassNotFoundException ex) { + try { + return getSubClass(root, name, false); + } catch (ClassNotFoundException ignored) { + } + } + throw new ClassNotFoundException("The sub class " + name + " doesn't exist!"); + } + + /** + * Gets a class within the craftbukkit package ({@value Version#CRAFT_CLASSES_PACKAGE}) or within a + * sub-package of it. + * + *

+ * + * @param name Name of the class to get + * @param package_name Name of the sub-package or null if the class is not within a sub-package + * @return Class with the provided name + */ + public static Class getCraftClass(String name, String package_name) { + try { + String id = + "craft-" + + (StringUtils.isEmpty(package_name) + ? "" + : package_name.toLowerCase() + ".") + + name; + if (CACHED_CLASSES.containsKey(id)) return CACHED_CLASSES.get(id); + + Class clazz = + Class.forName( + Version.SERVER_VERSION.getObcPackage() + + "." + + (StringUtils.isEmpty(package_name) + ? "" + : package_name.toLowerCase() + ".") + + name); + CACHED_CLASSES.put(id, clazz); + return clazz; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Gets a class within the minecraft server ({@value Version#NMS_CLASSES_PACKAGE}) package. + * + *

+ * + * @param name Name of the class to get + * @param v17Package Package name/path after 'net.minecraft.' + * @return Class with the provided name + */ + public static Class getNmsClass(String name, String v17Package) { + try { + if (CACHED_CLASSES.containsKey(name)) return CACHED_CLASSES.get(name); + + Class clazz; + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_17_R1)) + clazz = + Class.forName( + "net.minecraft." + + (StringUtils.isEmpty(v17Package) + ? "" + : v17Package.toLowerCase() + ".") + + name); + else + clazz = + Class.forName( + Version.SERVER_VERSION.getNmsPackage() + + "." + + name); + + CACHED_CLASSES.put(name, clazz); + return clazz; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + public static boolean nmsClassExist(String name, String v17Package) { + try { + Class clazz; + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_17_R1)) + clazz = + Class.forName( + "net.minecraft." + + (StringUtils.isEmpty(v17Package) + ? "" + : v17Package.toLowerCase() + ".") + + name); + else + clazz = + Class.forName( + Version.SERVER_VERSION.getNmsPackage() + + "." + + name); + CACHED_CLASSES.put(name, clazz); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Scans all classes accessible from the context class loader which belong to the given package + * and subpackages. + * + *

+ * + * @param packageName Base package + * @return The classes + */ + public static Set> getClasses(String packageName) { + return getClasses(packageName, Object.class); + } + + public static Set> getClasses(String packageName, Class inherit) { + Reflections ref = new Reflections(packageName); + return ref.getSubTypesOf(inherit); + } + + /** + * Recursive method used to find all classes in a given directory and subdirs. + * + *

+ * + * @param directory Base directory + * @param packageName Package name for classes found inside the base directory + * @return The classes + * @throws ClassNotFoundException + */ + @Deprecated + public static List> findClasses(File directory, String packageName) + throws ClassNotFoundException { + List> classes = new ArrayList<>(); + if (!directory.exists()) { + return classes; + } + File[] files = directory.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + assert !file.getName().contains("."); + classes.addAll(findClasses(file, packageName + "." + file.getName())); + } else if (file.getName().endsWith(".class")) { + classes.add( + Class.forName( + packageName + + '.' + + file.getName() + .substring(0, file.getName().length() - 6))); + } + } + return classes; + } + + /** + * Scans the names of all the classes within a package contained by the provided {@code + * .jar}. + * + *

+ * + * @param jarFile File that represents the .jar + * @param packageName Name of the desired package that contains the classes to get, or null to + * get all the classes contained by the .jar + * @return Set with the name of the classes + */ + @Deprecated + public static Set getClassNames(File jarFile, String packageName) { + Set names = new HashSet<>(); + try { + JarFile file = new JarFile(jarFile); + for (Enumeration entry = file.entries(); entry.hasMoreElements(); ) { + JarEntry jarEntry = entry.nextElement(); + String name = jarEntry.getName().replace("/", "."); + if ((packageName == null + || packageName.trim().isEmpty() + || name.startsWith(packageName.trim())) + && name.endsWith(".class")) { + names.add(name.substring(0, name.lastIndexOf(".class"))); + } + } + file.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return names; + } + + /** + * Scans all the classes within a package contained by the provided {@code + * .jar}. + * + *

+ * + * @param jarFile File that represents the .jar + * @param packageName Name of the desired package that contains the classes to get, or null to + * get all the classes contained by the .jar + * @return Set with the scanned classes + */ + @Deprecated + public static Set> getClasses(File jarFile, String packageName) { + Set> classes = new HashSet<>(); + getClassNames(jarFile, packageName) + .forEach( + class_name -> { + try { + classes.add(Class.forName(class_name)); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + }); + return classes; + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/ConstructorReflection.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/ConstructorReflection.java new file mode 100644 index 0000000..82f3aa8 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/ConstructorReflection.java @@ -0,0 +1,467 @@ +package com.pepedevs.corelib.utils.reflection.general; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; + +/** Class for reflecting constructor */ +public class ConstructorReflection { + + /** + * Returns a {@code Constructor} object that reflects the specified public constructor of the + * class represented by the provided {@code Class} object. The {@code parameter_types} parameter + * is an array of {@code Class} objects that identify the constructor's formal parameter types, + * in declared order. + * + *

+ * + * @param clazz Class that holds the constructor + * @param declared Whether or not the constructor is declared + * @param parameter_types Parameter array + * @return {@code Constructor} object that matches the specified {@code parameter_types} + * @throws NoSuchMethodException if a matching constructor is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * constructor + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ */ + public static Constructor get(Class clazz, boolean declared, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + return declared + ? clazz.getDeclaredConstructor(parameter_types) + : clazz.getConstructor(parameter_types); + } + + /** + * Returns a {@code Constructor} object (No matter if declared or not) that + * reflects the specified public constructor of the class represented by the provided {@code + * Class} object. The {@code parameter_types} parameter is an array of {@code Class} objects + * that identify the constructor's formal parameter types, in declared order. + * + *

+ * + * @param clazz Class that holds the constructor + * @param parameter_types Parameter array + * @return {@code Constructor} object that matches the specified {@code parameter_types} + * @throws NoSuchMethodException if a matching constructor is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * constructor + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ * + * @see #get(Class, boolean, Class...) + */ + public static Constructor get(Class clazz, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + try { + return get(clazz, false, parameter_types); + } catch (NoSuchMethodException ex) { + return get(clazz, true, parameter_types); + } + } + + /** + * Returns a {@code Constructor} object that reflects the specified public constructor of the + * class represented by the provided {@code Class} object. The {@code parameter_types} parameter + * is an array of {@code Class} objects that identify the constructor's formal parameter types, + * in declared order. + * + *

+ * + * @param clazz Class that holds the constructor + * @param declared Whether or not the constructor is declared + * @param parameter_types Parameter array + * @return {@code Constructor} object that matches the specified {@code parameter_types} + * @throws NoSuchMethodException if a matching constructor is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * constructor + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ */ + public static Constructor getAccessible( + Class clazz, boolean declared, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + final Constructor constructor = get(clazz, declared, parameter_types); + constructor.setAccessible(true); + return constructor; + } + + /** + * Returns a {@code Constructor} object (No matter if declared or not) that + * reflects the specified public constructor of the class represented by the provided {@code + * Class} object. The {@code parameter_types} parameter is an array of {@code Class} objects + * that identify the constructor's formal parameter types, in declared order. + * + *

+ * + * @param clazz Class that holds the constructor + * @param parameter_types Parameter array + * @return {@code Constructor} object that matches the specified {@code parameter_types} + * @throws NoSuchMethodException if a matching constructor is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * constructor + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ */ + public static Constructor getAccessible(Class clazz, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + try { + return getAccessible(clazz, false, parameter_types); + } catch (NoSuchMethodException ex) { + return getAccessible(clazz, true, parameter_types); + } + } + + /** + * Uses the matching {@code Constructor} object to create and initialize a new instance of the + * constructor's declaring class, with the specified initialization parameters. Individual + * parameters are automatically unwrapped to match primitive formal parameters, and both + * primitive and reference parameters are subject to method invocation conversions as necessary. + *
+ * If the number of formal parameters required by the underlying constructor is 0, the supplied + * {@code arguments} array may be of length 0 or null.
+ * If the constructor's declaring class is an inner class in a non-static context, the first + * argument to the constructor needs to be the enclosing instance; see section 15.9.3 of + * The Java™ Language Specification.
+ * If the required access and argument checks succeed and the instantiation will proceed, the + * constructor's declaring class is initialized if it has not already been initialized.
+ * If the constructor completes normally, returns the newly created and initialized instance. + * + * @param clazz Class that holds the constructor + * @param declared Whether or not the constructor is declared + * @param parameter_types Parameters of the constructor, used for matching it + * @param arguments Array of objects to be passed as arguments to the constructor call; values + * of primitive types are wrapped in a wrapper object of the appropriate type (e.g. a {@code + * float} in a {@link Float Float}) + * @return Object created by calling the matching constructor + * @throws NoSuchMethodException if a matching constructor is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *
    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * constructor + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ * + * @throws InstantiationException if the class that declares the underlying constructor + * represents an abstract class. + * @throws IllegalAccessException if the {@code Constructor} object is enforcing Java language + * access control and the underlying constructor is inaccessible. + * @throws IllegalArgumentException if the number of actual and formal parameters differ; if an + * unwrapping conversion for primitive arguments fails; or if, after possible unwrapping, a + * parameter value cannot be converted to the corresponding formal parameter type by a + * method invocation conversion; if this constructor pertains to an enum type. + * @throws InvocationTargetException if the underlying constructor throws an exception. + */ + @SuppressWarnings("unchecked") + public static T newInstance( + Class clazz, boolean declared, Class[] parameter_types, Object... arguments) + throws NoSuchMethodException, SecurityException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return (T) getAccessible(clazz, declared, parameter_types).newInstance(arguments); + } + + // /** + // * Uses the matching {@code Constructor} object to create and initialize a new + // * instance of the constructor's declaring class, with the specified + // * initialization arguments. Individual parameters are automatically unwrapped + // * to match primitive formal parameters, and both primitive and reference + // * parameters are subject to method invocation conversions as necessary. + // * + // *
+ // * If the number of formal parameters required by the underlying constructor is + // * 0, the supplied {@code arguments} array may be of length 0 or null. + // * + // *
+ // * If the constructor's declaring class is an inner class in a non-static + // * context, the first argument to the constructor needs to be the enclosing + // * instance; see section 15.9.3 of The Java™ Language + // * Specification. + // * + // *
+ // * If the required access and argument checks succeed and the instantiation will + // * proceed, the constructor's declaring class is initialized if it has not + // * already been initialized. + // * + // *
+ // * If the constructor completes normally, returns the newly created and + // * initialized instance. + // * + // * @param + // * @param clazz Class that holds the constructor. + // * @param declared Whether or not the constructor is declared. + // * @param arguments Array of objects to be passed as arguments to the + // * constructor call; values of primitive types are wrapped in a + // * wrapper object of the appropriate type (e.g. a {@code float} + // * in a {@link java.lang.Float Float}) + // * @return Object created by calling the matching constructor. + // * @throws NoSuchMethodException if a matching constructor is not found. + // * @throws SecurityException if a security manager, s, is present + // * and any of the following conditions is met: + // * + // *
    + // * + // *
  • the caller's class loader is not the + // * same as the class loader of this class and + // * invocation of + // * {@link SecurityManager#checkPermission + // * s.checkPermission} method with + // * {@code RuntimePermission("accessDeclaredMembers")} + // * denies access to the declared constructor + // * + // *
  • the caller's class loader is not the + // * same as or an ancestor of the class loader + // * for the current class and invocation of + // * {@link SecurityManager#checkPackageAccess + // * s.checkPackageAccess()} denies access to + // * the package of this class + // * + // *
+ // * @throws InstantiationException if the class that declares the underlying + // * constructor represents an abstract class. + // * @throws IllegalAccessException if the {@code Constructor} object is + // * enforcing Java language access control and + // * the underlying constructor is inaccessible. + // * @throws IllegalArgumentException if the number of actual and formal + // * parameters differ; if an unwrapping + // * conversion for primitive arguments fails; + // * or if, after possible unwrapping, a + // * parameter value cannot be converted to the + // * corresponding formal parameter type by a + // * method invocation conversion; if this + // * constructor pertains to an enum type. + // * @throws InvocationTargetException if the underlying constructor throws an + // * exception. + // */ + // public static T newInstance ( Class < ? > clazz , boolean declared , Object... arguments + // ) + // throws NoSuchMethodException, SecurityException, InstantiationException, + // IllegalAccessException, + // IllegalArgumentException, InvocationTargetException { + // return newInstance ( clazz , declared , getTypes ( arguments ), arguments ); + // } + + /** + * Uses the matching {@code Constructor} object (No matter if declared or not) + * to create and initialize a new instance of the constructor's declaring class, with the + * specified initialization parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary.
+ * If the number of formal parameters required by the underlying constructor is 0, the supplied + * {@code arguments} array may be of length 0 or null.
+ * If the constructor's declaring class is an inner class in a non-static context, the first + * argument to the constructor needs to be the enclosing instance; see section 15.9.3 of + * The Java™ Language Specification.
+ * If the required access and argument checks succeed and the instantiation will proceed, the + * constructor's declaring class is initialized if it has not already been initialized.
+ * If the constructor completes normally, returns the newly created and initialized instance. + * + * @param clazz Class that holds the constructor + * @param parameter_types Parameters of the constructor, used for matching it + * @param arguments Array of objects to be passed as arguments to the constructor call; values + * of primitive types are wrapped in a wrapper object of the appropriate type (e.g. a {@code + * float} in a {@link Float Float}) + * @return Object created by calling the matching constructor + * @throws NoSuchMethodException if a matching constructor is not found. + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *
    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * constructor + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ * + * @throws InstantiationException if the class that declares the underlying constructor + * represents an abstract class. + * @throws IllegalAccessException if the {@code Constructor} object is enforcing Java language + * access control and the underlying constructor is inaccessible. + * @throws IllegalArgumentException if the number of actual and formal parameters differ; if an + * unwrapping conversion for primitive arguments fails; or if, after possible unwrapping, a + * parameter value cannot be converted to the corresponding formal parameter type by a + * method invocation conversion; if this constructor pertains to an enum type. + * @throws InvocationTargetException if the underlying constructor throws an exception. + */ + public static T newInstance(Class clazz, Class[] parameter_types, Object... arguments) + throws NoSuchMethodException, SecurityException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { + try { + return newInstance(clazz, false, parameter_types, arguments); + } catch (NoSuchMethodException ex) { + return newInstance(clazz, true, parameter_types, arguments); + } + } + + // /** + // * Uses the matching {@code Constructor} object (No matter if declared + // * or not) to create and initialize a new instance of the constructor's + // * declaring class, with the specified initialization arguments. Individual + // * parameters are automatically unwrapped to match primitive formal parameters, + // * and both primitive and reference parameters are subject to method invocation + // * conversions as necessary. + // *

+ // * Note that {@code null} arguments are unsupported. + // *

+ // * If the number of formal parameters required by the underlying constructor is + // * 0, the supplied {@code arguments} array may be of length 0 or null. + // * + // *

+ // * If the constructor's declaring class is an inner class in a non-static + // * context, the first argument to the constructor needs to be the enclosing + // * instance; see section 15.9.3 of The Java™ Language + // * Specification. + // * + // *

+ // * If the required access and argument checks succeed and the instantiation will + // * proceed, the constructor's declaring class is initialized if it has not + // * already been initialized. + // * + // *

+ // * If the constructor completes normally, returns the newly created and + // * initialized instance. + // * + // * @param + // * @param clazz the class that holds the constructor. + // * @param arguments array of objects to be passed as arguments to the + // * constructor call; values of primitive types are wrapped in a + // * wrapper object of the appropriate type (e.g. a {@code float} + // * in a {@link java.lang.Float Float}) + // * @return a new object created by calling the matching constructor. + // * @throws NoSuchMethodException if a matching constructor is not found. + // * @throws SecurityException SecurityException If a security manager, + // * s, is present and any of the + // * following conditions is met: + // * + // *

    + // * + // *
  • the caller's class loader is not the + // * same as the class loader of this class and + // * invocation of + // * {@link SecurityManager#checkPermission + // * s.checkPermission} method with + // * {@code RuntimePermission("accessDeclaredMembers")} + // * denies access to the declared constructor + // * + // *
  • the caller's class loader is not the + // * same as or an ancestor of the class loader + // * for the current class and invocation of + // * {@link SecurityManager#checkPackageAccess + // * s.checkPackageAccess()} denies access to + // * the package of this class + // * + // *
+ // * @throws InstantiationException if the class that declares the underlying + // * constructor represents an abstract class. + // * @throws IllegalAccessException if the {@code Constructor} object is + // * enforcing Java language access control and + // * the underlying constructor is inaccessible. + // * @throws IllegalArgumentException if the number of actual and formal + // * parameters differ; if an unwrapping + // * conversion for primitive arguments fails; + // * or if, after possible unwrapping, a + // * parameter value cannot be converted to the + // * corresponding formal parameter type by a + // * method invocation conversion; if this + // * constructor pertains to an enum type. + // * @throws InvocationTargetException if the underlying constructor throws an + // * exception. + // */ + // public static T newInstance ( Class < ? > clazz , Object... arguments ) + // throws NoSuchMethodException, SecurityException, InstantiationException, + // IllegalAccessException, + // IllegalArgumentException, InvocationTargetException { + // try { + // return newInstance ( clazz , false , getTypes ( arguments ) , arguments ); + // } catch ( NoSuchMethodException ex ) { + // return newInstance ( clazz , true , getTypes ( arguments ) , arguments ); + // } + // } + + /** + * Sets the provided constructor as accessible/inaccessible. + * + * @param constructor Constructor to set + * @param accessible Whether or not the method is accessible + * @return Object, for chaining + * @throws SecurityException if the request is denied + */ + public static Constructor setAccessible(Constructor constructor, boolean accessible) + throws SecurityException { + constructor.setAccessible(accessible); + if (accessible) { + try { + Field modifiersField = Constructor.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(constructor, constructor.getModifiers() & ~Modifier.FINAL); + modifiersField.setAccessible(false); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + return constructor; + } + + /** + * Sets the provided constructor as accessible. + * + * @param constructor Constructor to set + * @return Object, for chaining + * @throws SecurityException if the request is denied + * @see #setAccessible(Constructor, boolean) + */ + public static Constructor setAccessible(Constructor constructor) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, + IllegalAccessException { + return setAccessible(constructor, true); + } + + // /** + // * Gets an array containing the classes of the provided arguments. + // *

+ // * @param arguments the objects to get. + // * @return an array containing the types. + // */ + // private static Class < ? > [] getTypes ( Object... arguments ) { + // final Class < ? > [] types = new Class < ? > [ arguments.length ]; + // for ( int i = 0 ; i < types.length ; i ++ ) { + // types [ i ] = arguments [ i ].getClass(); + // } + // return types; + // } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/EnumReflection.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/EnumReflection.java new file mode 100644 index 0000000..7ae5ca0 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/EnumReflection.java @@ -0,0 +1,43 @@ +package com.pepedevs.corelib.utils.reflection.general; + +/** Class to reflect Enumerations */ +public class EnumReflection { + + /** + * Returns the enum constant of the specified enum type with the specified name. The name must + * match exactly an identifier used to declare an enum constant in this type. (Extraneous + * whitespace characters are not permitted.) + * + *

Note that for a particular enum type {@code T}, the implicitly declared {@code public + * static T valueOf(String)} method on that enum may be used instead of this method to map from + * a name to the corresponding enum constant. All the constants of an enum type can be obtained + * by calling the implicit {@code public static T[] values()} method of that type. + * + *

+ * + * @param The enum type whose constant is to be returned + * @param clazz {@code Class} object of the enum type from which to return a constant + * @param name Name of the constant to return + * @return Enum constant of the specified enum type with the specified name, or null if doesn't + * exist + */ + public static > T getEnumConstant(Class clazz, String name) { + try { + return Enum.valueOf(clazz, name); + } catch (IllegalArgumentException ex) { + return null; + } + } + + public static > T getEnumConstant(Class enumClass, String enumName, int fallbackOrdinal) { + try { + return Enum.valueOf(enumClass, enumName); + } catch (IllegalArgumentException e) { + T[] constants = enumClass.getEnumConstants(); + if (constants.length > fallbackOrdinal) { + return constants[fallbackOrdinal]; + } + throw e; + } + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/FieldReflection.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/FieldReflection.java new file mode 100644 index 0000000..be3fee1 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/FieldReflection.java @@ -0,0 +1,379 @@ +package com.pepedevs.corelib.utils.reflection.general; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; + +/** Class for reflecting fields */ +public class FieldReflection { + + /** + * Gets the field with the provided name from the provided class. + * + *

+ * + * @param clazz Class that holds the field + * @param name Name of the field + * @param declared Whether the desired field is declared or not + * @return {@code Field} object of the provided class specified by {@code name} + * @throws NoSuchFieldException if a field with the specified name is not found. + * @throws SecurityException If a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ */ + public static Field get(Class clazz, String name, boolean declared) + throws SecurityException, NoSuchFieldException { + return declared ? clazz.getDeclaredField(name) : clazz.getField(name); + } + + /** + * Gets the field with the provided name from the provided class. (No matter if declared + * or not). + * + *

+ * + * @param clazz Class that holds the field + * @param name Name of the field + * @return {@code Field} object of the provided class specified by {@code name} + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ * + * @see #get(Class, String, boolean) + */ + public static Field get(Class clazz, String name) + throws SecurityException, NoSuchFieldException { + try { + return get(clazz, name, false); + } catch (NoSuchFieldException ex) { + return get(clazz, name, true); + } + } + + /** + * Gets the field with the provided name from the provided class, and sets it accessible. + * + *

+ * + * @param clazz Class that holds the field + * @param name Name of the field + * @param declared Whether the desired field is declared or not + * @return {@code Field} object of the provided class specified by {@code name} + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
  • the field cannot be made accessible. + *
+ */ + public static Field getAccessible(Class clazz, String name, boolean declared) + throws SecurityException, NoSuchFieldException { + final Field field = get(clazz, name, declared); + field.setAccessible(true); + return field; + } + + /** + * Gets the field with the provided name from the provided class, and sets it accessible. + * (No matter declared or not). + * + *

+ * + * @param clazz Class that holds the field + * @param name Name of the field + * @return {@code Field} object of the provided class specified by {@code name} + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
  • the field cannot be made accessible. + *
+ * + * @see #getAccessible(Class, String, boolean) + */ + public static Field getAccessible(Class clazz, String name) + throws SecurityException, NoSuchFieldException { + final Field field = get(clazz, name); + field.setAccessible(true); + return field; + } + + /** + * Returns the value of the field represented by the field with the provided name, on the + * specified object. The value is automatically wrapped in an object if it has a primitive type. + * + *

+ * + * @param Type of the object to return + * @param object Object from which the represented field's value is to be extracted + * @param name Name of the field to get + * @param declared Whether the field is declared or not + * @return Value of the field + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws IllegalArgumentException if the specified object is not an instance of the class or + * interface declaring the underlying field (or a subclass or implementor thereof). + * @throws IllegalAccessException if the field is completely inaccessible. + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
  • the field cannot be made accessible. + *
+ * + * @see Field#get(Object) + */ + @SuppressWarnings("unchecked") + public static T getValue(Object object, String name, boolean declared) + throws SecurityException, NoSuchFieldException, IllegalArgumentException, + IllegalAccessException { + final Field field = getAccessible(object.getClass(), name, declared); + final boolean b0 = field.isAccessible(); + + field.setAccessible(true); + try { + return (T) field.get(object); + } catch (Throwable ex) { + throw ex; + } finally { + field.setAccessible(b0); + } + } + + /** + * Returns the value of the field represented by the field with the provided name (No + * matter if declared or not), on the specified object. The value is automatically + * wrapped in an object if it has a primitive type. + * + *

+ * + * @param Type of the object to return + * @param object Object from which the represented field's value is to be extracted + * @param name Name of the field to get + * @return Value of the field + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws IllegalArgumentException if the specified object is not an instance of the class or + * interface declaring the underlying field (or a subclass or implementor thereof). + * @throws IllegalAccessException if the field is completely inaccessible. + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
  • the field cannot be made accessible. + *
+ * + * @see #getValue(Object, String, boolean) + */ + public static T getValue(Object object, String name) + throws SecurityException, NoSuchFieldException, IllegalArgumentException, + IllegalAccessException { + try { + return getValue(object, name, false); + } catch (NoSuchFieldException ex) { + return getValue(object, name, true); + } + } + + /** + * Sets the field represented by the field with the provided name, on the specified object + * argument to the specified new value. The new value is automatically unwrapped if the + * underlying field has a primitive type. + * + *

+ * + * @param object Object whose field should be modified + * @param name Name of the field to set + * @param declared Whether the field is declared or not + * @param value New value for the field of {@code object} being modified + * @return Same Object, for chaining + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws IllegalAccessException if the field is completely inaccessible + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
  • the field cannot be made accessible. + *
+ * + * @see Field#set(Object, Object) + */ + public static Object setValue(Object object, String name, boolean declared, Object value) + throws SecurityException, NoSuchFieldException, IllegalAccessException { + final Field field = getAccessible(object.getClass(), name, declared); + final boolean b0 = field.isAccessible(); + + field.setAccessible(true); + try { + field.set(object, value); + } catch (Throwable ex) { + throw ex; + } finally { + field.setAccessible(b0); + } + return object; + } + + /** + * Sets the field represented by the field with the provided name (No matter if declared + * or not), on the specified object argument to the specified new value. The new value + * is automatically unwrapped if the underlying field has a primitive type. + * + *

+ * + * @param object Object whose field should be modified + * @param name Name of the field to set + * @param value New value for the field of {@code object} being modified + * @return Same Object, for chaining + * @throws NoSuchFieldException if a field with the specified name is not found + * @throws IllegalAccessException if the field is completely inaccessible + * @throws SecurityException if a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * field + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
  • the field cannot be made accessible. + *
+ * + * @see Field#set(Object, Object) + */ + public static Object setValue(Object object, String name, Object value) + throws SecurityException, NoSuchFieldException, IllegalAccessException { + try { + return setValue(object, name, false, value); + } catch (NoSuchFieldException ex) { + return setValue(object, name, true, value); + } + } + + /** + * Sets the provided field as accessible/inaccessible. + * + *

Note that the if the field is final, it will set as "not final", so + * that the value can be changed. + * + *

+ * + * @param field Field to set + * @param accessible Whether or not the field is accessible + * @return Same Object, for chaining + * @throws SecurityException if the request is denied + */ + public static Field setAccessible(Field field, boolean accessible) throws SecurityException { + field.setAccessible(accessible); + if (accessible) { + try { + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + // modifiersField.setAccessible ( false ); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + + return field; + } + + /** + * Sets the provided field as accessible. + * + *

Note that the if the field is final, it will set as "not final", so + * that the value can be changed. + * + *

+ * + * @param field Field to set + * @return Same Object, for chaining + * @throws SecurityException if the request is denied + * @see #setAccessible(Field, boolean) + */ + public static Field setAccessible(Field field) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, + IllegalAccessException { + return setAccessible(field, true); + } + + /** + * Returns the classes of the types of a parameterized field, like {@link Collection} or {@link + * Map}. + * + *

Examples: - {@code getParameterizedTypesClasses(Collection)} (of a collection + * field in this case) will return: [java.lang.Integer] - {@code + * getParameterizedTypesClasses(Map)} (of a map field in this case) will + * return: [java.lang.Integer, java.lang.String] + * + *

+ * + * @param field Parameterized field + * @return Classes of the types of a parameterized field + */ + public static Class[] getParameterizedTypesClasses(Field field) { + if (!(field.getGenericType() instanceof ParameterizedType)) { + throw new IllegalArgumentException("The field doesn't represent a parameterized type!"); + } + + ParameterizedType parameterized_type = (ParameterizedType) field.getGenericType(); + Type[] types_arguments = parameterized_type.getActualTypeArguments(); + Class[] classes = new Class[types_arguments.length]; + for (int i = 0; i < classes.length; i++) { + classes[i] = (Class) types_arguments[i]; + } + return classes; + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/MethodReflection.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/MethodReflection.java new file mode 100644 index 0000000..7b5a99f --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/general/MethodReflection.java @@ -0,0 +1,387 @@ +package com.pepedevs.corelib.utils.reflection.general; + +import com.pepedevs.corelib.utils.reflection.DataType; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** Class for reflecting methods */ +public class MethodReflection { + + /** + * Gets the method with the provided name from the provided class. + * + *

+ * + * @param clazz the class that holds the method + * @param name the name of the method + * @param declared whether the desired method is declared or not + * @param parameter_types parameterTypes the list of parameters + * @return the {@code Method} object that matches the specified {@code name} and {@code + * parameterTypes} + * @throws NoSuchMethodException if a matching method is not found or if the name is + * "<init>"or "<clinit>". + * @throws SecurityException If a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * method + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ */ + public static Method get( + Class clazz, String name, boolean declared, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + return declared + ? clazz.getDeclaredMethod(name, parameter_types) + : clazz.getMethod(name, parameter_types); + } + + /** + * Gets the method with the provided name (No matter if declared or not) from + * the provided class. + * + *

+ * + * @param clazz the class that holds the method + * @param name the name of the method + * @param parameter_types parameterTypes the list of parameters + * @return the {@code Method} object that matches the specified {@code name} and {@code + * parameterTypes} + * @throws NoSuchMethodException if a matching method is not found or if the name is + * "<init>"or "<clinit>". + * @throws SecurityException If a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * method + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ * + * @see #get(Class, String, boolean, Class...) + */ + public static Method get(Class clazz, String name, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + try { + return get(clazz, name, false, parameter_types); + } catch (NoSuchMethodException ex) { + return get(clazz, name, true, parameter_types); + } + } + + /** + * Gets the method with the provided name from the provided class, and sets it accessible. + * + *

+ * + * @param clazz the class that holds the method + * @param name the name of the method + * @param declared whether the desired method is declared or not + * @param parameter_types parameterTypes the list of parameters + * @return the {@code Method} object that matches the specified {@code name} and {@code + * parameterTypes} + * @throws NoSuchMethodException if a matching method is not found or if the name is + * "<init>"or "<clinit>". + * @throws SecurityException If a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * method + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ */ + public static Method getAccessible( + Class clazz, String name, boolean declared, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + final Method method = get(clazz, name, declared, parameter_types); + method.setAccessible(true); + return method; + } + + /** + * Gets the method with the provided name (No matter if declared or not) from + * the provided class, and sets it accessible. + * + *

+ * + * @param clazz the class that holds the method + * @param name the name of the method + * @param parameter_types parameterTypes the list of parameters + * @return the {@code Method} object that matches the specified {@code name} and {@code + * parameterTypes} + * @throws NoSuchMethodException if a matching method is not found or if the name is + * "<init>"or "<clinit>". + * @throws SecurityException If a security manager, s, is present and any of the + * following conditions is met: + *

    + *
  • the caller's class loader is not the same as the class loader of this class and + * invocation of {@link SecurityManager#checkPermission s.checkPermission} method with + * {@code RuntimePermission("accessDeclaredMembers")} denies access to the declared + * method + *
  • the caller's class loader is not the same as or an ancestor of the class loader for + * the current class and invocation of {@link SecurityManager#checkPackageAccess + * s.checkPackageAccess()} denies access to the package of this class + *
+ * + * @see #getAccessible(Class, String, boolean, Class...) + */ + public static Method getAccessible(Class clazz, String name, Class... parameter_types) + throws NoSuchMethodException, SecurityException { + final Method method = get(clazz, name, parameter_types); + method.setAccessible(true); + return method; + } + + /** + * Invokes the underlying method represented by the provided {@code Method}, on the specified + * object with the specified parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary. + * + *

+ * + * @param method the method to invoke + * @param object the object the underlying method is invoked from + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by this object on {@code object} + * with parameters {@code arguments} + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language + * access control and the underlying method is inaccessible. + * @throws IllegalArgumentException if the method is an instance method and the specified object + * argument is not an instance of the class or interface declaring the underlying method (or + * of a subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after possible + * unwrapping, a parameter value cannot be converted to the corresponding formal parameter + * type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method throws an exception. + */ + public static Object invoke(Method method, Object object, Object... arguments) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return method.invoke(object, arguments); + } + + /** + * Invokes the underlying method represented by the provided {@code Method}, on the specified + * object with the specified parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary. + * + *

Note that the method is set as accessible before invoking. + * + *

+ * + * @param method the method to invoke + * @param object the object the underlying method is invoked from + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by this object on {@code object} + * with parameters {@code arguments} + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language + * access control and the underlying method is inaccessible. + * @throws IllegalArgumentException if the method is an instance method and the specified object + * argument is not an instance of the class or interface declaring the underlying method (or + * of a subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after possible + * unwrapping, a parameter value cannot be converted to the corresponding formal parameter + * type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method throws an exception. + */ + public static Object invokeAccessible(Method method, Object object, Object... arguments) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + final boolean b0 = method.isAccessible(); + try { + return method.invoke(object, arguments); + } catch (Throwable ex) { + throw ex; + } finally { + method.setAccessible(b0); + } + } + + /** + * Invokes the underlying method represented by the provided {@code name}, on the specified + * object with the specified parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary. + * + *

+ * + * @param name the name of the method to invoke + * @param object the object the underlying method is invoked from + * @param parameter_types parameterTypes the list of parameters + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by this object on {@code object} + * with parameters {@code arguments} + * @throws SecurityException + * @throws NoSuchMethodException + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language + * access control and the underlying method is inaccessible. + * @throws IllegalArgumentException if the method is an instance method and the specified object + * argument is not an instance of the class or interface declaring the underlying method (or + * of a subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after possible + * unwrapping, a parameter value cannot be converted to the corresponding formal parameter + * type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method throws an exception. + */ + public static Object invoke( + Object object, String name, Class[] parameter_types, Object... arguments) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, + NoSuchMethodException, SecurityException { + return invoke(get(object.getClass(), name, parameter_types), object, arguments); + } + + /** + * Invokes the underlying method represented by the provided {@code name}, on the specified + * object with the specified parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary. + * + *

Note that the method is set as accessible before invoking. + * + *

+ * + * @param name the name of the method to invoke + * @param object the object the underlying method is invoked from + * @param parameter_types parameterTypes the list of parameters + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by this object on {@code object} + * with parameters {@code arguments} + * @throws SecurityException + * @throws NoSuchMethodException + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language + * access control and the underlying method is inaccessible. + * @throws IllegalArgumentException if the method is an instance method and the specified object + * argument is not an instance of the class or interface declaring the underlying method (or + * of a subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after possible + * unwrapping, a parameter value cannot be converted to the corresponding formal parameter + * type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method throws an exception. + */ + public static Object invokeAccessible( + Object object, String name, Class[] parameter_types, Object... arguments) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, + NoSuchMethodException, SecurityException { + return invokeAccessible(get(object.getClass(), name, parameter_types), object, arguments); + } + + /** + * Invokes the underlying method represented by the provided {@code name}, on the specified + * object with the specified parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary. + * + *

+ * + * @param name the name of the method to invoke + * @param object the object the underlying method is invoked from + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by this object on {@code object} + * with parameters {@code arguments} + * @throws SecurityException + * @throws NoSuchMethodException + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language + * access control and the underlying method is inaccessible. + * @throws IllegalArgumentException if the method is an instance method and the specified object + * argument is not an instance of the class or interface declaring the underlying method (or + * of a subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after possible + * unwrapping, a parameter value cannot be converted to the corresponding formal parameter + * type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method throws an exception. + */ + public static Object invoke(Object object, String name, Object... arguments) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, + NoSuchMethodException, SecurityException { + return invoke( + get(object.getClass(), name, DataType.getPrimitive(arguments)), object, arguments); + } + + /** + * Invokes the underlying method represented by the provided {@code name}, on the specified + * object with the specified parameters. Individual parameters are automatically unwrapped to + * match primitive formal parameters, and both primitive and reference parameters are subject to + * method invocation conversions as necessary. + * + *

Note that the method is set as accessible before invoking. + * + *

+ * + * @param name the name of the method to invoke. + * @param object the object the underlying method is invoked from + * @param arguments the arguments used for the method call + * @return the result of dispatching the method represented by this object on {@code object} + * with parameters {@code arguments} + * @throws SecurityException + * @throws NoSuchMethodException + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language + * access control and the underlying method is inaccessible. + * @throws IllegalArgumentException if the method is an instance method and the specified object + * argument is not an instance of the class or interface declaring the underlying method (or + * of a subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after possible + * unwrapping, a parameter value cannot be converted to the corresponding formal parameter + * type by a method invocation conversion. + * @throws InvocationTargetException if the underlying method throws an exception. + */ + public static Object invokeAccessible(Object object, String name, Object... arguments) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, + NoSuchMethodException, SecurityException { + return invokeAccessible( + get(object.getClass(), name, DataType.getPrimitive(arguments)), object, arguments); + } + + /** + * Sets the provided method as accessible/inaccessible. + * + *

+ * + * @param method the method to set + * @param accessible whether or not the method is accessible + * @return the same Object, for chaining + * @throws SecurityException if the request is denied + */ + public static Method setAccessible(Method method, boolean accessible) throws SecurityException { + method.setAccessible(accessible); + if (accessible) { + try { + Field modifiersField = Method.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(method, method.getModifiers() & ~Modifier.FINAL); + modifiersField.setAccessible(false); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + return method; + } + + /** + * Sets the provided method as accessible. + * + *

+ * + * @param method the method to set + * @return the same Object, for chaining + * @throws SecurityException if the request is denied + * @see #setAccessible(Method, boolean) + */ + public static Method setAccessible(Method method) + throws SecurityException, IllegalArgumentException { + return setAccessible(method, true); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ClassResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ClassResolver.java new file mode 100644 index 0000000..409a2ab --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ClassResolver.java @@ -0,0 +1,39 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; + +/** Default {@link ClassResolver} */ +public class ClassResolver extends ResolverAbstract { + + public ClassWrapper resolveWrapper(String... names) { + return new ClassWrapper<>(this.resolveSilent(names)); + } + + public Class resolveSilent(String... names) { + try { + return this.resolve(names); + } catch (ClassNotFoundException ignored) { + } + return null; + } + + public Class resolve(String... names) throws ClassNotFoundException { + ResolverQuery.Builder builder = ResolverQuery.builder(); + for (String name : names) builder.with(name); + try { + return super.resolve(builder.build()); + } catch (ReflectiveOperationException e) { + throw (ClassNotFoundException) e; + } + } + + @Override + protected Class resolveObject(ResolverQuery query) throws ReflectiveOperationException { + return Class.forName(query.getName()); + } + + @Override + protected ClassNotFoundException notFoundException(String joinedNames) { + return new ClassNotFoundException("Could not resolve class for " + joinedNames); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ConstructorResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ConstructorResolver.java new file mode 100644 index 0000000..130898c --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ConstructorResolver.java @@ -0,0 +1,106 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import com.pepedevs.corelib.utils.reflection.general.ConstructorReflection; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ConstructorWrapper; + +import java.lang.reflect.Constructor; + +/** Resolver for constructors */ +public class ConstructorResolver extends MemberResolver { + + public ConstructorResolver(Class clazz) { + super(clazz); + } + + public ConstructorResolver(String className) throws ClassNotFoundException { + super(className); + } + + @Override + public Constructor resolveIndex(int index) + throws IndexOutOfBoundsException, ReflectiveOperationException { + return ConstructorReflection.setAccessible(this.clazz.getDeclaredConstructors()[index]); + } + + @Override + public Constructor resolveIndexSilent(int index) { + try { + return this.resolveIndex(index); + } catch (IndexOutOfBoundsException | ReflectiveOperationException ignored) { + } + return null; + } + + @Override + public ConstructorWrapper resolveIndexWrapper(int index) { + return new ConstructorWrapper<>(this.resolveIndexSilent(index)); + } + + public ConstructorWrapper resolveWrapper(Class[]... types) { + return new ConstructorWrapper<>(this.resolveSilent(types)); + } + + public Constructor resolveSilent(Class[]... types) { + try { + return this.resolve(types); + } catch (NoSuchMethodException ignored) { + } + return null; + } + + public Constructor resolve(Class[]... types) throws NoSuchMethodException { + ResolverQuery.Builder builder = ResolverQuery.builder(); + for (Class[] type : types) builder.with(type); + try { + return super.resolve(builder.build()); + } catch (ReflectiveOperationException e) { + throw (NoSuchMethodException) e; + } + } + + @Override + protected Constructor resolveObject(ResolverQuery query) throws ReflectiveOperationException { + return ConstructorReflection.setAccessible( + this.clazz.getDeclaredConstructor(query.getTypes())); + } + + public Constructor resolveFirstConstructor() throws ReflectiveOperationException { + for (Constructor constructor : this.clazz.getDeclaredConstructors()) { + return ConstructorReflection.setAccessible(constructor); + } + return null; + } + + public Constructor resolveFirstConstructorSilent() { + try { + return this.resolveFirstConstructor(); + } catch (Exception ignored) { + } + return null; + } + + public Constructor resolveLastConstructor() throws ReflectiveOperationException { + Constructor constructor = null; + for (Constructor constructor1 : this.clazz.getDeclaredConstructors()) { + constructor = constructor1; + } + if (constructor != null) { + return ConstructorReflection.setAccessible(constructor); + } + return null; + } + + public Constructor resolveLastConstructorSilent() { + try { + return this.resolveLastConstructor(); + } catch (Exception ignored) { + } + return null; + } + + @Override + protected NoSuchMethodException notFoundException(String joinedNames) { + return new NoSuchMethodException( + "Could not resolve constructor for " + joinedNames + " in class " + this.clazz); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/FieldResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/FieldResolver.java new file mode 100644 index 0000000..83d489b --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/FieldResolver.java @@ -0,0 +1,268 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.general.FieldReflection; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.FieldWrapper; + +import java.lang.reflect.Field; + +/** Resolver for fields */ +public class FieldResolver extends MemberResolver { + + public FieldResolver(Class clazz) { + super(clazz); + } + + public FieldResolver(String className) throws ClassNotFoundException { + super(className); + } + + @Override + public Field resolveIndex(int index) + throws IndexOutOfBoundsException, ReflectiveOperationException { + return FieldReflection.setAccessible(this.clazz.getDeclaredFields()[index]); + } + + @Override + public Field resolveIndexSilent(int index) { + try { + return this.resolveIndex(index); + } catch (IndexOutOfBoundsException | ReflectiveOperationException ignored) { + } + return null; + } + + @Deprecated + @Override + public FieldWrapper resolveIndexWrapper(int index) { + return new FieldWrapper<>(this.resolveIndexSilent(index)); + } + + @Deprecated + public FieldWrapper resolveWrapper(String... names) { + return new FieldWrapper<>(this.resolveSilent(names)); + } + + @Deprecated + public FieldWrapper resolveWrapper(ResolverQuery... queries) { + return new FieldWrapper<>(this.resolveSilent(queries)); + } + + public FieldAccessor resolveIndexAccessor(int index) { + return new FieldAccessor(this.resolveIndexSilent(index)); + } + + public FieldAccessor resolveAccessor(String... names) { + return new FieldAccessor(this.resolveSilent(names)); + } + + public FieldAccessor resolveAccessor(ResolverQuery... queries) { + return new FieldAccessor(this.resolveSilent(queries)); + } + + public Field resolveSilent(String... names) { + try { + return resolve(names); + } catch (NoSuchFieldException ignored) { + } + return null; + } + + public Field resolve(String... names) throws NoSuchFieldException { + ResolverQuery.Builder builder = ResolverQuery.builder(); + for (String name : names) builder.with(name); + try { + return super.resolve(builder.build()); + } catch (ReflectiveOperationException e) { + throw (NoSuchFieldException) e; + } + } + + public Field resolveSilent(ResolverQuery... queries) { + try { + return this.resolve(queries); + } catch (NoSuchFieldException ignored) { + } + return null; + } + + public Field resolve(ResolverQuery... queries) throws NoSuchFieldException { + try { + return super.resolve(queries); + } catch (ReflectiveOperationException e) { + throw (NoSuchFieldException) e; + } + } + + @Override + protected Field resolveObject(ResolverQuery query) throws ReflectiveOperationException { + if (query.getTypes() == null || query.getTypes().length == 0) { + return FieldReflection.setAccessible(this.clazz.getDeclaredField(query.getName())); + } else { + for (Field field : this.clazz.getDeclaredFields()) { + if (field.getName().equals(query.getName())) { + for (Class type : query.getTypes()) { + if (field.getType().equals(type)) { + return field; + } + } + } + } + } + return null; + } + + /** + * Attempts to find the first field of the specified type + * + * @param type Type to find + * @return the Field + * @throws ReflectiveOperationException (usually never) + * @see #resolveByLastType(Class) + */ + public Field resolveByFirstType(Class type) throws ReflectiveOperationException { + for (Field field : this.clazz.getDeclaredFields()) { + if (field.getType().equals(type)) { + return FieldReflection.setAccessible(field); + } + } + throw new NoSuchFieldException( + "Could not resolve field of type '" + type.toString() + "' in class " + this.clazz); + } + + public FieldAccessor resolveByFirstTypeAccessor(Class type) { + return new FieldAccessor(this.resolveByFirstTypeSilent(type)); + } + + /** + * Attempts to find the first field of the specified type + * + * @param type Type to find + * @return the Field + * @see #resolveByLastTypeSilent(Class) + */ + public Field resolveByFirstTypeSilent(Class type) { + try { + return this.resolveByFirstType(type); + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + /** + * Attempts to find the first field which extends/implements the specified type + * + * @param type Type to find + * @return the Field + * @throws ReflectiveOperationException (usually never) + * @see #resolveByLastType(Class) + */ + public Field resolveByFirstExtendingType(Class type) throws ReflectiveOperationException { + for (Field field : this.clazz.getDeclaredFields()) { + if (type.isAssignableFrom(field.getType())) { + return FieldReflection.setAccessible(field); + } + } + throw new NoSuchFieldException( + "Could not resolve field of type '" + type.toString() + "' in class " + this.clazz); + } + + /** + * Attempts to find the first field which extends/implements the specified type + * + * @param type Type to find + * @return the Field + * @see #resolveByLastTypeSilent(Class) + */ + public Field resolveByFirstExtendingTypeSilent(Class type) { + try { + return this.resolveByFirstExtendingType(type); + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + public FieldAccessor resolveByFirstExtendingTypeAccessor(Class type) { + return new FieldAccessor(this.resolveByFirstExtendingTypeSilent(type)); + } + + /** + * Attempts to find the last field of the specified type + * + * @param type Type to find + * @return the Field + * @throws ReflectiveOperationException (usually never) + * @see #resolveByFirstType(Class) + */ + public Field resolveByLastType(Class type) throws ReflectiveOperationException { + Field field = null; + for (Field field1 : this.clazz.getDeclaredFields()) { + if (field1.getType().equals(type)) { + field = field1; + } + } + if (field == null) { + throw new NoSuchFieldException( + "Could not resolve field of type '" + + type.toString() + + "' in class " + + this.clazz); + } + return FieldReflection.setAccessible(field); + } + + public Field resolveByLastTypeSilent(Class type) { + try { + return this.resolveByLastType(type); + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + public FieldAccessor resolveByLastTypeAccessor(Class type) { + return new FieldAccessor(resolveByLastTypeSilent(type)); + } + + /** + * Attempts to find the last field which extends/implements the specified type + * + * @param type Type to find + * @return the Field + * @throws ReflectiveOperationException (usually never) + * @see #resolveByFirstType(Class) + */ + public Field resolveByLastExtendingType(Class type) throws ReflectiveOperationException { + Field field = null; + for (Field field1 : this.clazz.getDeclaredFields()) { + if (type.isAssignableFrom(field1.getType())) { + field = field1; + } + } + if (field == null) { + throw new NoSuchFieldException( + "Could not resolve field of type '" + + type.toString() + + "' in class " + + this.clazz); + } + return FieldReflection.setAccessible(field); + } + + public Field resolveByLastExtendingTypeSilent(Class type) { + try { + return this.resolveByLastExtendingType(type); + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + public FieldAccessor resolveByLastExtendingTypeAccessor(Class type) { + return new FieldAccessor(this.resolveByLastExtendingTypeSilent(type)); + } + + @Override + protected NoSuchFieldException notFoundException(String joinedNames) { + return new NoSuchFieldException( + "Could not resolve field for " + joinedNames + " in class " + this.clazz); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/MemberResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/MemberResolver.java new file mode 100644 index 0000000..a8e7eb0 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/MemberResolver.java @@ -0,0 +1,57 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.WrapperAbstract; + +import java.lang.reflect.Member; + +/** + * Abstract class to resolve members + * + * @param member type + * @see ConstructorResolver + * @see FieldResolver + * @see MethodResolver + */ +public abstract class MemberResolver extends ResolverAbstract { + + protected Class clazz; + + public MemberResolver(Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("class cannot be null"); + } + this.clazz = clazz; + } + + public MemberResolver(String className) throws ClassNotFoundException { + this(new ClassResolver().resolve(className)); + } + + /** + * Resolve a member by its index + * + * @param index index + * @return the member + * @throws IndexOutOfBoundsException if the specified index is out of the available member + * bounds + * @throws ReflectiveOperationException if the object could not be set accessible + */ + public abstract T resolveIndex(int index) + throws IndexOutOfBoundsException, ReflectiveOperationException; + + /** + * Resolve member by its index (without exceptions) + * + * @param index index + * @return the member or null + */ + public abstract T resolveIndexSilent(int index); + + /** + * Resolce member wrapper by its index + * + * @param index index + * @return the wrapped member + */ + public abstract WrapperAbstract resolveIndexWrapper(int index); +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/MethodResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/MethodResolver.java new file mode 100644 index 0000000..e2f1527 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/MethodResolver.java @@ -0,0 +1,132 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import com.pepedevs.corelib.utils.reflection.general.MethodReflection; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; + +import java.lang.reflect.Method; + +/** Resolver for methods */ +public class MethodResolver extends MemberResolver { + + public MethodResolver(Class clazz) { + super(clazz); + } + + public MethodResolver(String className) throws ClassNotFoundException { + super(className); + } + + static boolean classListEqual(Class[] l1, Class[] l2) { + boolean equal = true; + if (l1.length != l2.length) { + return false; + } + for (int i = 0; i < l1.length; i++) { + if (l1[i] != l2[i]) { + equal = false; + break; + } + } + return equal; + } + + public Method resolveSignature(String... signatures) throws ReflectiveOperationException { + for (Method method : clazz.getDeclaredMethods()) { + String methodSignature = MethodWrapper.getMethodSignature(method); + for (String s : signatures) { + if (s.equals(methodSignature)) { + return MethodReflection.setAccessible(method); + } + } + } + return null; + } + + public Method resolveSignatureSilent(String... signatures) { + try { + return this.resolveSignature(signatures); + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + public MethodWrapper resolveSignatureWrapper(String... signatures) { + return new MethodWrapper<>(this.resolveSignatureSilent(signatures)); + } + + @Override + public Method resolveIndex(int index) + throws IndexOutOfBoundsException, ReflectiveOperationException { + return MethodReflection.setAccessible(this.clazz.getDeclaredMethods()[index]); + } + + @Override + public Method resolveIndexSilent(int index) { + try { + return this.resolveIndex(index); + } catch (IndexOutOfBoundsException | ReflectiveOperationException ignored) { + } + return null; + } + + @Override + public MethodWrapper resolveIndexWrapper(int index) { + return new MethodWrapper<>(this.resolveIndexSilent(index)); + } + + public MethodWrapper resolveWrapper(String... names) { + return new MethodWrapper<>(this.resolveSilent(names)); + } + + public MethodWrapper resolveWrapper(ResolverQuery... queries) { + return new MethodWrapper<>(this.resolveSilent(queries)); + } + + public Method resolveSilent(String... names) { + try { + return this.resolve(names); + } catch (NoSuchMethodException ignored) { + } + return null; + } + + @Override + public Method resolveSilent(ResolverQuery... queries) { + return super.resolveSilent(queries); + } + + public Method resolve(String... names) throws NoSuchMethodException { + ResolverQuery.Builder builder = ResolverQuery.builder(); + for (String name : names) { + builder.with(name); + } + return this.resolve(builder.build()); + } + + @Override + public Method resolve(ResolverQuery... queries) throws NoSuchMethodException { + try { + return super.resolve(queries); + } catch (ReflectiveOperationException e) { + throw (NoSuchMethodException) e; + } + } + + @Override + protected Method resolveObject(ResolverQuery query) throws ReflectiveOperationException { + for (Method method : this.clazz.getDeclaredMethods()) { + if (method.getName().equals(query.getName()) + && (query.getTypes().length == 0 + || classListEqual(query.getTypes(), method.getParameterTypes()))) { + return MethodReflection.setAccessible(method); + } + } + throw new NoSuchMethodException(); + } + + @Override + protected NoSuchMethodException notFoundException(String joinedNames) { + return new NoSuchMethodException( + "Could not resolve method for " + joinedNames + " in class " + this.clazz); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ResolverAbstract.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ResolverAbstract.java new file mode 100644 index 0000000..4bc562f --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ResolverAbstract.java @@ -0,0 +1,73 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Abstract resolver class + * + * @param resolved type + * @see ClassResolver + * @see ConstructorResolver + * @see FieldResolver + * @see MethodResolver + */ +public abstract class ResolverAbstract { + + protected final Map resolvedObjects = + new ConcurrentHashMap<>(); + + /** + * Same as {@link #resolve(ResolverQuery...)} but throws no exceptions + * + * @param queries Array of possible queries + * @return the resolved object if it was found, null otherwise + */ + protected T resolveSilent(ResolverQuery... queries) { + try { + return resolve(queries); + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + /** + * Attempts to resolve an array of possible queries to an object + * + * @param queries Array of possible queries + * @return the resolved object (if it was found) + * @throws ReflectiveOperationException if none of the possibilities could be resolved + * @throws IllegalArgumentException if the given possibilities are empty + */ + protected T resolve(ResolverQuery... queries) throws ReflectiveOperationException { + if (queries == null || queries.length <= 0) { + throw new IllegalArgumentException("Given possibilities are empty"); + } + for (ResolverQuery query : queries) { + // Object is already resolved, return it directly + if (resolvedObjects.containsKey(query)) { + return resolvedObjects.get(query); + } + + // Object is not yet resolved, try to find it + try { + T resolved = this.resolveObject(query); + // Store if it was found + resolvedObjects.put(query, resolved); + return resolved; + } catch (ReflectiveOperationException e) { + // Not found, ignore the exception + } + } + + // Couldn't find any of the possibilities + throw notFoundException(Arrays.asList(queries).toString()); + } + + protected abstract T resolveObject(ResolverQuery query) throws ReflectiveOperationException; + + protected ReflectiveOperationException notFoundException(String joinedNames) { + return new ReflectiveOperationException("Objects could not be resolved: " + joinedNames); + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ResolverQuery.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ResolverQuery.java new file mode 100644 index 0000000..a8bc35d --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/ResolverQuery.java @@ -0,0 +1,106 @@ +package com.pepedevs.corelib.utils.reflection.resolver; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Container class for resolver-queries Used by {@link MethodResolver} + * + * @see Builder + */ +public class ResolverQuery { + + private String name; + private Class[] types; + + public ResolverQuery(String name, Class... types) { + this.name = name; + this.types = types; + } + + public ResolverQuery(String name) { + this.name = name; + this.types = new Class[0]; + } + + public ResolverQuery(Class... types) { + this.types = types; + } + + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public Class[] getTypes() { + return types; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ResolverQuery that = (ResolverQuery) o; + + if (!Objects.equals(name, that.name)) { + return false; + } + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(types, that.types); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (types != null ? Arrays.hashCode(types) : 0); + return result; + } + + @Override + public String toString() { + return "ResolverQuery{" + + "name='" + + name + + '\'' + + ", types=" + + Arrays.toString(types) + + '}'; + } + + /** Builder class for {@link ResolverQuery} Access using {@link ResolverQuery#builder()} */ + public static class Builder { + + private final List queryList = new ArrayList<>(); + + private Builder() {} + + public Builder with(String name, Class... types) { + queryList.add(new ResolverQuery(name, types)); + return this; + } + + public Builder with(String name) { + queryList.add(new ResolverQuery(name)); + return this; + } + + public Builder with(Class... types) { + queryList.add(new ResolverQuery(types)); + return this; + } + + public ResolverQuery[] build() { + return queryList.toArray(new ResolverQuery[0]); + } + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/minecraft/CraftClassResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/minecraft/CraftClassResolver.java new file mode 100644 index 0000000..c1b147c --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/minecraft/CraftClassResolver.java @@ -0,0 +1,18 @@ +package com.pepedevs.corelib.utils.reflection.resolver.minecraft; + +import com.pepedevs.corelib.utils.reflection.resolver.ClassResolver; +import com.pepedevs.corelib.utils.version.Version; + +public class CraftClassResolver extends ClassResolver { + + @Override + public Class resolve(String... names) throws ClassNotFoundException { + for (int i = 0; i < names.length; i++) { + if (!names[i].startsWith("org.bukkit")) { + names[i] = Version.SERVER_VERSION.getObcPackage() + "." + names[i]; + } + } + return super.resolve(names); + } + +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/minecraft/NMSClassResolver.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/minecraft/NMSClassResolver.java new file mode 100644 index 0000000..a86ff14 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/minecraft/NMSClassResolver.java @@ -0,0 +1,25 @@ +package com.pepedevs.corelib.utils.reflection.resolver.minecraft; + +import com.pepedevs.corelib.utils.reflection.resolver.ClassResolver; +import com.pepedevs.corelib.utils.version.Version; + +/** + * {@link ClassResolver} for net.minecraft.server.* classes + */ +public class NMSClassResolver extends ClassResolver { + + @Override + public Class resolve(String... names) throws ClassNotFoundException { + for (int i = 0; i < names.length; i++) { + if (names[i].startsWith("net.minecraft")) continue; + + /* use the whole name */ + names[i] = + Version.SERVER_VERSION.getNmsPackage() + + "." + + names[i]; + } + return super.resolve(names); + } + +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/ClassWrapper.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/ClassWrapper.java new file mode 100644 index 0000000..cf05890 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/ClassWrapper.java @@ -0,0 +1,60 @@ +package com.pepedevs.corelib.utils.reflection.resolver.wrapper; + +import java.util.Objects; + +public class ClassWrapper extends WrapperAbstract { + + private final Class clazz; + + public ClassWrapper(Class clazz) { + this.clazz = clazz; + } + + @Override + public boolean exists() { + return this.clazz != null; + } + + public Class getClazz() { + return clazz; + } + + public String getName() { + return this.clazz.getName(); + } + + public R newInstance() { + try { + return this.clazz.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public R newInstanceSilent() { + try { + return this.clazz.newInstance(); + } catch (InstantiationException | IllegalAccessException ignored) { + } + return null; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || this.getClass() != object.getClass()) { + return false; + } + + ClassWrapper that = (ClassWrapper) object; + + return Objects.equals(clazz, that.clazz); + } + + @Override + public int hashCode() { + return clazz != null ? clazz.hashCode() : 0; + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/ConstructorWrapper.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/ConstructorWrapper.java new file mode 100644 index 0000000..5042afa --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/ConstructorWrapper.java @@ -0,0 +1,64 @@ +package com.pepedevs.corelib.utils.reflection.resolver.wrapper; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; + +public class ConstructorWrapper extends WrapperAbstract { + + private final Constructor constructor; + + public ConstructorWrapper(Constructor constructor) { + this.constructor = constructor; + } + + @Override + public boolean exists() { + return this.constructor != null; + } + + public R newInstance(Object... args) { + try { + return this.constructor.newInstance(args); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public R newInstanceSilent(Object... args) { + try { + return this.constructor.newInstance(args); + } catch (InvocationTargetException + | InstantiationException + | IllegalAccessException ignored) { + } + return null; + } + + public Class[] getParameterTypes() { + return this.constructor.getParameterTypes(); + } + + public Constructor getConstructor() { + return constructor; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + ConstructorWrapper that = (ConstructorWrapper) object; + + return Objects.equals(constructor, that.constructor); + } + + @Override + public int hashCode() { + return constructor != null ? constructor.hashCode() : 0; + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/FieldWrapper.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/FieldWrapper.java new file mode 100644 index 0000000..9befe8d --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/FieldWrapper.java @@ -0,0 +1,78 @@ +package com.pepedevs.corelib.utils.reflection.resolver.wrapper; + +import java.lang.reflect.Field; +import java.util.Objects; + +public class FieldWrapper extends WrapperAbstract { + + private final Field field; + + public FieldWrapper(Field field) { + this.field = field; + } + + @Override + public boolean exists() { + return this.field != null; + } + + public String getName() { + return this.field.getName(); + } + + @SuppressWarnings("unchecked") + public R get(Object object) { + try { + return (R) this.field.get(object); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public R getSilent(Object object) { + try { + return (R) this.field.get(object); + } catch (IllegalAccessException ignored) { + } + return null; + } + + public void set(Object object, R value) { + try { + this.field.set(object, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public void setSilent(Object object, R value) { + try { + this.field.set(object, value); + } catch (IllegalAccessException ignored) { + } + } + + public Field getField() { + return field; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + FieldWrapper that = (FieldWrapper) object; + + return Objects.equals(field, that.field); + } + + @Override + public int hashCode() { + return field != null ? field.hashCode() : 0; + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/MethodWrapper.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/MethodWrapper.java new file mode 100644 index 0000000..298b1e2 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/MethodWrapper.java @@ -0,0 +1,305 @@ +package com.pepedevs.corelib.utils.reflection.resolver.wrapper; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MethodWrapper extends WrapperAbstract { + + private final Method method; + + public MethodWrapper(Method method) { + this.method = method; + } + + /** + * Generates a method's signature. + * + * @param method the method to get the signature for + * @param fullClassNames whether to use the full class name + * @return the method's signature + */ + public static String getMethodSignature(Method method, boolean fullClassNames) { + // StringBuilder stringBuilder = new StringBuilder(); + // + // Class returnType = method.getReturnType(); + // if (returnType.isPrimitive()) { + // stringBuilder.append(returnType); + // } else { + // stringBuilder.append(fullClassNames ? returnType.getName() : + // returnType.getSimpleName()); + // } + // stringBuilder.append(" "); + // stringBuilder.append(method.getName()); + // + // stringBuilder.append("("); + // + // boolean first = true; + // for (Class clazz : method.getParameterTypes()) { + // if (!first) { stringBuilder.append(","); } + // stringBuilder.append(fullClassNames ? clazz.getName() : clazz.getSimpleName()); + // first = false; + // } + // return stringBuilder.append(")").toString(); + + return MethodSignature.of(method, fullClassNames).getSignature(); + } + + /** + * @param method Method to get the signature for + * @return the signature + * @see #getMethodSignature(Method, boolean) + */ + public static String getMethodSignature(Method method) { + return getMethodSignature(method, false); + } + + @Override + public boolean exists() { + return this.method != null; + } + + public String getName() { + return this.method.getName(); + } + + @SuppressWarnings("unchecked") + public R invoke(Object object, Object... args) { + try { + return (R) this.method.invoke(object, args); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public R invokeSilent(Object object, Object... args) { + try { + return (R) this.method.invoke(object, args); + } catch (InvocationTargetException | IllegalAccessException ignored) { + } + return null; + } + + public Method getMethod() { + return method; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + MethodWrapper that = (MethodWrapper) object; + + return Objects.equals(method, that.method); + } + + @Override + public int hashCode() { + return method != null ? method.hashCode() : 0; + } + + public static class MethodSignature { + + static final Pattern SIGNATURE_STRING_PATTERN = Pattern.compile("(.+) (.*)\\((.*)\\)"); + + private final String returnType; + private final Pattern returnTypePattern; + private final String name; + private final Pattern namePattern; + private final String[] parameterTypes; + private final String signature; + + public MethodSignature(String returnType, String name, String[] parameterTypes) { + this.returnType = returnType; + this.returnTypePattern = + Pattern.compile( + returnType + .replace("?", "\\w") + .replace("*", "\\w*") + .replace("[", "\\[") + .replace("]", "\\]")); + this.name = name; + this.namePattern = Pattern.compile(name.replace("?", "\\w").replace("*", "\\w*")); + this.parameterTypes = parameterTypes; + + StringBuilder builder = new StringBuilder(); + builder.append(returnType).append(" ").append(name).append("("); + boolean first = true; + for (String parameterType : parameterTypes) { + if (!first) { + builder.append(","); + } + builder.append(parameterType); + first = false; + } + this.signature = builder.append(")").toString(); + } + + public static MethodSignature of(Method method, boolean fullClassNames) { + Class returnType = method.getReturnType(); + Class[] parameterTypes = method.getParameterTypes(); + + String returnTypeString; + if (returnType.isPrimitive()) { + returnTypeString = returnType.toString(); + } else { + returnTypeString = + fullClassNames ? returnType.getName() : returnType.getSimpleName(); + } + String methodName = method.getName(); + String[] parameterTypeStrings = new String[parameterTypes.length]; + for (int i = 0; i < parameterTypeStrings.length; i++) { + if (parameterTypes[i].isPrimitive()) { + parameterTypeStrings[i] = parameterTypes[i].toString(); + } else { + parameterTypeStrings[i] = + fullClassNames + ? parameterTypes[i].getName() + : parameterTypes[i].getSimpleName(); + } + } + + return new MethodSignature(returnTypeString, methodName, parameterTypeStrings); + } + + public static MethodSignature fromString(String signatureString) { + if (signatureString == null) { + return null; + } + Matcher matcher = SIGNATURE_STRING_PATTERN.matcher(signatureString); + if (matcher.find()) { + if (matcher.groupCount() != 3) { + throw new IllegalArgumentException("invalid signature"); + } + return new MethodSignature( + matcher.group(1), matcher.group(2), matcher.group(3).split(",")); + } else { + throw new IllegalArgumentException("invalid signature"); + } + } + + public String getReturnType() { + return returnType; + } + + public boolean isReturnTypeWildcard() { + return "?".equals(returnType) || "*".equals(returnType); + } + + public String getName() { + return name; + } + + public boolean isNameWildcard() { + return "?".equals(name) || "*".equals(name); + } + + public String[] getParameterTypes() { + return parameterTypes; + } + + public String getParameterType(int index) throws IndexOutOfBoundsException { + return parameterTypes[index]; + } + + public boolean isParameterWildcard(int index) throws IndexOutOfBoundsException { + return "?".equals(getParameterType(index)) || "*".equals(getParameterType(index)); + } + + public String getSignature() { + return signature; + } + + /** + * Checks whether this signature matches another signature. Wildcards are checked in this + * signature, but not the other signature. + * + * @param other signature to check + * @return whether the signatures match + */ + public boolean matches(MethodSignature other) { + if (other == null) { + return false; + } + + // if (!returnType.equals(other.returnType)) { + // if (!isReturnTypeWildcard()) { return false; } + // } + // if (!name.equals(other.name)) { + // if (!isNameWildcard()) { return false; } + // } + // if (parameterTypes.length != other.parameterTypes.length) { return false; } + // for (int i = 0; i < parameterTypes.length; i++) { + // if (!getParameterType(i).equals(other.getParameterType(i))) { + // if (!isParameterWildcard(i)) { return false; } + // } + // } + + if (!returnTypePattern.matcher(other.returnType).matches()) { + return false; + } + if (!namePattern.matcher(other.name).matches()) { + return false; + } + if (parameterTypes.length != other.parameterTypes.length) { + return false; + } + for (int i = 0; i < parameterTypes.length; i++) { + if (!Pattern.compile(getParameterType(i).replace("?", "\\w").replace("*", "\\w*")) + .matcher(other.getParameterType(i)) + .matches()) { + return false; + } + } + + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MethodSignature signature1 = (MethodSignature) o; + + if (!returnType.equals(signature1.returnType)) { + return false; + } + if (!name.equals(signature1.name)) { + return false; + } + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(parameterTypes, signature1.parameterTypes)) { + return false; + } + return signature.equals(signature1.signature); + } + + @Override + public int hashCode() { + int result = returnType.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + Arrays.hashCode(parameterTypes); + result = 31 * result + signature.hashCode(); + return result; + } + + @Override + public String toString() { + return getSignature(); + } + } +} diff --git a/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/WrapperAbstract.java b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/WrapperAbstract.java new file mode 100644 index 0000000..a8ba194 --- /dev/null +++ b/reflection-utils/src/main/java/com/pepedevs/corelib/utils/reflection/resolver/wrapper/WrapperAbstract.java @@ -0,0 +1,11 @@ +package com.pepedevs.corelib.utils.reflection.resolver.wrapper; + +public abstract class WrapperAbstract { + + /** + * Check whether the wrapped object exists (i.e. is not null) + * + * @return true if the wrapped object exists + */ + public abstract boolean exists(); +} diff --git a/scoreboard/pom.xml b/scoreboard/pom.xml new file mode 100644 index 0000000..e9dc1bd --- /dev/null +++ b/scoreboard/pom.xml @@ -0,0 +1,37 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + scoreboard + + + 8 + 8 + + + + + com.pepedevs + reflection-utils + ${project.parent.version} + + + com.pepedevs + minecraft-version + ${project.parent.version} + + + com.pepedevs + utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/scoreboard/src/main/java/com/pepedevs/corelib/scoreboard/Scoreboard.java b/scoreboard/src/main/java/com/pepedevs/corelib/scoreboard/Scoreboard.java new file mode 100644 index 0000000..0a9d8a6 --- /dev/null +++ b/scoreboard/src/main/java/com/pepedevs/corelib/scoreboard/Scoreboard.java @@ -0,0 +1,424 @@ +package com.pepedevs.corelib.scoreboard; + +import com.pepedevs.corelib.utils.reflection.PacketConstant; +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.bukkit.BukkitReflection; +import com.pepedevs.corelib.utils.reflection.general.EnumReflection; +import com.pepedevs.corelib.utils.reflection.resolver.ConstructorResolver; +import com.pepedevs.corelib.utils.reflection.resolver.MethodResolver; +import com.pepedevs.corelib.utils.reflection.resolver.ResolverQuery; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.CraftClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.NMSClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ConstructorWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import com.pepedevs.corelib.utils.version.Version; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; + +public class Scoreboard { + + private static final Map, FieldAccessor[]> PACKETS = new HashMap<>(8); + private static final String[] COLOR_CODES = Arrays.stream(ChatColor.values()) + .map(Object::toString) + .toArray(String[]::new); + // Packets and components + private static final ClassWrapper CHAT_COMPONENT_CLASS; + private static final ClassWrapper CHAT_FORMAT_ENUM; + private static final Object EMPTY_MESSAGE; + private static final Object RESET_FORMATTING; + private static final MethodWrapper MESSAGE_FROM_STRING; + // Scoreboard packets + private static final ConstructorWrapper PACKET_SB_OBJ; + private static final ConstructorWrapper PACKET_SB_DISPLAY_OBJ; + private static final ConstructorWrapper PACKET_SB_SCORE; + private static final ConstructorWrapper PACKET_SB_TEAM; + private static final ConstructorWrapper PACKET_SB_SERIALIZABLE_TEAM; + // Scoreboard enums + private static final ClassWrapper ENUM_SB_HEALTH_DISPLAY; + private static final ClassWrapper ENUM_SB_ACTION; + private static final Object ENUM_SB_HEALTH_DISPLAY_INTEGER; + private static final Object ENUM_SB_ACTION_CHANGE; + private static final Object ENUM_SB_ACTION_REMOVE; + + static { + CraftClassResolver craftClassResolver = new CraftClassResolver(); + NMSClassResolver nmsClassResolver = new NMSClassResolver(); + ClassWrapper craftChatMessageClass = craftClassResolver.resolveWrapper("util.CraftChatMessage"); + ClassWrapper sbTeamClass = nmsClassResolver.resolveWrapper(PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_TEAM.getClazz().getName() + "$b"); + + MESSAGE_FROM_STRING = new MethodResolver(craftChatMessageClass.getClazz()).resolveWrapper( + ResolverQuery.builder().with("fromString", String.class).build()); + CHAT_COMPONENT_CLASS = nmsClassResolver.resolveWrapper("IChatBaseComponent", "net.minecraft.network.chat.IChatBaseComponent"); + CHAT_FORMAT_ENUM = nmsClassResolver.resolveWrapper("EnumChatFormat"); + EMPTY_MESSAGE = Array.get(MESSAGE_FROM_STRING.invoke(null, ""), 0); + RESET_FORMATTING = EnumReflection.getEnumConstant(CHAT_FORMAT_ENUM.getClazz().asSubclass(Enum.class), "RESET", 21); + PACKET_SB_OBJ = new ConstructorResolver(PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_OBJECTIVE.getClazz()).resolveWrapper(new Class[0]); + PACKET_SB_DISPLAY_OBJ = new ConstructorResolver(PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJECTIVE.getClazz()).resolveWrapper(new Class[0]); + PACKET_SB_SCORE = new ConstructorResolver(PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_SCORE.getClazz()).resolveWrapper(new Class[0]); + PACKET_SB_TEAM = new ConstructorResolver(PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_TEAM.getClazz()).resolveWrapper(new Class[0]); + PACKET_SB_SERIALIZABLE_TEAM = sbTeamClass == null ? null : new ConstructorResolver(sbTeamClass.getClazz()).resolveWrapper(new Class[0]); + + for (ClassWrapper clazz : new ClassWrapper[]{PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_OBJECTIVE, PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJECTIVE, + PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_SCORE, PacketConstant.PACKET_PLAY_OUT_SCOREBOARD_TEAM, sbTeamClass}) { + if (clazz == null) + continue; + FieldAccessor[] fields = Arrays.stream(clazz.getClazz().getDeclaredFields()) + .map(FieldAccessor::new) + .filter(field -> !field.isStatic()) + .toArray(FieldAccessor[]::new); + PACKETS.put(clazz, fields); + } + + String enumSbActionClass = Version.SERVER_VERSION.isNewerEquals(Version.v1_13_R1) + ? "ScoreboardServer$Action" + : "PacketPlayOutScoreboardScore$EnumScoreboardAction"; + ENUM_SB_HEALTH_DISPLAY = nmsClassResolver.resolveWrapper("IScoreboardCriteria$EnumScoreboardHealthDisplay", + "net.minecraft.world.scores.criteria.IScoreboardCriteria$EnumScoreboardHealthDisplay"); + ENUM_SB_ACTION = nmsClassResolver.resolveWrapper(enumSbActionClass, "net.minecraft.server." + enumSbActionClass); + ENUM_SB_HEALTH_DISPLAY_INTEGER = EnumReflection.getEnumConstant(ENUM_SB_HEALTH_DISPLAY.getClazz().asSubclass(Enum.class), "INTEGER", 0); + ENUM_SB_ACTION_CHANGE = EnumReflection.getEnumConstant(ENUM_SB_ACTION.getClazz().asSubclass(Enum.class), "CHANGE", 0); + ENUM_SB_ACTION_REMOVE = EnumReflection.getEnumConstant(ENUM_SB_ACTION.getClazz().asSubclass(Enum.class), "REMOVE", 1); + } + + private static final int MAX_DISPLAY_NAME_LENGTH = 32; + private static final int MAX_ELEMENTS_LENGTH = 40; + + private String title; + private List elements; + private String oldTitle; + private List oldElements; + + private final Map shown; + + public Scoreboard(String title, String... elements) { + this(title, Arrays.asList(elements)); + } + + public Scoreboard(String title, List elements) { + Validate.isTrue(!(title.length() > MAX_DISPLAY_NAME_LENGTH && Version.SERVER_VERSION.isOlder(Version.v1_13_R1)), + "Title is longer than 32 chars."); + + this.title = title; + this.oldTitle = title; + this.elements = new LinkedList<>(); + this.oldElements = new ArrayList<>(); + this.shown = new ConcurrentHashMap<>(); + } + + public String getTitle() { + return this.title; + } + + public Scoreboard setTitle(String title) { + this.title = title; + return this; + } + + public List getElements() { + return Collections.unmodifiableList(this.elements); + } + + public String getLine(int line) { + this.checkLineNumber(line, true, false); + return this.elements.get(line); + } + + public Scoreboard addLines(String... elements) { + return this.addLines(Arrays.asList(elements)); + } + + public Scoreboard addLines(Collection elements) { + this.checkLineNumber(this.elements.size() + elements.size(), false, true); + if (Version.SERVER_VERSION.isOlder(Version.v1_13_R1)) { + int lineCount = 0; + for (String s : elements) { + if (s != null && s.length() > MAX_ELEMENTS_LENGTH) { + throw new IllegalArgumentException("Line " + lineCount + " is longer than 40 chars"); + } + lineCount++; + } + } + + this.oldElements = new ArrayList<>(elements); + this.elements.addAll(elements); + return this; + } + + public void setLine(int index, String line) { + this.checkLineNumber(index, true, false); + this.checkLineNumber(this.elements.size() + elements.size(), false, true); + if (Version.SERVER_VERSION.isOlder(Version.v1_13_R1)) { + if (line != null && line.length() > MAX_ELEMENTS_LENGTH) { + throw new IllegalArgumentException("Line `" + line + "` is longer than 40 chars"); + } + } + this.oldElements = new ArrayList<>(this.elements); + this.elements.set(index, line); + } + + public void removeLine(int index) { + this.checkLineNumber(index, true, false); + this.oldElements = new ArrayList<>(this.elements); + this.elements.remove(index); + } + + public void show(Player... players) { + for (Player player : players) { + if (this.shown.containsKey(player.getUniqueId())) + continue; + + String id = "coreboard-" + Integer.toHexString(ThreadLocalRandom.current().nextInt()); + this.sendObjectivePacket(ObjectiveMode.CREATE, player, id); + this.sendDisplayObjectivePacket(player, id); + for (int i = 0; i < this.elements.size(); i++) { + this.sendScorePacket(i, ScoreboardAction.CHANGE, player, id); + this.sendTeamPacket(i, TeamMode.CREATE, player, id); + } + this.shown.put(player.getUniqueId(), id); + } + } + + public void hide(Player... players) { + for (Player player : players) { + if (!this.shown.containsKey(player.getUniqueId())) + continue; + + String id = this.shown.get(player.getUniqueId()); + for (int i = 0; i < this.elements.size(); i++) { + this.sendTeamPacket(i - 1, TeamMode.REMOVE, player, id); + } + this.sendObjectivePacket(ObjectiveMode.REMOVE, player, id); + this.shown.remove(player.getUniqueId()); + } + } + + public synchronized Scoreboard update(boolean force, Player... players) { + for (Player player : players) { + Validate.isTrue(this.shown.containsKey(player.getUniqueId()), "The player is not viewing the this scoreboard."); + String id = this.shown.get(player.getUniqueId()); + if (force) { + this.sendObjectivePacket(ObjectiveMode.UPDATE, player, id); + for (int i = this.elements.size(); i > 0; i--) { + this.sendTeamPacket(i - 1, TeamMode.REMOVE, player, id); + this.sendScorePacket(i - 1, ScoreboardAction.REMOVE, player, id); + } + for (int i = 0; i < this.elements.size(); i++) { + this.sendScorePacket(i, ScoreboardAction.CHANGE, player, id); + this.sendTeamPacket(i, TeamMode.CREATE, player, id); + } + this.oldElements = new ArrayList<>(this.elements); + } else { + if (!this.oldTitle.equals(this.title)) + this.sendObjectivePacket(ObjectiveMode.UPDATE, player, id); + + if (this.oldElements.size() != this.elements.size()) { + List oldLinesCopy = new ArrayList<>(this.oldElements); + + if (this.oldElements.size() > this.elements.size()) { + for (int i = oldLinesCopy.size(); i > this.elements.size(); i--) { + this.sendTeamPacket(i - 1, TeamMode.REMOVE, player, id); + this.sendScorePacket(i - 1, ScoreboardAction.REMOVE, player, id); + + this.oldElements.remove(0); + } + } else { + for (int i = oldLinesCopy.size(); i < this.elements.size(); i++) { + this.sendScorePacket(i, ScoreboardAction.CHANGE, player, id); + this.sendTeamPacket(i, TeamMode.CREATE, player, id); + + this.oldElements.add(this.oldElements.size() - i, this.elements.get(this.elements.size() - 1 - i)); + } + } + } + + for (int i = 0; i < this.elements.size(); i++) { + if (!Objects.equals(this.oldElements.get(this.oldElements.size() - 1 - i), this.elements.get(this.elements.size() - 1 - i))) { + this.sendTeamPacket(i, TeamMode.UPDATE, player, id); + } + } + } + + this.oldTitle = title; + } + + return this; + } + + public synchronized Scoreboard updateAll(boolean force) { + for (UUID uuid : this.shown.keySet()) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + this.update(force, player); + } + } + return this; + } + + private void checkLineNumber(int line, boolean checkInRange, boolean checkMax) { + Validate.isTrue(line > 0, "Line number must be positive"); + + if (checkInRange && line >= this.elements.size()) { + throw new IllegalArgumentException("Line number must be under " + this.elements.size()); + } + + if (checkMax && line >= COLOR_CODES.length - 1) { + throw new IllegalArgumentException("Line number is too high: " + line); + } + } + + private void sendObjectivePacket(ObjectiveMode mode, Player player, String id) { + Object packet = PACKET_SB_OBJ.newInstance(); + + this.updatePacketField(packet, String.class, id); + this.updatePacketField(packet, int.class, mode.ordinal()); + + if (mode != ObjectiveMode.REMOVE) { + this.setComponentField(packet, this.title, 1); + + this.updatePacketField(packet, ENUM_SB_HEALTH_DISPLAY.getClazz(), ENUM_SB_HEALTH_DISPLAY_INTEGER); + } + + BukkitReflection.sendPacket(player, packet); + } + + private void sendDisplayObjectivePacket(Player player, String id) { + Object packet = PACKET_SB_DISPLAY_OBJ.newInstance(); + + this.updatePacketField(packet, int.class, 1); // Position (1: sidebar) + this.updatePacketField(packet, String.class, id); // Score Name + + BukkitReflection.sendPacket(player, packet); + } + + private void sendScorePacket(int score, ScoreboardAction action, Player player, String id) { + Object packet = PACKET_SB_SCORE.newInstance(); + + this.updatePacketField(packet, String.class, COLOR_CODES[score], 0); // Player Name + this.updatePacketField(packet, ENUM_SB_ACTION.getClazz(), action == ScoreboardAction.REMOVE ? ENUM_SB_ACTION_REMOVE : ENUM_SB_ACTION_CHANGE); + + if (action == ScoreboardAction.CHANGE) { + this.updatePacketField(packet, String.class, id, 1); // Objective Name + this.updatePacketField(packet, int.class, score); // Score + } + + BukkitReflection.sendPacket(player, packet); + } + + private void sendTeamPacket(int score, TeamMode mode, Player player, String id) { + if (mode == TeamMode.ADD_PLAYERS || mode == TeamMode.REMOVE_PLAYERS) { + throw new UnsupportedOperationException(); + } + + int maxLength = Version.SERVER_VERSION.isOlder(Version.v1_13_R1) ? 16 : 1024; + Object packet = PACKET_SB_TEAM.newInstance(); + + this.updatePacketField(packet, String.class, id + ':' + score); // Team name + this.updatePacketField(packet, int.class, mode.ordinal(), Version.SERVER_VERSION.equalsVersion(Version.v1_8_R3) ? 1 : 0); // Update mode + + if (mode == TeamMode.CREATE || mode == TeamMode.UPDATE) { + String line = this.elements.get(this.elements.size() - 1 - score); + String prefix; + String suffix = null; + + if (line == null || line.isEmpty()) { + prefix = COLOR_CODES[score] + ChatColor.RESET; + } else if (line.length() <= maxLength) { + prefix = line; + } else { + // Prevent splitting color codes + int index = line.charAt(maxLength - 1) == ChatColor.COLOR_CHAR ? (maxLength - 1) : maxLength; + prefix = line.substring(0, index); + String suffixTmp = line.substring(index); + ChatColor chatColor = null; + + if (suffixTmp.length() >= 2 && suffixTmp.charAt(0) == ChatColor.COLOR_CHAR) { + chatColor = ChatColor.getByChar(suffixTmp.charAt(1)); + } + + String color = ChatColor.getLastColors(prefix); + boolean addColor = chatColor == null || chatColor.isFormat(); + + suffix = (addColor ? (color.isEmpty() ? ChatColor.RESET.toString() : color) : "") + suffixTmp; + } + + if (prefix.length() > maxLength || (suffix != null && suffix.length() > maxLength)) { + // Something went wrong, just cut to prevent client crash/kick + prefix = prefix.substring(0, maxLength); + suffix = (suffix != null) ? suffix.substring(0, maxLength) : null; + } + + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_17_R1)) { + Object team = PACKET_SB_SERIALIZABLE_TEAM.newInstance(); + // Since the packet is initialized with null values, we need to change more things. + this.setComponentField(team, "", 0); // Display name + this.updatePacketField(team, CHAT_FORMAT_ENUM.getClazz(), RESET_FORMATTING); // Color + this.setComponentField(team, prefix, 1); // Prefix + this.setComponentField(team, suffix == null ? "" : suffix, 2); // Suffix + this.updatePacketField(team, String.class, "always", 0); // Visibility + this.updatePacketField(team, String.class, "always", 1); // Collisions + this.updatePacketField(packet, Optional.class, Optional.of(team)); + } else { + this.setComponentField(packet, prefix, 2); // Prefix + this.setComponentField(packet, suffix == null ? "" : suffix, 3); // Suffix + this.updatePacketField(packet, String.class, "always", 4); // Visibility for 1.8+ + this.updatePacketField(packet, String.class, "always", 5); // Collisions for 1.9+ + } + + if (mode == TeamMode.CREATE) { + this.updatePacketField(packet, Collection.class, Collections.singletonList(COLOR_CODES[score])); // Players in the team + } + } + + BukkitReflection.sendPacket(player, packet); + } + + + private void updatePacketField(Object object, Class fieldType, Object value) { + this.updatePacketField(object, fieldType, value, 0); + } + + private void updatePacketField(Object packet, Class fieldType, Object value, int count) { + int i = 0; + for (FieldAccessor field : PACKETS.get(new ClassWrapper<>(packet.getClass()))) { + if (field.getType() == fieldType && count == i++) { + field.set(packet, value); + } + } + } + + private void setComponentField(Object packet, String value, int count) { + if (!Version.SERVER_VERSION.isNewerEquals(Version.v1_13_R1)) { + this.updatePacketField(packet, String.class, value, count); + return; + } + + int i = 0; + for (FieldAccessor field : PACKETS.get(new ClassWrapper<>(packet.getClass()))) { + if ((field.getType() == String.class || field.getType() == CHAT_COMPONENT_CLASS.getClazz()) && count == i++) { + field.set(packet, value.isEmpty() ? EMPTY_MESSAGE : Array.get(MESSAGE_FROM_STRING.invoke(value), 0)); + } + } + } + + enum TeamMode { + CREATE, REMOVE, UPDATE, ADD_PLAYERS, REMOVE_PLAYERS + } + + enum ScoreboardAction { + CHANGE, REMOVE + } + + enum ObjectiveMode { + CREATE, REMOVE, UPDATE, + ; + } + +} diff --git a/task-utils/pom.xml b/task-utils/pom.xml new file mode 100644 index 0000000..a18c027 --- /dev/null +++ b/task-utils/pom.xml @@ -0,0 +1,27 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + task-utils + + + 8 + 8 + + + + + com.pepedevs + utils + ${project.parent.version} + + + + \ No newline at end of file diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/CancellableWorkload.java b/task-utils/src/main/java/com/pepedevs/corelib/task/CancellableWorkload.java new file mode 100644 index 0000000..1a00f19 --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/CancellableWorkload.java @@ -0,0 +1,34 @@ +package com.pepedevs.corelib.task; + +import com.pepedevs.corelib.utils.Cancellable; + +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class CancellableWorkload implements Workload, Cancellable { + + protected final AtomicBoolean cancelled; + + public CancellableWorkload() { + this(false); + } + + public CancellableWorkload(boolean cancelled) { + this.cancelled = new AtomicBoolean(cancelled); + } + + @Override + public boolean reSchedule() { + return !this.isCancelled(); + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled.set(cancelled); + } + + @Override + public boolean isCancelled() { + return this.cancelled.get(); + } + +} diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/Task.java b/task-utils/src/main/java/com/pepedevs/corelib/task/Task.java new file mode 100644 index 0000000..0475e66 --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/Task.java @@ -0,0 +1,15 @@ +package com.pepedevs.corelib.task; + +public interface Task { + + int getID(); + + void submit(Workload workload); + + void cancel(); + + boolean isCancelled(); + + boolean isRunning(); + +} diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/TaskQueueHandler.java b/task-utils/src/main/java/com/pepedevs/corelib/task/TaskQueueHandler.java new file mode 100644 index 0000000..f365b16 --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/TaskQueueHandler.java @@ -0,0 +1,113 @@ +package com.pepedevs.corelib.task; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadFactory; +import java.util.function.Predicate; + +public class TaskQueueHandler { + + public static final int MAX_QUEUING_POOL_CONSTRAIN = 30 * 1000000; + public static final int DEFAULT_QUEUING_POOL_CONSTRAIN = 30 * 1000000; + + private final ThreadFactory threadFactory; + private final Map queueingPool; + + private int queueID; + + public TaskQueueHandler() { + this("Thread %d"); + } + + public TaskQueueHandler(String threadNameFormat) { + this.threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNameFormat).build(); + this.queueingPool = new ConcurrentHashMap<>(); + this.queueID = 0; + } + + public Task newPool(int poolCapacity) { + return this.newPool(poolCapacity, DEFAULT_QUEUING_POOL_CONSTRAIN); + } + + public Task newPool(int poolCapacity, long constrainNanos) { + if (constrainNanos >= MAX_QUEUING_POOL_CONSTRAIN) + throw new IllegalArgumentException("Pool constrain cannot be larger than 50ms."); + + WorkloadDistributor distributor = new WorkloadDistributor(queueID++); + for (int i = 0; i < poolCapacity; i++) { + distributor.createThread(constrainNanos); + } + this.queueingPool.put(queueID, distributor); + distributor.start(this.threadFactory); + return distributor; + } + + public void submit(int poolId, Workload workload) { + this.queueingPool.get(poolId).submit(workload); + } + + public CancellableWorkload runTaskLater(int poolId, Runnable runnable, long delay) { + CancellableWorkload workload = new CancellableWorkload() { + final long start = System.currentTimeMillis(); + @Override + public void compute() { + this.setCancelled(true); + runnable.run(); + } + + @Override + public boolean shouldExecute() { + return !this.isCancelled() && System.currentTimeMillis() - this.start >= delay; + } + }; + this.submit(poolId, workload); + return workload; + } + + public CancellableWorkload runTaskTimer(int poolId, Runnable runnable, long time) { + CancellableWorkload workload = new CancellableWorkload() { + long lastExec = System.currentTimeMillis(); + @Override + public void compute() { + lastExec = System.currentTimeMillis(); + runnable.run(); + } + + @Override + public boolean shouldExecute() { + return !this.isCancelled() && System.currentTimeMillis() - this.lastExec >= time; + } + }; + this.submit(poolId, workload); + return workload; + } + + public CancellableWorkload runTaskTimer(int poolId, Runnable runnable, long time, Predicate interruption) { + CancellableWorkload workload = new CancellableWorkload() { + long lastExec = System.currentTimeMillis(); + @Override + public void compute() { + lastExec = System.currentTimeMillis(); + runnable.run(); + if (interruption.test(runnable)) { + this.setCancelled(true); + } + } + + @Override + public boolean shouldExecute() { + return !this.isCancelled() && System.currentTimeMillis() - this.lastExec >= time; + } + }; + this.submit(poolId, workload); + return workload; + } + + public void cancelTask(int poolID) { + Task task = this.queueingPool.remove(poolID); + task.cancel(); + } + +} diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/Workload.java b/task-utils/src/main/java/com/pepedevs/corelib/task/Workload.java new file mode 100644 index 0000000..0da67af --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/Workload.java @@ -0,0 +1,42 @@ +package com.pepedevs.corelib.task; + +/** An interface to determine works. */ +public interface Workload { + + /** Computes a piece of the work. */ + void compute(); + + /** + * Computes then checks {@link #reSchedule()}. + * + *

+ * + * @return {@code true} if re-scheduling + */ + default boolean computeThenCheckForScheduling() { + this.compute(); + return !this.reSchedule(); + } + + /** + * Checks if the work is reSchedulable. + * + *

+ * + * @return {@code true} if the work should be re schedule + */ + default boolean reSchedule() { + return false; + } + + /** + * Checks if the work should execute. if it's {@code true} than runs {@link Workload#compute()}. + * + *

+ * + * @return {@code true} if the work should execute + */ + default boolean shouldExecute() { + return true; + } +} diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/WorkloadDistributor.java b/task-utils/src/main/java/com/pepedevs/corelib/task/WorkloadDistributor.java new file mode 100644 index 0000000..68b0b7f --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/WorkloadDistributor.java @@ -0,0 +1,79 @@ +package com.pepedevs.corelib.task; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadFactory; + +/** A class that stores and distributes {@link WorkloadThread} instances. */ +public class WorkloadDistributor implements Task { + + private final Map tasks = new ConcurrentHashMap<>(); + + private final int id; + private ThreadFactory threadFactory; + private boolean running; + + private int nextId = 0; + + WorkloadDistributor(final int id) { + this.id = id; + this.running = false; + } + + protected WorkloadThread createThread(final long nanoPerTick) { + final WorkloadThread thread = new WorkloadThread(++this.nextId, nanoPerTick); + this.tasks.put(this.nextId, thread); + if (this.running) + thread.init(this.threadFactory); + return thread; + } + + protected void start(ThreadFactory threadFactory) { + for (WorkloadThread value : this.tasks.values()) { + value.init(threadFactory); + } + this.threadFactory = threadFactory; + this.running = true; + } + + @Override + public int getID() { + return this.id; + } + + @Override + public void cancel() { + for (WorkloadThread value : this.tasks.values()) { + if (!value.isCancelled()) + value.cancel(); + } + this.running = false; + } + + @Override + public boolean isCancelled() { + return this.tasks.values().stream().allMatch(WorkloadThread::isCancelled); + } + + @Override + public boolean isRunning() { + return this.running; + } + + public WorkloadThread getLeastWorker() { + WorkloadThread thread = null; + for (WorkloadThread value : this.tasks.values()) { + if (thread == null || thread.getDeque().size() > value.getDeque().size()) { + thread = value; + } + } + + return thread; + } + + @Override + public void submit(Workload workload) { + this.getLeastWorker().submit(workload); + } + +} diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/WorkloadThread.java b/task-utils/src/main/java/com/pepedevs/corelib/task/WorkloadThread.java new file mode 100644 index 0000000..ad01fdc --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/WorkloadThread.java @@ -0,0 +1,92 @@ +package com.pepedevs.corelib.task; + +import com.pepedevs.corelib.task.threads.RepeatingThread; + +import java.util.Collection; +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadFactory; + +/** A class that stores and computes {@link Workload} instances. */ +public class WorkloadThread implements Runnable, Task { + + private final Queue deque = new ConcurrentLinkedQueue<>(); + + private final long maxNanosPerTick; + private final int workThreadId; + private RepeatingThread thread; + + WorkloadThread(final int workThreadId, final long maxNanosPerTick) { + this.workThreadId = workThreadId; + this.maxNanosPerTick = maxNanosPerTick; + } + + @Override + public void submit(Workload workload) { + deque.add(workload); + } + + public Collection getDeque() { + return Collections.unmodifiableCollection(deque); + } + + public long getMaxNanosPerTick() { + return this.maxNanosPerTick; + } + + @Override + public void run() { + final long stopTime = System.nanoTime() + this.maxNanosPerTick; + final Workload first = this.deque.poll(); + if (first == null) { + return; + } + this.computeWorkload(first); + Workload workload; + while (System.nanoTime() <= stopTime && (workload = this.deque.poll()) != null) { + this.computeWorkload(workload); + if (!first.reSchedule() && first.equals(workload)) { + break; + } + } + } + + private void computeWorkload(final Workload workload) { + if (workload.shouldExecute()) { + workload.compute(); + } + if (workload.reSchedule()) { + this.deque.add(workload); + } + } + + protected void init(ThreadFactory threadFactory) { + this.thread = new RepeatingThread(this.workThreadId, 50, threadFactory, this); + this.thread.run(); + } + + @Override + public int getID() { + return this.workThreadId; + } + + @Override + public void cancel() { + if (this.thread == null || this.thread.isCancelled()) + throw new IllegalStateException("Task already stopped!"); + + this.thread.cancel(); + } + + @Override + public boolean isCancelled() { + return this.thread.isCancelled(); + } + + @Override + public boolean isRunning() { + return this.thread != null && !this.isCancelled(); + } + +} diff --git a/task-utils/src/main/java/com/pepedevs/corelib/task/threads/RepeatingThread.java b/task-utils/src/main/java/com/pepedevs/corelib/task/threads/RepeatingThread.java new file mode 100644 index 0000000..34d0fec --- /dev/null +++ b/task-utils/src/main/java/com/pepedevs/corelib/task/threads/RepeatingThread.java @@ -0,0 +1,86 @@ +package com.pepedevs.corelib.task.threads; + +import com.pepedevs.corelib.task.Task; +import com.pepedevs.corelib.task.Workload; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +public class RepeatingThread implements Runnable, Task { + + protected Future task; + protected final int id; + protected long interval; + protected final ExecutorService executor; + protected final Runnable runnable; + protected boolean cancelled = false; + + public RepeatingThread(int id, long interval, ThreadFactory threadFactory, Runnable runnable) { + this.id = id; + this.interval = interval; + this.executor = Executors.newSingleThreadExecutor(threadFactory); + this.runnable = runnable; + } + + public long getInterval() { + return this.interval; + } + + public synchronized void setInterval(long interval) { + this.interval = interval; + } + + @Override + public int getID() { + return this.id; + } + + @Deprecated + @Override + public void submit(Workload workload) { + + } + + @Override + public void cancel() { + if (this.task == null) + throw new IllegalStateException("Task never started!"); + this.task.cancel(true); + this.cancelled = true; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public boolean isRunning() { + return this.task != null && !this.task.isCancelled(); + } + + @SuppressWarnings("unchecked") + @Override + public void run() { + this.task = (Future) this.executor.submit(() -> { + long nextLoop = System.currentTimeMillis() - this.interval; + while (true) { + try { + nextLoop += this.interval; + long sleep = nextLoop - System.currentTimeMillis(); + if (sleep > 0) Thread.sleep(sleep); + runnable.run(); + } catch (InterruptedException pluginDisabled) { + Thread.currentThread().interrupt(); + break; + } catch (Exception | NoClassDefFoundError e) { + e.printStackTrace(); + } + } + this.executor.shutdown(); + }); + } + +} diff --git a/utils/pom.xml b/utils/pom.xml new file mode 100644 index 0000000..90ea95b --- /dev/null +++ b/utils/pom.xml @@ -0,0 +1,76 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + utils + + + 8 + 8 + + + + + minecraft-repo + https://libraries.minecraft.net/ + + + + + + com.pepedevs + minecraft-version + ${project.parent.version} + + + io.netty + netty-transport + 5.0.0.Alpha2 + provided + + + + com.mojang + authlib + 1.5.25 + provided + + + + com.pepedevs + reflection-utils + ${project.parent.version} + + + + commons-io + commons-io + 2.5 + + + commons-codec + commons-codec + 1.15 + + + + com.github.cryptomorin + XSeries + 8.4.0 + + + + xyz.xenondevs + particle + 1.6.4 + + + + \ No newline at end of file diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/Acceptor.java b/utils/src/main/java/com/pepedevs/corelib/utils/Acceptor.java new file mode 100644 index 0000000..05981c4 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/Acceptor.java @@ -0,0 +1,15 @@ +package com.pepedevs.corelib.utils; + +/** A value tester for accepting/reject values. */ +public interface Acceptor { + + /** + * Tests whether the provided value should be accepted or not. + * + *

+ * + * @param value Value to test. + * @return Whether the provided value is accepted or not. + */ + public boolean accept(T value); +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/Cancellable.java b/utils/src/main/java/com/pepedevs/corelib/utils/Cancellable.java new file mode 100644 index 0000000..4fc7cf8 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/Cancellable.java @@ -0,0 +1,9 @@ +package com.pepedevs.corelib.utils; + +public interface Cancellable { + + void setCancelled(boolean cancelled); + + boolean isCancelled(); + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/ColorUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/ColorUtils.java new file mode 100644 index 0000000..21faaa8 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/ColorUtils.java @@ -0,0 +1,492 @@ +package com.pepedevs.corelib.utils; + +import com.google.common.collect.ImmutableMap; +import org.bukkit.Color; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Utility class to do color parsing or converting work. A color is a either a keyword or a + * numerical RGB specification. + * + *

Color property format preference have the following choices: + * + *

    + *
  • INT_FORMAT, display color as an integer + *
  • HTML_FORMAT (#RRGGBB) + *
  • JAVA_FORMAT (0xRRGGBB) + *
  • CSS_ABSOLUTE_FORMAT (RGB(r,g,b)) + *
  • CSS_RELATIVE_FORMAT (RGB(r%,g%,b%)) + *
+ */ +public class ColorUtils { + + /** + * Color display preference for HTML style: #RRGGBB. + * + *

#RRGGBB + */ + public static final int HTML_FORMAT = 1; + /** Useful constant for Color display preference, display Color as integer. */ + public static final int INT_FORMAT = 0; + /** Color display preference for JAVA style: 0xRRGGBB. */ + public static final int JAVA_FORMAT = 2; + /** Color display preference for CSS absolute style: RGB(r,g,b). */ + public static final int CSS_ABSOLUTE_FORMAT = 3; + /** Color display preference for CSS relative style: RGB(r%,g%,b%). */ + public static final int CSS_RELATIVE_FORMAT = 4; + /** Default format for display preference: CSS_ABSOLUTE_FORMAT. */ + public static final int DEFAULT_FORMAT = CSS_ABSOLUTE_FORMAT; + + public static final Map COLOR_BY_NAME = + new HashMap<>( + ImmutableMap.builder() + .put("WHITE", Color.WHITE) + .put("SILVER", Color.SILVER) + .put("GRAY", Color.GRAY) + .put("BLACK", Color.BLACK) + .put("RED", Color.RED) + .put("MAROON", Color.MAROON) + .put("YELLOW", Color.YELLOW) + .put("OLIVE", Color.OLIVE) + .put("LIME", Color.LIME) + .put("GREEN", Color.GREEN) + .put("AQUA", Color.AQUA) + .put("TEAL", Color.TEAL) + .put("BLUE", Color.BLUE) + .put("NAVY", Color.NAVY) + .put("FUCHSIA", Color.FUCHSIA) + .put("PURPLE", Color.PURPLE) + .put("ORANGE", Color.ORANGE) + .build()); + /** Regular expression for "RGB(255, 0, 0)", integer range 0 - 255 */ + private static final String COLOR_CSS_PATTERN = + "[rR][gG][bB][(]" + + "[\\s]*[\\d]+[\\s]*[,]" + + "[\\s]*[\\d]+[\\s]*[,]" + + "[\\s]*[\\d]+[\\s]*" + + "[)]"; + /** Regular expression for "RGB(255.0%, 0.0%, 0.0%)", float range 0.0% - 100.0% */ + private static final String COLOR_CSS_PERCENT_PATTERN = + "[rR][gG][bB][(]" + + "[\\s]*[\\d]+(.\\d+)?[\\s]*[%][\\s]*[,]" + + "[\\s]*[\\d]+(.\\d+)?[\\s]*[%][\\s]*[,]" + + "[\\s]*[\\d]+(.\\d+)?[\\s]*[%][\\s]*" + + "[)]"; + /** Compiled pattern for CSS absolute pattern: "RGB(255, 0, 0)" */ + private static Pattern cssAbsolutePattern = Pattern.compile(COLOR_CSS_PATTERN); + /** Compiled pattern for CSS relative pattern: "RGB(255%, 0%, 0%)" */ + private static Pattern cssRelativePattern = Pattern.compile(COLOR_CSS_PERCENT_PATTERN); + + /** + * Indicates whether the color value is of valid css absolute format: "RGB(r,g,b)". The RGB + * prefix is case insensitive and r, g, b should be integer value. Whitespace characters are + * allowed around the numerical values. The followings are some cases of valid css absolute + * colors: + * + *

+ * + *

    + *
  • RGB(255,0,0) + *
  • Rgb( 255, 0, 0) + *
  • rgb(300,300,300) + *
+ * + *

+ * + * @param value String color value + * @return true if the color value is in a valid css absolute color representation + */ + public static boolean isCssAbsolute(String value) { + return cssAbsolutePattern.matcher(value).matches(); + } + + /** + * Indicates whether the color value is of a valid css relative format: "RGB( r%, g%, b%)". The + * RGB prefix is case insensitive and r, g, b should be a float value. Whitespace characters are + * allowed around the numerical values. The followings are some cases of valid css relative + * colors: + * + *

+ * + *

    + *
  • RGB(100%,0%,0%) + *
  • Rgb( 100% , 0% , 0% ) + *
  • rgb(200%,200%,200%) + *
+ * + *

+ * + * @param value String color value + * @return true if the color value is in a valid css relative. color representation + */ + public static boolean isCssRelative(String value) { + return cssRelativePattern.matcher(value).matches(); + } + + /** + * Formats an integer RGB value according to the format preference provided. An integer RGB + * value can be formatted as follows: + * + *

    + *
  • INT_FORMAT, display color as an integer. + *
  • HTML_FORMAT (#RRGGBB) + *
  • JAVA_FORMAT (0xRRGGBB) + *
  • CSS_ABSOLUTE_FORMAT (RGB(r,g,b)) + *
  • CSS_RELATIVE_FORMAT (RGB(r%,g%,b%)) + *
+ * + *

The integer value will first be converted into 6-digits hex format(filling "0" from left), + * so only the right most 6 digits will be used. + * + *

+ * + * @param rgbValue Integer RGB value for a color + * @param rgbFormat Color display preference, one of Color display preference constants. For + * example, CSS_ABSOLUTE_FORMAT that will convert into style "RGB(255,0,0)". If the + * preference provided is not in the predefined list, then {@link + * ColorUtils#CSS_ABSOLUTE_FORMAT} will be applied. + * @return String representation of the color in the target format. + * @see ColorUtils#INT_FORMAT + * @see ColorUtils#HTML_FORMAT + * @see ColorUtils#JAVA_FORMAT + * @see ColorUtils#CSS_ABSOLUTE_FORMAT + * @see ColorUtils#CSS_RELATIVE_FORMAT + * @see ColorUtils#DEFAULT_FORMAT + */ + public static String format(int rgbValue, int rgbFormat) { + String rgbText = StringUtils.toRgbText(rgbValue).toUpperCase(); + + switch (rgbFormat) { + case ColorUtils.INT_FORMAT: + return String.valueOf(rgbValue); + case ColorUtils.HTML_FORMAT: + return rgbText; + case ColorUtils.JAVA_FORMAT: + return rgbText.replaceFirst("#", "0x"); + case ColorUtils.CSS_ABSOLUTE_FORMAT: + return hexToRGB(rgbText, true); + case ColorUtils.CSS_RELATIVE_FORMAT: + return hexToRGB(rgbText, false); + default: + return hexToRGB(rgbText, true); + } + } + + /** + * Formats an color value according to the format preference provided. See {@link + * #parseColor(String)}for the allowed color value representations. + * + *

+ * + * @param value Given string containing one of the allowed notation. + * @param rgbFormat Color display preference, one of Color display preference constants. For + * example, CSS_ABSOLUTE_FORMAT that will convert into style "RGB(255,0,0)". If the + * preference provided is not in the predefined list, then {@link + * ColorUtils#CSS_ABSOLUTE_FORMAT}will be applied. + * @return String representation of the color in the target format. + * @throws NumberFormatException if the String representing a numerical value does + * not contain a parsable integer. + * @see ColorUtils#INT_FORMAT + * @see ColorUtils#HTML_FORMAT + * @see ColorUtils#JAVA_FORMAT + * @see ColorUtils#CSS_ABSOLUTE_FORMAT + * @see ColorUtils#CSS_RELATIVE_FORMAT + * @see ColorUtils#DEFAULT_FORMAT + * @see #parseColor(String) + * @see #format(int, int) + */ + public static String format(String value, int rgbFormat) { + int rgbValue = parseColor(value); + + if (rgbValue != -1) return format(rgbValue, rgbFormat); + + return null; + } + + /** + * Convert an hex color text into an css absolute or relative format. For example, "#FF0000" to + * "RGB(255, 0, 0) or to "RGB(100.0%, 0%, 0%)s". + * + *

+ * + * @param hexColor Hex color text + * @param isAbsolute if true, return will be in css absolute format, e.g, RGB(255,0,0); + * if false, return will be in css relative format, e.g, RGB(100.0%,0%,0%). + * @return Formatted css relative or absolute color format. + */ + private static String hexToRGB(String hexColor, boolean isAbsolute) { + assert hexColor.length() == 7; + + // convert from "#FF0000" to "RGB(255, 0, 0)" or to "RGB(100.0%, 0%, 0%)" + String r = hexColor.substring(1, 3); + String g = hexColor.substring(3, 5); + String b = hexColor.substring(5, 7); + + r = Integer.valueOf(r, 16).toString(); + g = Integer.valueOf(g, 16).toString(); + b = Integer.valueOf(b, 16).toString(); + + StringBuilder sb = new StringBuilder(); + + if (isAbsolute) { + sb.append("RGB(").append(r).append(","); + sb.append(g).append(","); + sb.append(b).append(")"); + } else { + int r_iValue = Integer.valueOf(r).intValue(); + int g_iValue = Integer.valueOf(g).intValue(); + int b_iValue = Integer.valueOf(b).intValue(); + + // round to 1 digit after the comma + float r_fValue = ((int) (r_iValue * 10 * 100 / 255f + 0.5f)) / 10.0f; + float g_fValue = ((int) (g_iValue * 10 * 100 / 255f + 0.5f)) / 10.0f; + float b_fValue = ((int) (b_iValue * 10 * 100 / 255f + 0.5f)) / 10.0f; + + sb.append("RGB(").append(String.valueOf(r_fValue)).append("%,"); + sb.append(String.valueOf(g_fValue)).append("%,"); + sb.append(String.valueOf(b_fValue)).append("%)"); + } + + return sb.toString(); + } + + /** + * Parse a valid css color expressed in absolute or relative format as "RGB(r%,g%,b%)" or + * "RGB(r,g,b)" and return the integer value of the color. + * + *

+ * + * @param rgbColor Input color value of a valid css relative or absolute format. + * @return Integer value of the color representation. Return -1 if the string is + * not in a valid absolute or relative representation. + * @see #isCssAbsolute(String) + * @see #isCssRelative(String) + */ + private static int parseRGBColor(String rgbColor) { + // not valid, return -1. + + if (!isCssAbsolute(rgbColor) && !isCssRelative(rgbColor)) return -1; + + boolean hasPercentage = false; + + int start = rgbColor.indexOf('('); + int end = rgbColor.indexOf(')'); + + // Cut from "rgb(255,0,0)" to "255, 0, 0", rgb(100%,100%,100%) to "100%,100%,100%" + String subStr1 = rgbColor.substring(start + 1, end).trim(); + + if (subStr1.indexOf('%') != -1) { + // get rid of '%' + subStr1 = subStr1.replace('%', ' '); + hasPercentage = true; + } + + // Split into 3 strings, we got {255,0,0} + String[] numbers = subStr1.split(","); // $NON-NLS-1$ + + // #FF0000 + StringBuilder colorValue = new StringBuilder("#"); // $NON-NLS-1$ + + // parse String(base 10) into String(base 16) + + for (int i = 0; i < 3; i++) { + int intValue = 0; + + String number = numbers[i].trim(); + + if (hasPercentage) { + float value = Float.parseFloat(number); + + if (value > 100.0f) value = 100.0f; + + // 100.0% => 255, 0.0% => 0 + intValue = (int) (value * 255.0f / 100.0f + 0.5f); + } else { + intValue = Integer.parseInt(number); + + // e,g. clip "300" to "255" + if (intValue > 255) intValue = 255; + } + + // Fill "f" to "0f" + String strValue = "0" + Integer.toHexString(intValue); // $NON-NLS-1$ + strValue = strValue.substring(strValue.length() - 2); + + colorValue.append(strValue); + } + + return Integer.decode(colorValue.toString()).intValue(); + } + + /** + * Parses the string color value as a color keyword or a numerical RGB notation, return its + * corresponding rgb integer value. The string value can be one of the followings: + * + *

+ * + *

    + *
  • A decimal number: "16711680". The number will be clipped into 0~#FFFFFF + *
  • A hexadecimal number in HTML format. '#' immediately followed by either three or six + * hexadecimal characters. The three-digit RGB notation (#rgb) is converted into six-digit + * form (#rrggbb) by replicating digits, not by adding zeros. For example, #fb0 expands to + * #ffbb00. Values outside "#FFFFFF" will be clipped. + *
  • A hexadecimal number in Java format: "0xRRGGBB" + *
  • A css predefined color name: "red", "green", "yellow" etc. + *
  • A css absolute or relative notation: "rgb(r,g,b)" or "rgb(r%,g%,b%)". 'rgb(' followed + * by a comma-separated list of three numerical values (either three integer values in the + * range of 0-255, or three percentage values in the range of 0.0% to 100.0%) followed by + * '), Values outside the numerical ranges will be clipped. Whitespace characters are + * allowed around the numerical values. + *
+ * + *

These examples given a allowed color value that can be parsed into an integer. + * + *

    + *
  • "16711680" + *
  • "#FFFF00" + *
  • "#FFFF00F", will be clipped into "#FFFFFF" + *
  • "0xFF00FF" + *
  • "red" or "green" + *
  • rgb(255,0,0) + *
  • rgb(100.0%,0%,0%) + *
+ * + *

+ * + * @param value Given string containing one of the allowed notation. + * @return Integer value of the color, return -1 if the value is not in one of the + * allowed format. If the value is in a valid integer format, return value will be clipped + * to 0 ~ 0xFFFFFF + */ + public static int parseColor(String value) { + if (StringUtils.isBlank(value)) return -1; + + // 1. Is the value a hexadecimal number or decimal number. It can be + // six-digit form (#rrggbb) or three-digit form (#rgb) or java style + // as (0xRRGGBB ) + int first = value.charAt(0); + + if (first == '#' || (first >= '0' && first <= '9')) { + if (first == '#' && value.length() == 4) { + // #RGB + char[] rgb_chars = value.toCharArray(); + char[] rrggbb_chars = { + '#', + rgb_chars[1], + rgb_chars[1], + rgb_chars[2], + rgb_chars[2], + rgb_chars[3], + rgb_chars[3] + }; + + value = String.valueOf(rrggbb_chars); + } + + try { + int retValue = Integer.decode(value).intValue(); + + return Math.min(retValue, 0xFFFFFF); + + } catch (NumberFormatException e) { + return -1; + } + } + + // // 2. Is this a predefined color? + // + // int rgbValue = parsePredefinedColor( value ); + // if ( rgbValue != -1 ) + // return rgbValue; + + // 3. CSS absolute or relative format: {rgb(r,g,b)} or {rgb(r%,g%,b%)} + + if (isCssAbsolute(value) || isCssRelative(value)) return parseRGBColor(value); + + return -1; + } + + public static Color parseToBukkitColor(String s) { + String[] parts = s.split(";"); + if (parts.length != 3) { + Color color; + if (s.charAt(0) == '#' && s.length() >= 7) { // Hex color + try { + color = Color.fromRGB(Integer.parseUnsignedInt(s.substring(1, 7), 16)); + } catch (IllegalArgumentException e) { + color = null; + } + } else { + color = COLOR_BY_NAME.get(s.toUpperCase()); + } + if (color == null) { + throw new IllegalStateException( + "Invalid color \"" + s + "\", use \"R;G;B\", \"#RRGGBB\" or color value!"); + } else return color; + } else { + return Color.fromRGB( + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2])); + } + } + + /** + * Returns the Red, Blue, Green value for a integer RGB color value. The given RGB value should + * be in the scope of (0~0xFFFFFF), otherwise return null. + * + *

+ * + * @param rgbValue Given integer RGB color value. + * @return Array containing Red, Blue, Green separately. Return null if the value + * is not in (0~0xFFFFFF). + */ + public static int[] getRGBs(int rgbValue) { + if (rgbValue > 0xFFFFFF || rgbValue < 0) return null; + + int r = rgbValue >> 16; + int g = (rgbValue >> 8) & 0xFF; + int b = rgbValue & 0xFF; + + return new int[] {r, g, b}; + } + + /** + * Returns the Red, Blue, Green value for a color value. The given string containing one of the + * allowed notations. + * + *

+ * + * @param colorValue Given string color value in one of the allowed notations. + * @return Array containing Red, Blue, Green separately. Return null if the given + * color value is not parsable. + */ + public static int[] getRGBs(String colorValue) { + int rgb = parseColor(colorValue); + return rgb == -1 ? null : getRGBs(rgb); + } + + /** + * Calculates the integer color value given its red, blue, green values. If any color factor is + * over 0xFF, it will be clipped to 0xFF; if any color factor is below 0, it will be increased + * to 0. + * + *

+ * + * @param r Red value. + * @param g Green value. + * @param b Blue value. + * @return Integer color value of the given color factors. + */ + public static int formRGB(int r, int g, int b) { + // clip to 0 ~ 0xFF + r = r > 0xFF ? 0xFF : Math.max(r, 0); + g = g > 0xFF ? 0xFF : Math.max(g, 0); + b = b > 0xFF ? 0xFF : Math.max(b, 0); + + return (r << 16) + (g << 8) + b; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/Duration.java b/utils/src/main/java/com/pepedevs/corelib/utils/Duration.java new file mode 100644 index 0000000..1bb08bf --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/Duration.java @@ -0,0 +1,287 @@ +package com.pepedevs.corelib.utils; + +import java.util.concurrent.TimeUnit; + +/** A duration that can be converted to milliseconds, seconds, minutes, hours or days. */ +public class Duration { + + /** Represents the zero in the {@link Duration} */ + public static final Duration ZERO = new Duration(0L, TimeUnit.NANOSECONDS); + + protected long duration; + protected TimeUnit unit; + + /** + * Construct duration. + * + *

+ * + * @param duration Duration + * @param unit Time unit + */ + public Duration(long duration, TimeUnit unit) { + if (duration <= 0L || unit == null) { + this.duration = 0L; + this.unit = TimeUnit.NANOSECONDS; + } else { + this.duration = duration; + this.unit = unit; + } + } + + /** + * Construct duration from milliseconds. + * + *

+ * + * @param millis Duration in milliseconds. + */ + public Duration(long millis) { + this(millis, TimeUnit.MILLISECONDS); + } + + /** + * Returns a new {@link Duration} that has been created using the given time unit. + * + *

+ * + * @param unit Time unit of the duration + * @param duration the time + * @return {@link Duration} in the given unit + */ + public static Duration of(TimeUnit unit, long duration) { + switch (unit) { + case DAYS: + return ofDays(duration); + case HOURS: + return ofHours(duration); + case MICROSECONDS: + return ofMicroseconds(duration); + case MILLISECONDS: + return ofMilliseconds(duration); + case MINUTES: + return ofMinutes(duration); + case NANOSECONDS: + return ofNanos(duration); + case SECONDS: + return ofSeconds(duration); + } + return of(TimeUnit.NANOSECONDS, 0L); + } + + /** + * Returns a new {@link Duration} that has been created using nanoseconds. + * + *

+ * + * @param nanos The nanoseconds + * @return {@link Duration} in nanoseconds + */ + public static Duration ofNanos(long nanos) { + return new Duration(nanos, TimeUnit.NANOSECONDS); + } + + /** + * Returns a new {@link Duration} that has been created using microseconds. + * + *

+ * + * @param micros The microseconds + * @return {@link Duration} in microseconds + */ + public static Duration ofMicroseconds(long micros) { + return new Duration(micros, TimeUnit.MICROSECONDS); + } + + /** + * Returns a new {@link Duration} that has been created using milliseconds. + * + *

+ * + * @param millis The milliseconds + * @return {@link Duration} in milliseconds + */ + public static Duration ofMilliseconds(long millis) { + return new Duration(millis, TimeUnit.MILLISECONDS); + } + + /** + * Returns a new {@link Duration} that has been created using milliseconds. + * + *

+ * + * @param seconds The seconds + * @return {@link Duration} in milliseconds + */ + public static Duration ofSeconds(long seconds) { + return new Duration(seconds, TimeUnit.SECONDS); + } + + /** + * Returns a new {@link Duration} that has been created using minutes. + * + *

+ * + * @param minutes The minutes + * @return {@link Duration} in minutes + */ + public static Duration ofMinutes(long minutes) { + return new Duration(minutes, TimeUnit.MINUTES); + } + + /** + * Returns a new {@link Duration} that has been created using hours. + * + *

+ * + * @param hours The hours + * @return {@link Duration} in hours + */ + public static Duration ofHours(long hours) { + return new Duration(hours, TimeUnit.HOURS); + } + + /** + * Returns a new {@link Duration} that has been created using days. + * + *

+ * + * @param days The days + * @return {@link Duration} in days + */ + public static Duration ofDays(long days) { + return new Duration(days, TimeUnit.DAYS); + } + + /** + * Returns the original duration. + * + *

+ * + * @return Original duration + */ + public long getDuration() { + return duration; + } + + /** + * Returns the time unit. + * + *

+ * + * @return Time unit + */ + public TimeUnit getUnit() { + return unit; + } + + /** + * Returns the duration converted to nanoseconds. + * + *

+ * + * @return Duration in nanoseconds + */ + public long toNanos() { + return unit.toNanos(duration); + } + + /** + * Returns the duration converted to microseconds. + * + *

+ * + * @return Duration in microseconds + */ + public long toMicros() { + return unit.toMicros(duration); + } + + /** + * Returns the duration converted to milliseconds. + * + *

+ * + * @return Duration in milliseconds + */ + public long toMillis() { + return unit.toMillis(duration); + } + + /** + * Returns the duration converted to seconds. + * + *

+ * + * @return Duration in seconds + */ + public long toSeconds() { + return unit.toSeconds(duration); + } + + /** + * Returns the duration converted to minutes. + * + *

+ * + * @return Duration in minutes + */ + public long toMinutes() { + return unit.toMinutes(duration); + } + + /** + * Returns the duration converted to hours. + * + *

+ * + * @return Duration in hours + */ + public long toHours() { + return unit.toHours(duration); + } + + /** + * Returns the duration converted to days. + * + *

+ * + * @return Duration in days + */ + public long toDays() { + return unit.toDays(duration); + } + + /** + * Returns true if this is zero. + * + *

+ * + * @return true if duration is zero, else false + */ + public boolean isZero() { + return ZERO.equals(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (duration ^ (duration >>> 32)); + result = prime * result + ((unit == null) ? 0 : unit.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else { + if (obj instanceof Duration) { + Duration other = (Duration) obj; + return other.duration == duration && unit == other.unit; + } + return false; + } + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/FileUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/FileUtils.java new file mode 100644 index 0000000..50275cd --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/FileUtils.java @@ -0,0 +1,85 @@ +package com.pepedevs.corelib.utils; + +import org.apache.commons.lang.Validate; + +import java.io.File; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Set; + +/** Class for dealing with files. */ +public class FileUtils extends org.apache.commons.io.FileUtils { + + /** + * Gets all the elements within desired {@code directory}, including the files within the + * sub-directories of the desired directory. + * + *

+ * + * @param directory Desired directory + * @return All the elements within the directory and its sub-directories + */ + public static Set getElements(File directory) { + Validate.isTrue(directory.isDirectory(), "the provided file is not a valid directory!"); + + final Set elements = new HashSet<>(); + for (File file : directory.listFiles()) { + if (file.isDirectory()) { + elements.addAll(getElements(file)); // recursive. + } else { + elements.add(file); + } + } + return elements; + } + + /** + * Gets the length of the provided {@code directory}. The returned value represents the sum of + * the lengths of all the elements within the desired directory. + * + *

Also if the provided {@link File} is a file ({@link File#isFile()}), then the returned + * value will be the length the that file. + * + *

+ * + * @param directory Desired directory + * @return Sum of the sum of the lengths of all the elements within the directory + */ + public static long getLength(File directory) { + if (directory.isFile()) { + return directory.length(); + } + + final Set elements = getElements(directory); + long length = 0L; + return elements.stream() + .map((e) -> e.length()) + .reduce(length, (accumulator, _item) -> accumulator + _item); + } + + /** + * Get the file type + * + *

+ * + * @param file File to get type of + * @return File type + */ + public static String getFileType(File file) { + try { + String mimetype = Files.probeContentType(file.toPath()); + + if (mimetype != null) { + if (mimetype.contains("/")) { + return mimetype.split("/")[0]; + } else { + return mimetype; + } + } + } catch (Throwable ex) { + ex.printStackTrace(); + } + + return ""; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/Initializable.java b/utils/src/main/java/com/pepedevs/corelib/utils/Initializable.java new file mode 100644 index 0000000..95f6b7c --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/Initializable.java @@ -0,0 +1,14 @@ +package com.pepedevs.corelib.utils; + +/** Simple interface that represents Objects that can be initialized. */ +public interface Initializable { + + /** + * Gets whether this Object is initialized. + * + *

+ * + * @return true if initialized + */ + public boolean isInitialized(); +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/Pair.java b/utils/src/main/java/com/pepedevs/corelib/utils/Pair.java new file mode 100644 index 0000000..3e78381 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/Pair.java @@ -0,0 +1,29 @@ +package com.pepedevs.corelib.utils; + +public class Pair { + + private K key; + private V value; + + public Pair(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public void setKey(K key) { + this.key = key; + } + + public V getValue() { + return value; + } + + public void setValue(V value) { + this.value = value; + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/PlayerBlockAction.java b/utils/src/main/java/com/pepedevs/corelib/utils/PlayerBlockAction.java new file mode 100644 index 0000000..c721ca6 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/PlayerBlockAction.java @@ -0,0 +1,93 @@ +package com.pepedevs.corelib.utils; + +import org.bukkit.event.block.Action; + +/** + * Class for dealing with {@link Action} + * + *

Implements methods like {@link #isLeftClick()} that makes validations easier. + */ +public enum PlayerBlockAction { + + /** Left-clicking a block */ + LEFT_CLICK_BLOCK, + + /** Right-clicking a block */ + RIGHT_CLICK_BLOCK, + + /** Left-clicking the air */ + LEFT_CLICK_AIR, + + /** Right-clicking the air */ + RIGHT_CLICK_AIR, + + /** + * Stepping onto or into a block (Ass-pressure) + * + *

Examples: + * + *

    + *
  • Jumping on soil + *
  • Standing on pressure plate + *
  • Triggering redstone ore + *
  • Triggering tripwire + *
+ */ + PHYSICAL; + + /** + * Gets the corresponding {@link PlayerBlockAction}. + * + *

+ * + * @param action Action to get from + * @return The corresponding {@link PlayerBlockAction} + */ + public static PlayerBlockAction ofBukkit(Action action) { + return PlayerBlockAction.valueOf(action.name()); + } + + /** + * Gets whether this is left click (regardless of whether a block or air was clicked). + * + *

+ * + * @return Whether this is left click + */ + public boolean isLeftClick() { + return this == LEFT_CLICK_AIR || this == LEFT_CLICK_BLOCK; + } + + /** + * Gets whether this is right click (regardless of whether a block or air was clicked). + * + *

+ * + * @return Whether this is right click + */ + public boolean isRightClick() { + return this == RIGHT_CLICK_AIR || this == RIGHT_CLICK_BLOCK; + } + + /** + * Gets whether this is a physical action. + * + *

+ * + * @return Whether this is a physical action + */ + public boolean isPhysical() { + return this == PHYSICAL; + } + + /** + * Gets the corresponding {@link Action}. + * + *

+ * + * @return The corresponding {@link Action} + */ + public Action toBukkit() { + return Action.valueOf(name()); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/PluginHandler.java b/utils/src/main/java/com/pepedevs/corelib/utils/PluginHandler.java new file mode 100644 index 0000000..f490773 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/PluginHandler.java @@ -0,0 +1,122 @@ +package com.pepedevs.corelib.utils; + +import org.bukkit.Bukkit; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Class for handling {@link Plugin}. Also this is an implementation of {@link Listener}, so the + * events in this class can be registered by using {@link #register()} or unregister by using {@link + * #unregister()}. + * + *

Also {@link #isAllowMultipleInstances()} allows the developer to protect its handler from the + * creation of more than one instance of it. Also {@link #isAllowMultipleInstances()} or the class + * extending {@link PluginHandler} must be {@code final} to prevent overriding. + */ +public abstract class PluginHandler implements Listener { + + /** Map for storing handler instances. */ + protected static final Map, PluginHandler>> HANDLER_INSTANCES = + new ConcurrentHashMap<>(); + /** The handling plugin */ + protected final Plugin plugin; + + /** + * Constructs the plugin handler. + * + *

+ * + * @param plugin The plugin to handle. + */ + public PluginHandler(Plugin plugin) { + if (!this.isAllowMultipleInstances()) { + if (this.isSingleInstanceForAllPlugin()) { + for (Map, PluginHandler> value : HANDLER_INSTANCES.values()) { + if (value.containsKey(this.getClass())) + throw new IllegalStateException( + "Cannot create more than one instance of this handler!"); + } + } else { + Map, PluginHandler> value = HANDLER_INSTANCES.getOrDefault(plugin, null); + if (value != null && value.containsKey(this.getClass())) + throw new IllegalStateException( + "Cannot create more than one instance of this handler!"); + } + } + + this.plugin = plugin; + Map, PluginHandler> handlers = HANDLER_INSTANCES.getOrDefault(this.plugin, new ConcurrentHashMap<>()); + handlers.put(this.getClass(), this); + PluginHandler.HANDLER_INSTANCES.put(plugin, handlers); + } + + /** + * This method provides fast access to the plugin handler that has provided the given class. + * + *

+ * + * @param Class that extends PluginHandler + * @param clazz Class desired + * @return Instance of the plugin handle that provided the class + */ + public static PluginHandler getPluginHandler(Plugin plugin, Class clazz) { + return HANDLER_INSTANCES.get(plugin).get(clazz); + } + + /** + * This method provides fast access to the plugin handler that has provided the given class. + * + *

+ * + * @param Class that extends PluginHandler + * @param clazz Class desired + * @return Instance of the plugin handle that provided the class + */ + public static PluginHandler getSingletonHandler(Class clazz) { + for (Map, PluginHandler> value : HANDLER_INSTANCES.values()) { + if (value.containsKey(clazz)) + return value.get(clazz); + } + + return null; + } + + /** + * Gets the plugin this handler handles. + * + *

+ * + * @return The handling plugin. + */ + public Plugin getPlugin() { + return plugin; + } + + /** + * Gets whether this handler allows the creation of more than one instance of it. + * + *

This is useful to avoid users to create instances of this handler. + * + *

+ * + * @return true to allow more than one instance + */ + protected abstract boolean isAllowMultipleInstances(); + + protected abstract boolean isSingleInstanceForAllPlugin(); + + /** Registers events in this class. */ + protected void register() { + HandlerList.unregisterAll(this); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + /** Unregisters events in this class. */ + protected void unregister() { + HandlerList.unregisterAll(this); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/ServerPropertiesUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/ServerPropertiesUtils.java new file mode 100644 index 0000000..29ad780 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/ServerPropertiesUtils.java @@ -0,0 +1,105 @@ +package com.pepedevs.corelib.utils; + +import com.pepedevs.corelib.utils.reflection.bukkit.BukkitReflection; +import com.pepedevs.corelib.utils.reflection.general.FieldReflection; +import com.pepedevs.corelib.utils.version.Version; + +import java.util.Properties; + +/** Class for dealing with the server properties (Stored in "server.properties" file). */ +public class ServerPropertiesUtils { + + private static final Properties PROPERTIES; + + static { + Properties properties = null; + try { + boolean newest = Version.SERVER_VERSION.isNewerEquals(Version.v1_17_R1); + Object server = BukkitReflection.MINECRAFT_SERVER_GET_SERVER.invoke(null); + Object property_manager = FieldReflection.getValue(server, newest ? "x" : "propertyManager"); + + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_14_R1)) { + Object dedicatedServerProperties = FieldReflection.getValue(property_manager, newest ? "b" : "properties"); + properties = (Properties) FieldReflection.get(dedicatedServerProperties.getClass().getSuperclass(), newest ? "X" : "properties") + .get(dedicatedServerProperties); + } else { + properties = FieldReflection.getValue(property_manager, "properties"); + } + } catch (IllegalAccessException + | IllegalArgumentException + | SecurityException + | NoSuchFieldException ex) { + ex.printStackTrace(); + } + + PROPERTIES = properties; + } + + /** + * Get property from server.properties file with String type + * + *

+ * + * @param key Keyword to get the value + * @param default_value Default value if not found + * @return Value of the key + */ + public static String getStringProperty(String key, String default_value) { + try { + return PROPERTIES.getProperty(key, default_value); + } catch (Throwable ex) { + return default_value; + } + } + + /** + * Get property from server.properties file with Integer type + * + *

+ * + * @param key Keyword to get the value + * @param default_value Default value if not found + * @return Value of the key + */ + public static int getIntProperty(String key, int default_value) { + try { + return Integer.parseInt(PROPERTIES.getProperty(key, String.valueOf(default_value))); + } catch (Throwable ex) { + return default_value; + } + } + + /** + * Get property from server.properties file with Long type + * + *

+ * + * @param key Keyword to get the value + * @param default_value Default value if not found + * @return Value of the key + */ + public static long getLongProperty(String key, long default_value) { + try { + return Long.parseLong(PROPERTIES.getProperty(key, String.valueOf(default_value))); + } catch (Throwable ex) { + return default_value; + } + } + + /** + * Get property from server.properties file with Boolean type + * + *

+ * + * @param key Keyword to get the value + * @param default_value Default value if not found + * @return Value of the key + */ + public static boolean getBooleanProperty(String key, boolean default_value) { + try { + return Boolean.parseBoolean(PROPERTIES.getProperty(key, String.valueOf(default_value))); + } catch (Throwable ex) { + return default_value; + } + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/StringUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/StringUtils.java new file mode 100644 index 0000000..30a7e86 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/StringUtils.java @@ -0,0 +1,557 @@ +package com.pepedevs.corelib.utils; + +import com.google.common.base.Strings; +import org.bukkit.ChatColor; +import org.bukkit.NamespacedKey; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +/** Implements {@link org.apache.commons.lang.StringUtils} */ +public class StringUtils extends org.apache.commons.lang.StringUtils { + + public static final String LINE_SEPARATOR = System.lineSeparator(); + + /** + * Converts integer to an HTML RGB value + * + *

+ * + * @param rgb RGB (0 ~ 0xFFFFFF) + * @return Hex (#FFFFFF) + */ + public static String toRgbText(int rgb) { + if (rgb > 16777215) rgb = 16777215; + + if (rgb < 0) rgb = 0; + + String str = "000000" + Integer.toHexString(rgb); + + return "#" + str.substring(str.length() - 6); + } + + /** + * Limits a string to provided number of maximum characters + * + *

+ * + * @param string String to limit + * @param max_length Max length to limit + * @return Formatted string + */ + public static String limit(String string, int max_length) { + return (string.length() > max_length) ? string.substring(0, max_length) : string; + } + + /** + * Concatenates the provided {@link ChatColor} array, making it useful to be concatenated to any + * string. + * + *

+ * + * @param colors ColorUtils + * @return Single color line + */ + public static String concatenate(ChatColor... colors) { + StringBuilder builder = new StringBuilder(); + + for (int x = 0; x < 2; x++) { + boolean apply_colors = x == 0; + + for (ChatColor color : colors) { + if (apply_colors ? color.isColor() : color.isFormat()) { + builder.append(color.toString()); + } + } + } + return builder.toString(); + } + + /** + * Colorizes the provided {@link String}. + * + *

+ * + * @param colors {@link ChatColor} to apply + * @param string String to colorize + * @return Colorized string + */ + public static String colorize(ChatColor[] colors, String string) { + return concatenate(colors) + string; + } + + /** + * Colorizes the provided {@link String} + * + *

+ * + * @param string String to colorize + * @param colors {@link ChatColor} to apply + * @return Colorized string + */ + public static String colorize(String string, ChatColor... colors) { + return colorize(colors, string); + } + + /** + * Translates a string using an alternate color code character into a string that uses the + * internal {@link ChatColor#COLOR_CHAR} color code character. The alternate color code + * character will only be replaced if it is immediately followed by 0-9, A-F, a-f, K-O, k-o, R + * or r. + * + *

+ * + * @param alt_char The alternate color code character to replace. Ex: {@literal &} + * @param string Text containing the alternate color code character + * @return Text containing the ChatColor.COLOR_CODE color code character + * @see ChatColor#translateAlternateColorCodes(char, String) + */ + public static String translateAlternateColorCodes(char alt_char, String string) { + return ChatColor.translateAlternateColorCodes(alt_char, string); + } + + /** + * Translates a string using the alternate color code character '{@literal &}' into a string + * that uses the internal {@link ChatColor#COLOR_CHAR} color code character. The alternate color + * code character will only be replaced if it is immediately followed by 0-9, A-F, a-f, K-O, + * k-o, R or r. + * + *

+ * + * @param string Text containing the alternate color code character + * @return Text containing the ChatColor.COLOR_CODE color code character + * @see #translateAlternateColorCodes(char, String) + */ + public static String translateAlternateColorCodes(String string) { + return translateAlternateColorCodes('&', string); + } + + /** + * Translates the strings of the provided array using an alternate color code character into a + * string that uses the internal {@link ChatColor#COLOR_CHAR} color code character. The + * alternate color code character will only be replaced if it is immediately followed by 0-9, + * A-F, a-f, K-O, k-o, R or r. + * + *

+ * + * @param alt_char The alternate color code character to replace. Ex: {@literal &} + * @param array StringUtils array to translate + * @return New strings array containing the translated strings + */ + public static String[] translateAlternateColorCodes(char alt_char, String[] array) { + String[] copy = new String[array.length]; + + for (int i = 0; i < copy.length; i++) + copy[i] = translateAlternateColorCodes(alt_char, array[i]); + + return copy; + } + + /** + * Translates the strings of the provided array using the alternate color code character + * '{@literal &}' into a string that uses the internal {@link ChatColor#COLOR_CHAR} color code + * character. The alternate color code character will only be replaced if it is immediately + * followed by 0-9, A-F, a-f, K-O, k-o, R or r. + * + *

+ * + * @param array StringUtils collection to translate + * @return New strings array containing the translated strings + */ + public static String[] translateAlternateColorCodes(String[] array) { + return translateAlternateColorCodes('&', array); + } + + /** + * Translates the strings of the provided collection using an alternate color code character + * into a string that uses the internal {@link ChatColor#COLOR_CHAR} color code character. The + * alternate color code character will only be replaced if it is immediately followed by 0-9, + * A-F, a-f, K-O, k-o, R or r. + * + *

+ * + * @param alt_char The alternate color code character to replace. Ex: {@literal &} + * @param collection the collection of strings to translate + * @return New {@link List} of string containing the translated strings + */ + public static List translateAlternateColorCodes( + char alt_char, Collection collection) { + List list = new ArrayList<>(); + + for (String string : collection) list.add(translateAlternateColorCodes(alt_char, string)); + + return list; + } + + /** + * Translates the strings of the provided collection using the alternate color code character + * '{@literal &}' into a string that uses the internal {@link ChatColor#COLOR_CHAR} color code + * character. The alternate color code character will only be replaced if it is immediately + * followed by 0-9, A-F, a-f, K-O, k-o, R or r. + * + *

+ * + * @param collection Collection of strings to translate + * @return New {@link List} of string containing the translated strings + */ + public static List translateAlternateColorCodes(Collection collection) { + return translateAlternateColorCodes('&', collection); + } + + /** + * Translates the strings of the provided list using an alternate color code character into a + * string that uses the internal {@link ChatColor#COLOR_CHAR} color code character. The + * alternate color code character will only be replaced if it is immediately followed by 0-9, + * A-F, a-f, K-O, k-o, R or r. + * + *

+ * + * @param alt_char The alternate color code character to replace. Ex: {@literal &} + * @param list List of strings to translate + * @return {@link List} containing the translated strings + */ + public static List translateAlternateColorCodes(char alt_char, List list) { + for (int i = 0; i < list.size(); i++) + list.set(i, translateAlternateColorCodes(alt_char, list.get(i))); + + return list; + } + + /** + * Translates the strings of the provided list using the alternate color code character + * '{@literal &}' into a string that uses the internal {@link ChatColor#COLOR_CHAR} color code + * character. The alternate color code character will only be replaced if it is immediately + * followed by 0-9, A-F, a-f, K-O, k-o, R or r. + * + *

+ * + * @param list List of strings to translate + * @return {@link List} containing the translated strings + */ + public static List translateAlternateColorCodes(List list) { + return translateAlternateColorCodes('&', list); + } + + /** + * Replaces the Bukkit internal color character {@link ChatColor#COLOR_CHAR} by the provided + * character. + * + *

+ * + * @param alt_char Character replacer + * @param string Target string + * @return Formatted string + */ + public static String untranslateAlternateColorCodes(char alt_char, String string) { + char[] contents = string.toCharArray(); + + for (int i = 0; i < contents.length; i++) { + if (contents[i] == '\'') contents[i] = alt_char; + } + + return new String(contents); + } + + /** + * Replaces the Bukkit internal color character {@link ChatColor#COLOR_CHAR} by the well known + * color character '{@literal &}'. + * + *

+ * + * @param string Target string + * @return Formatted string + */ + public static String untranslateAlternateColorCodes(String string) { + return untranslateAlternateColorCodes('&', string); + } + + /** + * Replaces from the strings of the provided array the Bukkit internal color character {@link + * ChatColor#COLOR_CHAR} by the provided character. + * + *

+ * + * @param alt_char Character replacer + * @param array StringUtils array to replace + * @return StringUtils array containing the processed strings + */ + public static String[] untranslateAlternateColorCodes(char alt_char, String[] array) { + String[] copy = new String[array.length]; + + for (int i = 0; i < copy.length; i++) + copy[i] = untranslateAlternateColorCodes(alt_char, array[i]); + + return copy; + } + + /** + * Replaces from the strings of the provided array the Bukkit internal color character {@link + * ChatColor#COLOR_CHAR} by the well known color character '{@literal &}'. + * + *

+ * + * @param array StringUtils array to replace + * @return StringUtils array containing the processed strings + */ + public static String[] untranslateAlternateColorCodes(String[] array) { + return untranslateAlternateColorCodes('&', array); + } + + /** + * Replaces from the strings of the provided collection the Bukkit internal color character + * {@link ChatColor#COLOR_CHAR} by the provided character. + * + *

+ * + * @param alt_char Character replacer + * @param collection StringUtils collection to replace + * @return List containing the processed strings + */ + public static List untranslateAlternateColorCodes( + char alt_char, Collection collection) { + List list = new ArrayList<>(); + + for (String string : collection) list.add(translateAlternateColorCodes(alt_char, string)); + + return list; + } + + /** + * Replaces from the strings of the provided list the Bukkit internal color character {@link + * ChatColor#COLOR_CHAR} by the well known color character '{@literal &}'. + * + *

+ * + * @param collection StringUtils collection to replace + * @return List containing the processed strings + */ + public static List untranslateAlternateColorCodes(Collection collection) { + return untranslateAlternateColorCodes('&', collection); + } + + /** + * Replaces from the strings of the provided collection the Bukkit internal color character + * {@link ChatColor#COLOR_CHAR} by the provided character. + * + *

+ * + * @param alt_char Character replacer + * @param list List of strings to replace + * @return {@link List} containing the processed strings + */ + public static List untranslateAlternateColorCodes(char alt_char, List list) { + for (int i = 0; i < list.size(); i++) + list.set(i, translateAlternateColorCodes(alt_char, list.get(i))); + + return list; + } + + /** + * Replaces from the strings of the provided collection the Bukkit internal color character + * {@link ChatColor#COLOR_CHAR} by the well known color character '{@literal &}'. + * + *

+ * + * @param list List of strings to replace + * @return {@link List} containing the processed strings + */ + public static List untranslateAlternateColorCodes(List list) { + return untranslateAlternateColorCodes('&', list); + } + + /** + * Strips the given string of all colors. + * + *

+ * + * @param string the string to strip of color + * @return Formatted string + */ + public static String stripColors(String string) { + return ChatColor.stripColor(string); + } + + /** + * Fixes excessive whitespaces replacing it with a single whitespace. + * + *

+ * + * @param string String to fix + * @return Formatted string + */ + public static String fixExcessiveWhitespaces(String string) { + return string.replaceAll("\\s{2,}", " "); + } + + /** + * Converts only the characters sequence of the given target to lower case. + * + *

+ * + * @param string {@code String} where the characters to convert are located + * @param target Characters sequence reference + * @return Characters sequence of the given target in the given string converted to lower case + */ + public static String toLowerCase(String string, String target) { + return toLowerCase(string, target, Locale.getDefault()); + } + + /** + * Get a string progress bar for your given data, + * + *

+ * + * @param current The current progress + * @param max Max progress for progress bar + * @param totalBars Number of individual bars in the progress bar + * @param symbol Symbol of the bars in progress bar + * @param completedColor Color of completed bars + * @param notCompletedColor Default color of bars + * @return Progress bar for the given data + * @deprecated Better method mentioned in seeAlso + * @see StringUtils#getProgressBar(double, double, int, String, ChatColor, ChatColor) + */ + @Deprecated + public static String getProgressBar( + int current, + int max, + int totalBars, + char symbol, + ChatColor completedColor, + ChatColor notCompletedColor) { + float percent = (float) current / max; + int progressBars = (int) (totalBars * percent); + + return Strings.repeat("" + completedColor + symbol, progressBars) + + Strings.repeat("" + notCompletedColor + symbol, totalBars - progressBars); + } + + /** + * Get a string progress bar for your given data, + * + *

+ * + * @param current The current progress + * @param max Max progress for progress bar + * @param totalBars Number of individual bars in the progress bar + * @param symbol Symbol of the bars in progress bar + * @param completedColor Color of completed bars + * @param notCompletedColor Default color of bars + * @return Progress bar for the given data + */ + public static String getProgressBar( + double current, + double max, + int totalBars, + String symbol, + ChatColor completedColor, + ChatColor notCompletedColor) { + float percent = (float) ((float) current / max); + int progressBars = (int) (totalBars * percent); + + return Strings.repeat("" + completedColor + symbol, progressBars) + + Strings.repeat("" + notCompletedColor + symbol, totalBars - progressBars); + } + + /** + * Converts only the characters sequence of the given target to lower case, using the given + * {@code Locale} rules. + * + *

+ * + * @param string {@code String} where the characters to convert are located + * @param target Characters sequence reference + * @param locale {@code Locale} rules reference + * @return Characters sequence of the given target in the given string converted to lower case + */ + public static String toLowerCase(String string, String target, Locale locale) { + String lower_case = string.toLowerCase(locale); + String target_lower_case = target.toLowerCase(locale); + + if (!lower_case.contains(target_lower_case)) return lower_case; + + char[] chars = string.toCharArray(); + int last_index = 0; + + for (int i = 0; i < lower_case.length(); i++) { + int current_index = lower_case.indexOf(target_lower_case, last_index); + + if (current_index != -1) { + int end_index = current_index + target_lower_case.length(); + last_index = end_index; + + for (int j = current_index; j < end_index; j++) chars[j] = lower_case.charAt(j); + } + } + + return new String(chars); + } + + /** + * Converts only the characters sequence of the given target to upper case. + * + *

+ * + * @param string {@code String} where the characters to convert are located + * @param target Characters sequence reference + * @return Characters sequence of the given target in the given string converted to upper case + */ + public static String toUpperCase(String string, String target) { + return toUpperCase(string, target, Locale.getDefault()); + } + + /** + * Converts only the characters sequence of the given target to upper case, using the given + * {@code Locale} rules. + * + *

+ * + * @param string {@code String} where the characters to convert are located. + * @param target Characters sequence reference. + * @param locale {@code Locale} rules reference + * @return Characters sequence of the given target in the given string converted to upper case. + */ + public static String toUpperCase(String string, String target, Locale locale) { + String upper_case = string.toUpperCase(locale); + String target_upper_case = target.toUpperCase(locale); + + if (!upper_case.contains(target_upper_case)) return upper_case; + + char[] chars = string.toCharArray(); + int last_index = 0; + + for (int i = 0; i < upper_case.length(); i++) { + int current_index = upper_case.indexOf(target_upper_case, last_index); + + if (current_index != -1) { + int end_index = current_index + target_upper_case.length(); + last_index = end_index; + + for (int j = current_index; j < end_index; j++) chars[j] = upper_case.charAt(j); + } + } + + return new String(chars); + } + + /** + * Converts a raw string to NameSpaced key. + * + *

+ * + * @param raw String to convert + * @return NameSpaced key for the string + */ + public static NamespacedKey parseNameSpacedKey(String raw) { + int sepIndex = raw.indexOf(':'); + if (sepIndex == -1) { + return NamespacedKey.minecraft(raw); + } + @SuppressWarnings("deprecation") // Come on bukkit... + NamespacedKey x = + new NamespacedKey(raw.substring(0, sepIndex), raw.substring(sepIndex + 1)); + return x; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/Validable.java b/utils/src/main/java/com/pepedevs/corelib/utils/Validable.java new file mode 100644 index 0000000..8835822 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/Validable.java @@ -0,0 +1,25 @@ +package com.pepedevs.corelib.utils; + +/** Simple interface for validating Objects. */ +public interface Validable { + + /** + * Gets whether this Object represents a valid instance. + * + *

+ * + * @return true if valid + */ + boolean isValid(); + + /** + * Gets whether this Object represents an invalid instance. + * + *

+ * + * @return true if invalid + */ + default boolean isInvalid() { + return !this.isValid(); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/entity/EntityUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/entity/EntityUtils.java new file mode 100644 index 0000000..cecd9fb --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/entity/EntityUtils.java @@ -0,0 +1,51 @@ +package com.pepedevs.corelib.utils.entity; + +import org.bukkit.World; +import org.bukkit.entity.Entity; + +import java.util.UUID; + +/** Class for dealing with Bukkit {@link Entity} */ +public class EntityUtils { + + /** + * Returns the {@link Entity} associated with the given {@link UUID} and with the given type. + * + *

+ * + * @param Entity type + * @param world World in which the entity is + * @param type Class of the entity type + * @param id UUID of the entity to find + * @return {@link Entity} associated with the given {@link UUID} and with the given type, or + * null if could not be found. + */ + @SuppressWarnings("unchecked") + public static T getEntity( + World world, Class type, UUID id) { + return (T) + world.getEntities().stream() + .filter( + entity -> + type.isAssignableFrom(entity.getClass()) + && id.equals(entity.getUniqueId())) + .findAny() + .orElse(null); + } + + /** + * Returns the {@link Entity} associated with the given {@link UUID}. + * + *

+ * + * @param world World in which the entity is + * @param id UUID of the entity to find + * @return {@link Entity} associated with the given {@link UUID}, or null if could not be found + */ + public static Entity getEntity(World world, UUID id) { + return world.getEntities().stream() + .filter(entity -> id.equals(entity.getUniqueId())) + .findAny() + .orElse(null); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/entity/UUIDEntity.java b/utils/src/main/java/com/pepedevs/corelib/utils/entity/UUIDEntity.java new file mode 100644 index 0000000..cd41ec6 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/entity/UUIDEntity.java @@ -0,0 +1,125 @@ +package com.pepedevs.corelib.utils.entity; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.Optional; +import java.util.UUID; + +/** + * Represents an entity that is identified by using a {@link UUID unique id}. + * + * @param + */ +public class UUIDEntity { + + /** The entity's unique id. */ + protected final UUID uuid; + + /** The entity's type class. */ + protected final Class clazz; + + /** + * Construct the {@code UUIDEntity} from its {@link UUID unique id} and its type class. + * + *

+ * + * @param uuid Entity's {@link UUID unique id} + * @param clazz Type class of the entity + */ + public UUIDEntity(final UUID uuid, final Class clazz) { + this.uuid = uuid; + this.clazz = clazz; + } + + /** + * Construct the {@code UUIDEntity} from its respective {@link Entity entity}. + * + *

+ * + * @param entity Respective entity. + */ + @SuppressWarnings("unchecked") + public UUIDEntity(final Entity entity) { + this(entity.getUniqueId(), (Class) entity.getClass()); + } + + /** + * Gets the entity's {@link UUID unique id}. + * + *

+ * + * @return Entity's {@link UUID unique id}. + */ + public UUID getUniqueId() { + return uuid; + } + + /** + * Gets the entity's type class. + * + *

+ * + * @return entity's type class. + */ + public Class getTypeClass() { + return clazz; + } + + /** + * Gets the respective entity associated with the entity's {@link #uuid}. This will look in all + * the worlds for an entity whose its {@link UUID unique id} is the same as this. + * + *

+ * + * @return Respective entity. + */ + @SuppressWarnings("unchecked") + public T get() { + if (Player.class.equals(getTypeClass())) { + return (T) Bukkit.getPlayer(getUniqueId()); + } + + for (World world : Bukkit.getWorlds()) { + Optional filter = + world.getEntitiesByClass(getTypeClass()).stream() + .filter(entity -> entity.getUniqueId().equals(getUniqueId())) + .findAny(); + if (filter.isPresent()) { + return filter.get(); + } + + // // we do this to avoid ConcurrentModificationException + // synchronized ( world ) { + // List < Entity > entities = world.getEntities ( ); + // for ( int i = 0 ; i < entities.size ( ) ; i ++ ) { + // Entity entity = entities.get ( i ); + // if ( entity.getClass ( ).isAssignableFrom ( getTypeClass ( ) ) + // && entity.getUniqueId ( ).equals ( getUniqueId ( ) ) ) { + // return (T) entity; + // } + // } + // } + } + return null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof UUIDEntity) { + return getUniqueId().equals(((UUIDEntity) obj).getUniqueId()); + } else { + return false; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((uuid == null) ? 0 : uuid.hashCode()); + return result; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/entity/UUIDPlayer.java b/utils/src/main/java/com/pepedevs/corelib/utils/entity/UUIDPlayer.java new file mode 100644 index 0000000..0753060 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/entity/UUIDPlayer.java @@ -0,0 +1,38 @@ +package com.pepedevs.corelib.utils.entity; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.UUID; + +/** An implementation of {@link UUIDEntity} intended for {@link Player} entities only. */ +public class UUIDPlayer extends UUIDEntity { + + /** + * Construct the {@code UUIDPlayer} from its {@link UUID unique id}. + * + *

+ * + * @param uuid Player's {@link UUID unique id}. + */ + public UUIDPlayer(final UUID uuid) { + super(uuid, Player.class); + } + + /** + * Construct the {@code UUIDPlayer} from its respective {@link Player player}. + * + *

+ * + * @param player Respective player. + */ + public UUIDPlayer(final Player player) { + this(player.getUniqueId()); + } + + /** Gets the {@link Player} associated with the {@link UUIDEntity#uuid}. */ + @Override + public final Player get() { + return Bukkit.getPlayer(getUniqueId()); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/entity/spawning/ChunkEntitySpawner.java b/utils/src/main/java/com/pepedevs/corelib/utils/entity/spawning/ChunkEntitySpawner.java new file mode 100644 index 0000000..6d65031 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/entity/spawning/ChunkEntitySpawner.java @@ -0,0 +1,33 @@ +package com.pepedevs.corelib.utils.entity.spawning; + +import org.bukkit.Chunk; + +import java.util.function.Consumer; + +/** Class to deal with entity spawning in chunks */ +public abstract class ChunkEntitySpawner implements Consumer { + + protected final Chunk chunk; + + /** + * Chunk handle for spawning entity. + * + *

+ * + * @param chunk {@link Chunk} + */ + public ChunkEntitySpawner(Chunk chunk) { + this.chunk = chunk; + } + + /** + * Returns the chunk handle + * + *

+ * + * @return {@link Chunk} + */ + public Chunk getChunk() { + return chunk; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/entity/spawning/ChunkEntitySpawnerPool.java b/utils/src/main/java/com/pepedevs/corelib/utils/entity/spawning/ChunkEntitySpawnerPool.java new file mode 100644 index 0000000..9d795b1 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/entity/spawning/ChunkEntitySpawnerPool.java @@ -0,0 +1,90 @@ +package com.pepedevs.corelib.utils.entity.spawning; + +import com.pepedevs.corelib.utils.version.Version; +import org.bukkit.Chunk; + +import java.util.Iterator; +import java.util.Stack; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** Class for managing spawning entities chunk */ +public class ChunkEntitySpawnerPool implements Runnable { + + /** Spawners pool stack. */ + protected final Stack spawners; + + protected ExecutorService executor; + protected boolean started; + + /** Constructs the {@link ChunkEntitySpawnerPool}. */ + public ChunkEntitySpawnerPool() { + this.spawners = new Stack<>(); + } + + /** Starts the chunk entity spawner pool. */ + public void start() { + if (executor == null) { + executor = Executors.newSingleThreadExecutor(); + executor.execute(this); + } + } + + /** Stops the chunk entity spawner pool. */ + public void stop() { + executor.shutdownNow(); + executor = null; + } + + /** + * Checks whether the chunk entity spawner pool is terminated. + * + *

+ * + * @return {@code true} if it is terminated, else false + */ + public boolean isTerminated() { + return started && spawners.size() == 0; + } + + /** + * Submit chunks to spawn entities. + * + *

+ * + * @param spawners {@link ChunkEntitySpawner} + */ + public void submit(ChunkEntitySpawner... spawners) { + for (ChunkEntitySpawner spawner : spawners) { + this.spawners.addElement(spawner); + } + } + + @Override + public void run() { + started = true; // marking as started + + while (spawners.size() > 0) { + Iterator iterator = spawners.iterator(); + + while (iterator.hasNext()) { + ChunkEntitySpawner spawner = iterator.next(); + Chunk chunk = spawner.getChunk(); + + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_14_R1) + ? chunk.isLoaded() + : chunk.getWorld().isChunkInUse(chunk.getX(), chunk.getZ())) { + + spawner.accept(chunk); + iterator.remove(); + } + } + } + + if (executor != null) { + executor.shutdownNow(); + } + + executor = null; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/image/ImageUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/image/ImageUtils.java new file mode 100644 index 0000000..8222d03 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/image/ImageUtils.java @@ -0,0 +1,233 @@ +package com.pepedevs.corelib.utils.image; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +/** Class for dealing with images. */ +public class ImageUtils { + + /** + * Resizes an image to a absolute width and height (the image may not be proportional). + * + *

+ * + * @param inputImagePath Path of the original image + * @param outputImagePath Path to save the resized image + * @param scaledWidth Absolute width in pixels + * @param scaledHeight Absolute height in pixels + * @throws IOException thrown while dealing with I/O functions. + */ + public static void resize( + String inputImagePath, String outputImagePath, int scaledWidth, int scaledHeight) + throws IOException { + // reads input image + File inputFile = new File(inputImagePath); + BufferedImage inputImage = ImageIO.read(inputFile); + + // creates output image + BufferedImage outputImage = + new BufferedImage(scaledWidth, scaledHeight, inputImage.getType()); + + // scales the input image to the output image + Graphics2D g2d = outputImage.createGraphics(); + g2d.drawImage(inputImage, 0, 0, scaledWidth, scaledHeight, null); + g2d.dispose(); + + // extracts extension of output file + String formatName = outputImagePath.substring(outputImagePath.lastIndexOf(".") + 1); + + // writes to output file + ImageIO.write(outputImage, formatName, new File(outputImagePath)); + outputImage.flush(); + inputImage.flush(); + } + + /** + * Resizes an image by a percentage of original size (proportional). + * + *

+ * + * @param inputImagePath Path of the original image + * @param outputImagePath Path to save the resized image + * @param percent Double number specifies percentage of the output image over the input image + * @throws IOException thrown while dealing with I/O functions. + */ + public static void resize(String inputImagePath, String outputImagePath, double percent) + throws IOException { + File inputFile = new File(inputImagePath); + BufferedImage inputImage = ImageIO.read(inputFile); + int scaledWidth = (int) (inputImage.getWidth() * percent); + int scaledHeight = (int) (inputImage.getHeight() * percent); + resize(inputImagePath, outputImagePath, scaledWidth, scaledHeight); + inputImage.flush(); + } + + /** + * Converts the {@link BufferedImage} type. + * + *

+ * + * @param srcImage {@link BufferedImage} to convert + * @param destImgType Type to convert into + * @return Converted image + */ + public static BufferedImage convert(final BufferedImage srcImage, final int destImgType) { + BufferedImage img = + new BufferedImage(srcImage.getWidth(), srcImage.getHeight(), destImgType); + Graphics2D g2d = img.createGraphics(); + g2d.drawImage(srcImage, 0, 0, null); + g2d.dispose(); + return img; + } + + /** + * Read the image from the specified resource + * + * @param image the image resource path + * @return the image as string + */ + public static String readImage(InputStream image) { + StringBuilder builder = new StringBuilder(); + try { + BufferedImage img = ImageIO.read(image); + for (int i = 0; i < img.getHeight(); i++) { + for (int j = 0; j < img.getWidth(); j++) { + Color pixel = new Color(img.getRGB(j, i)); + double pixelValue = + (((pixel.getRed() * 0.30) + + (pixel.getBlue() * 0.59) + + (pixel.getGreen() * 0.11))); + builder.append(strChar(pixelValue)); + } + } + img.flush(); + image.close(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + + return builder.toString(); + } + + /** + * Read the image from the specified resource + * + * @param image the image resource path + * @return the image as string + */ + public static String readImage(File image) { + StringBuilder builder = new StringBuilder(); + try { + BufferedImage img = ImageIO.read(image); + for (int i = 0; i < img.getHeight(); i++) { + for (int j = 0; j < img.getWidth(); j++) { + Color pixel = new Color(img.getRGB(j, i)); + double pixelValue = + (((pixel.getRed() * 0.30) + + (pixel.getBlue() * 0.59) + + (pixel.getGreen() * 0.11))); + builder.append(strChar(pixelValue)); + } + } + img.flush(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + + return builder.toString(); + } + + /** + * Read the image from the specified resource + * + * @param image the image resource path + * @return the image as string + */ + public static String readImage(Path image) { + StringBuilder builder = new StringBuilder(); + try { + BufferedImage img = ImageIO.read(image.toFile()); + for (int i = 0; i < img.getHeight(); i++) { + for (int j = 0; j < img.getWidth(); j++) { + Color pixel = new Color(img.getRGB(j, i)); + double pixelValue = + (((pixel.getRed() * 0.30) + + (pixel.getBlue() * 0.59) + + (pixel.getGreen() * 0.11))); + builder.append(strChar(pixelValue)); + } + } + img.flush(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + + return builder.toString(); + } + + /** + * Read the image from the specified resource + * + * @param imagePath the image resource path + * @return the image as string + */ + public static String readImage(String imagePath) { + StringBuilder builder = new StringBuilder(); + try { + BufferedImage img = + ImageIO.read( + new File(new File(imagePath).getAbsolutePath().replace("%20", " "))); + for (int i = 0; i < img.getHeight(); i++) { + for (int j = 0; j < img.getWidth(); j++) { + Color pixel = new Color(img.getRGB(j, i)); + double pixelValue = + (((pixel.getRed() * 0.30) + + (pixel.getBlue() * 0.59) + + (pixel.getGreen() * 0.11))); + builder.append(strChar(pixelValue)); + } + } + img.flush(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + + return builder.toString(); + } + + /** + * Parse a char value to string + * + * @param g the char value + * @return the char string value + */ + private static String strChar(double g) { + String str; + if (g >= 240) { + str = " "; + } else if (g >= 210) { + str = "."; + } else if (g >= 190) { + str = "*"; + } else if (g >= 170) { + str = "+"; + } else if (g >= 120) { + str = "^"; + } else if (g >= 110) { + str = "&"; + } else if (g >= 80) { + str = "8"; + } else if (g >= 60) { + str = "#"; + } else { + str = "@"; + } + + return str; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/io/GameProfileBuilder.java b/utils/src/main/java/com/pepedevs/corelib/utils/io/GameProfileBuilder.java new file mode 100644 index 0000000..3027c20 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/io/GameProfileBuilder.java @@ -0,0 +1,156 @@ +package com.pepedevs.corelib.utils.io; + +import com.google.gson.*; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.util.UUIDTypeAdapter; +import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; + +public class GameProfileBuilder { + + private static final String SERVICE_URL = "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false"; + private static final String JSON_SKIN = "{\"timestamp\":%d,\"profileId\":\"%s\",\"profileName\":\"%s\",\"isPublic\":true,\"textures\":{\"SKIN\":{\"url\":\"%s\"}}}"; + private static final String JSON_CAPE = "{\"timestamp\":%d,\"profileId\":\"%s\",\"profileName\":\"%s\",\"isPublic\":true,\"textures\":{\"SKIN\":{\"url\":\"%s\"},\"CAPE\":{\"url\":\"%s\"}}}"; + + private final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) + .registerTypeAdapter(GameProfile.class, new GameProfileSerializer()) + .registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()) + .create(); + private HashMap cache = new HashMap<>(); + private long cacheTime = -1; + + private static final GameProfileBuilder INSTANCE = new GameProfileBuilder(); + + public GameProfile fetch(UUID uuid) { + return fetch(uuid, false); + } + + public GameProfile fetch(UUID uuid, boolean forceNew) { + // Check for cached profile + if (!(forceNew) && cache.containsKey(uuid) && cache.get(uuid).isValid()) + return cache.get(uuid).profile; + else { + try { + // Open http connection + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(SERVICE_URL, UUIDTypeAdapter.fromUUID(uuid))).openConnection(); + connection.setReadTimeout(5000); + + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + // Parse response + String json = "", line; + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + + while ((line = bufferedReader.readLine()) != null) + json += line; + + GameProfile result = gson.fromJson(json, GameProfile.class); + bufferedReader.close(); + + // Cache profile + cache.put(uuid, new CachedProfile(result)); + + return result; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); + JsonObject error = (JsonObject) new JsonParser().parse(reader.readLine()); + reader.close(); + } catch (IOException ignored) {} + + return null; + } + } + + public GameProfile getProfile(UUID uuid, String name, String skin) { + return getProfile(uuid, name, skin, null); + } + + public GameProfile getProfile(UUID uuid, String name, String skinUrl, String capeUrl) { + // Create profile from properties + GameProfile profile = new GameProfile(uuid, name); + boolean cape = capeUrl != null && !(capeUrl.isEmpty()); + + List args = new ArrayList<>(); + args.add(System.currentTimeMillis()); + args.add(UUIDTypeAdapter.fromUUID(uuid)); + args.add(name); + args.add(skinUrl); + + if (cape) + args.add(capeUrl); + + profile.getProperties().put("textures", new Property("textures", Base64Coder.encodeString(String.format(cape ? JSON_CAPE : JSON_SKIN, args.toArray(new Object[0]))))); + + return profile; + } + + public void setCacheTime(long time) { + cacheTime = time; + } + + public static GameProfileBuilder getInstance() { + return INSTANCE; + } + + private static class GameProfileSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public GameProfile deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject object = (JsonObject) json; + UUID id = object.has("id") ? (UUID) context.deserialize(object.get("id"), UUID.class) : null; + String name = object.has("name") ? object.getAsJsonPrimitive("name").getAsString() : null; + GameProfile profile = new GameProfile(id, name); + + if (object.has("properties")) { + for (Map.Entry prop : ((PropertyMap) context.deserialize(object.get("properties"), PropertyMap.class)).entries()) + profile.getProperties().put(prop.getKey(), prop.getValue()); + } + + return profile; + } + + @Override + public JsonElement serialize(GameProfile profile, Type type, JsonSerializationContext context) { + JsonObject result = new JsonObject(); + + if (profile.getId() != null) + result.add("id", context.serialize(profile.getId())); + + if (profile.getName() != null) + result.addProperty("name", profile.getName()); + + if (!(profile.getProperties().isEmpty())) + result.add("properties", context.serialize(profile.getProperties())); + + return result; + } + + } + + private class CachedProfile { + + private long timestamp = System.currentTimeMillis(); + private GameProfile profile; + + public CachedProfile(GameProfile profile) { + this.profile = profile; + } + + public boolean isValid() { + return cacheTime < 0 || (System.currentTimeMillis() - timestamp) < cacheTime; + } + + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/io/MineSkinAPI.java b/utils/src/main/java/com/pepedevs/corelib/utils/io/MineSkinAPI.java new file mode 100644 index 0000000..e818ae3 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/io/MineSkinAPI.java @@ -0,0 +1,42 @@ +package com.pepedevs.corelib.utils.io; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class MineSkinAPI { + + private static final String URL_FORMAT = "https://api.mineskin.org/get/uuid/%s"; + + public static String[] getTextureProperties(String id) { + + try { + //Open http connection + HttpURLConnection textureConnection = (HttpURLConnection) new URL(String.format(URL_FORMAT, id)).openConnection(); + textureConnection.setRequestProperty("User-Agent", "JustixDevelopment/APIClient"); + textureConnection.setRequestMethod("GET"); + textureConnection.setReadTimeout(5000); + + //Parse response + BufferedReader reader = new BufferedReader(new InputStreamReader(textureConnection.getInputStream())); + JsonObject jsonObject = new JsonParser().parse(reader).getAsJsonObject(); + JsonObject texture = jsonObject.get("data").getAsJsonObject().get("texture").getAsJsonObject(); + + String[] property = new String[]{texture.get("value").getAsString(), texture.get("signature").getAsString()}; + reader.close(); + textureConnection.disconnect(); + + return property; + } catch (IOException ex) { + ex.printStackTrace(); + } + + return new String[]{null, null}; + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/io/UUIDFetcher.java b/utils/src/main/java/com/pepedevs/corelib/utils/io/UUIDFetcher.java new file mode 100644 index 0000000..468218e --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/io/UUIDFetcher.java @@ -0,0 +1,99 @@ +package com.pepedevs.corelib.utils.io; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.util.UUIDTypeAdapter; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class UUIDFetcher { + + private static final String UUID_URL = "https://api.mojang.com/users/profiles/minecraft/%s?at=%d"; + private static final String NAME_URL = "https://api.mojang.com/user/profiles/%s/names"; + private final Map uuidCache = new ConcurrentHashMap<>(); + private final Map nameCache = new ConcurrentHashMap<>(); + + private static final UUIDFetcher INSTANCE = new UUIDFetcher(); + + private UUIDFetcher() { + } + + public UUID getUUID(String name) { + return getUUIDAt(name, System.currentTimeMillis()); + } + + public UUID getUUIDAt(String name, long timestamp) { + name = name.toLowerCase(); + + //Check for cached unique id + if (uuidCache.containsKey(name)) + return uuidCache.get(name); + + try { + //Open connection + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(UUID_URL, name, timestamp / 1000)).openConnection(); + connection.setReadTimeout(5000); + + //Parse response + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + + try { + //Parse response + JsonObject data = new JsonParser().parse(bufferedReader).getAsJsonObject(); + UUID uniqueId = UUID.fromString(data.get("id").getAsString().replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); + + //Cache data + uuidCache.put(name, uniqueId); + nameCache.put(uniqueId, data.get("name").getAsString()); + + return uniqueId; + } catch (VerifyError | IllegalStateException ignore) { + } + } + } catch (Exception ignored) { + } + + return null; + } + + public String getName(String name, UUID uuid) { + //Check for cached name + if (nameCache.containsKey(uuid)) + return nameCache.get(uuid); + + try { + //Open connection + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(NAME_URL, UUIDTypeAdapter.fromUUID(uuid))).openConnection(); + connection.setReadTimeout(5000); + + //Parse response + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + + JsonArray data = new JsonParser().parse(bufferedReader).getAsJsonArray(); + JsonObject currentNameData = data.get(data.size() - 1).getAsJsonObject(); + + //Cache data + uuidCache.put(currentNameData.get("name").getAsString().toLowerCase(), uuid); + nameCache.put(uuid, currentNameData.get("name").getAsString()); + + return currentNameData.get("name").getAsString(); + } + } catch (Exception ignore) { + + } + + return name; + } + + public static UUIDFetcher getInstance() { + return INSTANCE; + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/ItemMetaBuilder.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/ItemMetaBuilder.java new file mode 100644 index 0000000..637248d --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/ItemMetaBuilder.java @@ -0,0 +1,296 @@ +package com.pepedevs.corelib.utils.itemstack; + +import com.pepedevs.corelib.utils.material.MaterialUtils; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.Arrays; +import java.util.List; + +/** Represents a class for building {@link ItemMeta} */ +public final class ItemMetaBuilder { + + private final Material material; + private final ItemMeta result; + + /** + * Constructs the ItemMetaBuilder. + * + *

+ * + * @param material Material for getting ItemMeta + */ + public ItemMetaBuilder(Material material) { + this.material = MaterialUtils.getRightMaterial(material); + this.result = Bukkit.getItemFactory().getItemMeta(this.material); + if (this.result == null) { + throw new IllegalArgumentException("Unsupported Material: " + material.name()); + } + } + + /** + * Returns the instance of {@link ItemMetaBuilder} for the given ItemStack. + * + *

+ * + * @param stack ItemStack to get ItemMeta of + * @return {@link ItemMetaBuilder} instance + */ + public static ItemMetaBuilder of(ItemStack stack) { + return stack.hasItemMeta() + ? of(stack.getType(), stack.getItemMeta()) + : new ItemMetaBuilder(stack.getType()); + } + + /** + * Returns the instance of {@link ItemMetaBuilder} for the given Material and ItemMeta. + * + *

+ * + * @param material Material of ItemStack + * @param meta Meta of ItemStack + * @return {@link ItemMetaBuilder} instance + */ + public static ItemMetaBuilder of(Material material, ItemMeta meta) { + ItemMetaBuilder builder = new ItemMetaBuilder(material); + if (meta != null) { + builder.withDisplayName(meta.getDisplayName()); + builder.withLore(meta.getLore()); + builder.withItemFlags( + meta.getItemFlags().toArray(new ItemFlag[meta.getItemFlags().size()])); + meta.getEnchants().keySet().stream() + .filter(enchantment -> enchantment != null) + .forEach( + enchantment -> + builder.withEnchantment( + enchantment, meta.getEnchantLevel(enchantment))); + } + return builder; + } + + /** + * Sets the display name in ItemMeta. + * + *

+ * + * @param display_name Display name + * @return This Object, for chaining + */ + public ItemMetaBuilder withDisplayName(String display_name) { + result.setDisplayName(display_name); + return this; + } + + /** + * Sets the lore in ItemMeta. + * + *

+ * + * @param lore Lore + * @return This Object, for chaining + */ + public ItemMetaBuilder withLore(List lore) { + result.setLore(lore); + return this; + } + + /** + * Sets the lore in ItemMeta. + * + *

+ * + * @param lore Lore + * @return This Object, for chaining + */ + public ItemMetaBuilder withLore(String... lore) { + return withLore(Arrays.asList(lore)); + } + + /** + * Append line to the lore in ItemMeta. + * + *

+ * + * @param line Line to append to lore + * @return This Object, for chaining + */ + public ItemMetaBuilder appendToLore(String line) { + List lore = result.getLore(); + if (lore == null) { + return withLore(line); + } else { + lore.add(line); + return withLore(lore); + } + } + + /** + * Remove lore line from ItemMeta. + * + *

+ * + * @param line Line to remove from lore + * @return This Object, for chaining + */ + public ItemMetaBuilder removeFromLore(String line) { + List lore = result.getLore(); + if (lore != null) { + lore.remove(line); + return withLore(lore); + } + return this; + } + + /** + * Sets the enchantment in ItemMeta. + * + *

+ * + * @param enchantment Enchantment + * @param level Level of enchantment + * @param ignore_max_level Ignore max level cap? + * @return This Object, for chaining + */ + public ItemMetaBuilder withEnchantment( + Enchantment enchantment, int level, boolean ignore_max_level) { + result.addEnchant(enchantment, level, ignore_max_level); + return this; + } + + /** + * Sets the enchantment in ItemMeta. + * + *

+ * + * @param enchantment Enchantment + * @param level Level of enchantment + * @return This Object, for chaining + */ + public ItemMetaBuilder withEnchantment(Enchantment enchantment, int level) { + return withEnchantment(enchantment, level, true); + } + + /** + * Sets the enchantment in ItemMeta. + * + *

+ * + * @param enchantment Enchantment + * @return This Object, for chaining + */ + public ItemMetaBuilder withEnchantment(Enchantment enchantment) { + return withEnchantment(enchantment, 0); + } + + /** + * Remove the enchantment in ItemMeta. + * + *

+ * + * @param enchantment Enchantment to remove + * @return This Object, for chaining + */ + public ItemMetaBuilder withoutEnchantment(Enchantment enchantment) { + result.removeEnchant(enchantment); + return this; + } + + /** + * Sets the Item Flag in ItemMeta. + * + *

+ * + * @param flag Item Flag + * @return This Object, for chaining + */ + public ItemMetaBuilder withItemFlags(ItemFlag... flag) { + result.addItemFlags(flag); + return this; + } + + /** + * Remove the Item Flag in ItemMeta. + * + *

+ * + * @param flag Item Flag + * @return This Object, for chaining + */ + public ItemMetaBuilder withoutItemFlags(ItemFlag... flag) { + result.removeItemFlags(flag); + return this; + } + + /** + * Sets if item is unbreakable in ItemMeta. + * + *

+ * + * @param unbreakable unbreakable? + * @return This Object, for chaining + */ + @SuppressWarnings("deprecation") + public ItemMetaBuilder unbreakable(boolean unbreakable) { + result.setUnbreakable(unbreakable); + return this; + } + + /** + * Create the {@link ItemMeta} with the given configuration. + * + *

+ * + * @return Created {@link ItemMeta} + */ + public ItemMeta build() { + return result; + } + + /** + * Returns ItemStack for the given configuration of ItemMeta. + * + *

+ * + * @param amount Amount of ItemStack + * @return {@link ItemStack} + */ + public ItemStack toItemStack(int amount) { + return applyTo(new ItemStack(material, amount)); + } + + /** + * Returns ItemStack for the given configuration of ItemMeta. + * + *

+ * + * @return {@link ItemStack} + */ + public ItemStack toItemStack() { + return applyTo(new ItemStack(material, 1)); + } + + /** + * Applies the given ItemMeta configuration to the given ItemStack. + * + *

+ * + * @param stack ItemStack to apply to + * @return {@link ItemStack} + */ + public ItemStack applyTo(ItemStack stack) { + if (stack == null) { + return null; + } + + if (MaterialUtils.getRightMaterial(stack) != material) { + return stack; + } + + stack.setItemMeta(result); + return stack; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/ItemStackUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/ItemStackUtils.java new file mode 100644 index 0000000..2570c70 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/ItemStackUtils.java @@ -0,0 +1,511 @@ +package com.pepedevs.corelib.utils.itemstack; + +import com.cryptomorin.xseries.XMaterial; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.pepedevs.corelib.utils.StringUtils; +import com.pepedevs.corelib.utils.io.GameProfileBuilder; +import com.pepedevs.corelib.utils.io.UUIDFetcher; +import com.pepedevs.corelib.utils.reflection.bukkit.PlayerReflection; +import com.pepedevs.corelib.utils.reflection.general.FieldReflection; +import com.pepedevs.corelib.utils.reflection.resolver.MethodResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import com.pepedevs.corelib.utils.version.Version; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** Represents a useful class for handling ItemStacks. */ +public class ItemStackUtils { + + /** + * Whether the field 'durability' is present in the {@link ItemStack} class of this server + * version. + */ + public static final boolean AVAILABLE_DURABILITY_FIELD; + + public static final MethodWrapper CRAFT_PLAYER_GET_GAMEPROFILE; + + static { + /* durability field */ + boolean durability_field = false; + try { + durability_field = + new ItemStack(Material.AIR).getClass().getDeclaredField("durability") != null; + } catch (NoSuchFieldException | SecurityException e) { + durability_field = false; + } + AVAILABLE_DURABILITY_FIELD = durability_field; + + CRAFT_PLAYER_GET_GAMEPROFILE = new MethodResolver(PlayerReflection.CRAFT_PLAYER_CLASS.getClazz()) + .resolveWrapper("getProfile"); + } + + /** + * Gets the {@link ItemStack} with the given material and amount. + * + *

+ * + * @param material Material of the ItemStack + * @param amount Amount of the ItemStack + * @return {@link ItemStack} + */ + public static ItemStack ofMaterial(Material material, int amount) { + if (Version.SERVER_VERSION.isNewer(Version.v1_12_R1)) { + return new ItemStack(material); + } + return new ItemStack(material, amount); + } + + /** + * Gets the {@link ItemStack} with the given material and amount. + * + *

+ * + * @param material XMaterial for the ItemStack + * @param amount Amount of the ItemStack + * @return {@link ItemStack} + */ + public static ItemStack ofUniversalMaterial(XMaterial material, int amount) { + if (material.parseMaterial() == null) { + return null; + } + + if (Version.SERVER_VERSION.isNewer(Version.v1_12_R1)) { + return new ItemStack(material.parseMaterial()); + } + return new ItemStack(material.parseMaterial(), amount); + } + + /** + * Gets the {@link ItemMeta} of the given {@link ItemStack}. + * + *

A new {@link ItemMeta} will created if necessary. + * + *

+ * + * @param stack {@link ItemStack} to get + * @return {@link ItemMeta} of the given {@link ItemMeta} + */ + public static ItemMeta getItemMeta(ItemStack stack) { + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + meta = Bukkit.getItemFactory().getItemMeta(stack.getType()); + } + return meta; + } + + /** + * Convert {@link ItemStack} to Soulbound item. + * + *

+ * + * @param stack ItemStack to convert + * @return SoulBound ItemStack + */ + public static ItemStack addSoulbound(ItemStack stack) { + if (stack == null) { + return stack; + } + + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + meta = Bukkit.getItemFactory().getItemMeta(stack.getType()); + } + + List lore = meta.getLore(); + if (lore == null) { + lore = new ArrayList(); + } + + lore.add("Soulbound"); + meta.setLore(lore); + stack.setItemMeta(meta); + return stack; + } + + /** + * Check if {@link ItemStack} is SoulBound. + * + *

+ * + * @param stack ItemStack to check + * @return true if is a SoulBound + */ + public static boolean isSoulbound(ItemStack stack) { + if (stack == null) { + return false; + } + + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + return false; + } + + List lore = meta.getLore(); + if (lore == null) { + return false; + } + return lore.contains("Soulbound"); + } + + /** + * Extracts the name from the {@link ItemMeta} of an {@link ItemStack}. + * + *

+ * + * @param stack {@link ItemStack} to extract + * @param strip_colors Strip colors? + * @return Display name of the given {@link ItemStack} or an empty string if it doesn't have + * name + */ + public static String extractName(ItemStack stack, boolean strip_colors) { + if (stack == null || stack.getItemMeta() == null) { + return ""; + } + + String displayName = stack.getItemMeta().getDisplayName(); + return displayName == null + ? "" + : (strip_colors ? StringUtils.stripColors(displayName) : displayName); + } + + /** + * Extracts the lore from the {@link ItemMeta} of an {@link ItemStack}. + * + *

+ * + * @param stack {@link ItemStack} to extract + * @param strip_colors Strip colors of the lore? + * @return Lore of the given {@link ItemStack} or an empty list if it doesn't have lore + */ + public static List extractLore(ItemStack stack, boolean strip_colors) { + List lore = new ArrayList<>(); + if (stack != null && stack.getItemMeta() != null && stack.getItemMeta().getLore() != null) { + lore = new ArrayList<>(stack.getItemMeta().getLore()); + if (strip_colors) { + for (int i = 0; i < lore.size(); i++) { + lore.set(i, StringUtils.stripColors(lore.get(i))); + } + } + } + return lore; + } + + /** + * Set {@link ItemStack} name and lore. + * + *

+ * + * @param itemStack ItemStack to modify + * @param name New name + * @param lore New lore + * @return Modified ItemStack + */ + public static ItemStack setNameLore(ItemStack itemStack, String name, List lore) { + ItemStack ot = itemStack; + if (name != null) { + ot = setName(itemStack, name); + } + return setLore(ot, lore); + } + + /** + * Set {@link ItemStack} set ItemStack name. + * + *

+ * + * @param itemStack ItemStack to modify + * @param name New name + * @return Modified ItemStack + */ + public static ItemStack setName(ItemStack itemStack, String name) { + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + meta = Bukkit.getItemFactory().getItemMeta(itemStack.getType()); + } + + if (meta == null) { + return itemStack; + } + + meta.setDisplayName(StringUtils.translateAlternateColorCodes(name)); + itemStack.setItemMeta(meta); + return itemStack; + } + + /** + * Set {@link ItemStack} set ItemStack lore. + * + *

+ * + * @param itemStack ItemStack to modify + * @param lore New lore + * @return Modified ItemStack + */ + public static ItemStack setLore(ItemStack itemStack, List lore) { + if (lore == null || lore.isEmpty()) { + return itemStack; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + meta = Bukkit.getItemFactory().getItemMeta(itemStack.getType()); + } + + if (meta == null) { + return itemStack; + } + + for (int x = 0; x < lore.size(); x++) { + lore.set(x, StringUtils.translateAlternateColorCodes(lore.get(x))); + } + + meta.setLore(lore); + itemStack.setItemMeta(meta); + return itemStack; + } + + /** + * Add enchant to {@link ItemStack}. + * + *

+ * + * @param stack ItemStack + * @param enchant Enchantment + * @param level Enchant level + * @return Enchanted ItemStack + */ + public static ItemStack addEnchantment( + final ItemStack stack, final Enchantment enchant, int level) { + // get item meta. + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + meta = Bukkit.getItemFactory().getItemMeta(stack.getType()); + } + + // add enchant. + meta.addEnchant(enchant, level, true); + + // update meta. + stack.setItemMeta(meta); + return stack; + } + + /** + * Check two items have the the same lore. + * + *

+ * + * @param i1 First ItemStack + * @param i2 Second ItemStack + * @return true if have the same lore + */ + public static boolean equalsLore(final ItemStack i1, final ItemStack i2) { + // check not null. + if ((i1 != null) == (i2 != null)) { + // check meta. + if ((i1.getItemMeta() != null) == (i2.getItemMeta() != null)) { + // check has item meta. + if (i1.getItemMeta() == null) { + return true; + } + + // check meta lore. + if (i1.getItemMeta().hasLore() == i2.getItemMeta().hasLore()) { + // check has lore. + if (!i1.getItemMeta().hasLore()) { + return true; + } + + // get lores. + final List lore1 = i1.getItemMeta().getLore(); + final List lore2 = i2.getItemMeta().getLore(); + + // compare lores. + for (String line : lore1) { + // check not null + if (line == null) { + continue; + } + + // check if the other contains. + if (!lore2.contains(line)) { + return false; + } + } + return true; + } + } + } + return false; + } + + /** + * Get the player textured skull {@link ItemStack}.
+ * Should run asynchronous + * + *

+ * + * @param owner Player texture owner + * @return Skull ItemStack textured with the player skin + */ + public static ItemStack getSkull(Player owner) { + return owner != null ? ItemStackUtils.createSkull(ItemStackUtils.getTexture(owner), owner.getName()) : ItemStackUtils.getSkullMaterial(1); + } + + /** + * Get the player textured skull {@link ItemStack}.
+ * Should run asynchronous + * + *

+ * + * @param owner UUID of texture owner + * @return Skull ItemStack textured with the player skin + */ + public static ItemStack getSkull(UUID owner) { + return owner != null ? ItemStackUtils.createSkull(ItemStackUtils.getTexture(owner), "Head") : ItemStackUtils.getSkullMaterial(1); + } + + /** + * Get the player textured skull {@link ItemStack}.
+ * Should run asynchronous + * + *

+ * + * @param texture Texture of the Skull item + * @return Skull ItemStack textured with the player skin + */ + public static ItemStack getSkull(final String texture) { + return texture != null ? ItemStackUtils.createSkull(texture, "Head") : ItemStackUtils.getSkullMaterial(1); + } + + /** + * Create a Skull ItemStack. + * + *

+ * + * @param texture Texture + * @param displayname Item display name + * @return Textured skull item stack + */ + private static ItemStack createSkull(String texture, String displayname) { + // get item and get meta. + final ItemStack stack = ItemStackUtils.getSkullMaterial(1); + final SkullMeta meta = ItemStackUtils.setSkullMeta(((SkullMeta) stack.getItemMeta()), texture); + + // set display name. + meta.setDisplayName(StringUtils.translateAlternateColorCodes(displayname)); + + // update meta. + stack.setItemMeta(meta); + return stack; + } + + /** + * Get texture from owner name. + * + *

+ * + * @param owner Skull owner + * @return Texture + */ + private static String getTexture(String owner) { + UUID uuid = UUIDFetcher.getInstance().getUUID(owner); + return ItemStackUtils.getTexture(uuid); + } + + private static String getTexture(UUID owner) { + GameProfile profile = GameProfileBuilder.getInstance().fetch(owner); + return profile.getProperties().get("textures").iterator().next().getValue(); + } + + private static String getTexture(Player owner) { + // get Game Profile and return property. + GameProfile profile = (GameProfile) CRAFT_PLAYER_GET_GAMEPROFILE.invoke(owner); + return profile.getProperties().get("textures").iterator().next().getValue(); + } + + /** + * Sets the skull texture property. + * + *

+ * + * @param skullMeta {@link SkullMeta} + * @param texture Texture + * @return Textured SkullMeta + */ + private static SkullMeta setSkullMeta(final SkullMeta skullMeta, final String texture) { + // get profile. + GameProfile profile = new GameProfile(UUID.randomUUID(), "CoreHead"); + + // put textures property. + profile.getProperties().put("textures", new Property("texture", texture)); + + // set field. + try { + FieldReflection.setValue(skullMeta, "profile", profile); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + return skullMeta; + } + + /** + * Get a Skull Item stack. + * + *

+ * + * @param amount Stack amount + * @return Skull Item Stack + */ + private static ItemStack getSkullMaterial(int amount) { + ItemStack item = XMaterial.PLAYER_HEAD.parseItem(); + item.setAmount(amount); + return item; + } + + /** + * Get {@link PlayerInventory} contents. + * + *

+ * + * @param inventory Inventory + * @return PlayerInventory list of contents + */ + public static List getAllContents( + final PlayerInventory inventory, boolean addArmorContents) { + final List contents = new ArrayList(); + for (int x = 0; x < 2; x++) { + if (x > 0 && !addArmorContents) { + break; + } + + contents.addAll( + Arrays.asList( + (x == 0 ? inventory.getContents() : inventory.getArmorContents()))); + } + return contents; + } + + /** + * Returns an empty {@link ItemStack}. + * + *

+ * + * @return Empty {@link ItemStack} + */ + public static ItemStack getEmptyStack() { + return new ItemStack(Material.AIR); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/custom/CustomItemStack.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/custom/CustomItemStack.java new file mode 100644 index 0000000..b8d2980 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/custom/CustomItemStack.java @@ -0,0 +1,227 @@ +package com.pepedevs.corelib.utils.itemstack.custom; + +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Class to interact with custom ItemStack. */ +public class CustomItemStack extends ItemStack { + + /** + * Checks whether this item has display name. + * + *

+ * + * @return true if it has display name, else false + */ + public boolean hasDisplayName() { + return this.getItemMeta().hasDisplayName(); + } + + /** + * Returns the display name of this item. + * + *

+ * + * @return Display name of this item + */ + public String getDisplayName() { + return this.getItemMeta().getDisplayName(); + } + + /** + * Sets the display name of this item. + * + *

+ * + * @param name Display name + * @return This Object, for chaining + */ + public CustomItemStack setDisplayName(String name) { + this.getItemMeta().setDisplayName(name); + return this; + } + + /** + * Checks whether this item has lore. + * + *

+ * + * @return true if it has lore, else false + */ + public boolean hasLore() { + return this.getItemMeta().hasLore(); + } + + /** + * Returns the lore of this item. + * + *

+ * + * @return Lore of this item + */ + public List getLore() { + return this.getItemMeta().getLore(); + } + + /** + * Sets the lore of this item. + * + *

+ * + * @param lore Lore + * @return This Object, for chaining + */ + public CustomItemStack setLore(List lore) { + this.getItemMeta().setLore(lore); + return this; + } + + /** + * Sets the lore of this item. + * + *

+ * + * @param lore Lore + * @return This Object, for chaining + */ + public CustomItemStack setLore(String[] lore) { + return this.setLore(Arrays.asList(lore)); + } + + /** + * Checks whether this item has enchantments. + * + *

+ * + * @return true if it has enchantments, else false + */ + public boolean hasEnchants() { + return this.getItemMeta().hasEnchants(); + } + + /** + * Checks whether this item has the given enchantment. + * + *

+ * + * @return true if it has the given enchantment, else false + */ + public boolean hasEnchant(Enchantment ench) { + return this.getItemMeta().hasEnchant(ench); + } + + /** + * Returns the enchantment level of the given enchantment. + * + *

+ * + * @return Enchantments level + */ + public int getEnchantLevel(Enchantment ench) { + return this.getItemMeta().getEnchantLevel(ench); + } + + /** + * Returns the enchantments of this item. + * + *

+ * + * @return Enchantments of this item + */ + public Map getEnchants() { + return this.getItemMeta().getEnchants(); + } + + /** + * Adds enchantment to this item. + * + *

+ * + * @param ench Enchantment + * @param level Level of enchantment + * @param ignoreLevelRestriction Whether to ignore max level cap + * @return true if added successfully, else false + */ + public boolean addEnchant(Enchantment ench, int level, boolean ignoreLevelRestriction) { + return this.getItemMeta().addEnchant(ench, level, ignoreLevelRestriction); + } + + /** + * Removes enchantment from this item. + * + *

+ * + * @param ench Enchantment + * @return true if removed successfully, else false + */ + public boolean removeEnchant(Enchantment ench) { + return this.getItemMeta().removeEnchant(ench); + } + + /** + * Checks whether this item has any vanilla conflicting enchantment for the given enchantment. + * + *

+ * + * @param ench Enchantment to check for conflict + * @return true if there is conflicting enchantment, else false + */ + public boolean hasConflictingEnchant(Enchantment ench) { + return this.getItemMeta().hasConflictingEnchant(ench); + } + + /** + * Adds ItemFlag to this item. + * + *

+ * + * @param itemFlags ItemFlag to add + * @return This Object, for chaining + */ + public CustomItemStack addItemFlags(ItemFlag... itemFlags) { + this.getItemMeta().addItemFlags(itemFlags); + return this; + } + + /** + * Removes ItemFlag from this item. + * + *

+ * + * @param itemFlags ItemFlag to remove + * @return This Object, for chaining + */ + public CustomItemStack removeItemFlags(ItemFlag... itemFlags) { + this.getItemMeta().removeItemFlags(itemFlags); + return this; + } + + /** + * Returns the ItemFlag of this item. + * + *

+ * + * @return ItemFlag of this item + */ + public Set getItemFlags() { + return this.getItemMeta().getItemFlags(); + } + + /** + * Checks whether this item has the given ItemFlag. + * + *

+ * + * @param flag ItemFlag to check + * @return true if present, else false + */ + public boolean hasItemFlag(ItemFlag flag) { + return this.getItemMeta().hasItemFlag(flag); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/safe/SafeItemStack.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/safe/SafeItemStack.java new file mode 100644 index 0000000..5510a0e --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/safe/SafeItemStack.java @@ -0,0 +1,37 @@ +package com.pepedevs.corelib.utils.itemstack.safe; + +import com.pepedevs.corelib.utils.itemstack.custom.CustomItemStack; +import com.pepedevs.corelib.utils.material.MaterialUtils; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +/** + * A {@link ItemStack} that is safe because of uses the method {@link + * MaterialUtils#getRightMaterial(Material)}. + */ +public class SafeItemStack extends CustomItemStack { + + /** + * Construct new {@link SafeItemStack}. + * + *

+ * + * @param material Type + */ + public SafeItemStack(Material material) { + this(material, 1); + } + + /** + * Construct new {@link SafeItemStack}. + * + *

+ * + * @param material Type + * @param amount Stack size + */ + public SafeItemStack(Material material, int amount) { + super.setType(MaterialUtils.getRightMaterial(material)); + this.setAmount(amount); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/stainedglass/StainedGlassColor.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/stainedglass/StainedGlassColor.java new file mode 100644 index 0000000..397e0fc --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/stainedglass/StainedGlassColor.java @@ -0,0 +1,102 @@ +package com.pepedevs.corelib.utils.itemstack.stainedglass; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +/** Enumeration of glass colors. */ +public enum StainedGlassColor { + WHITE(0), + + ORANGE(1), + + MAGENTA(2), + + LIGHT_BLUE(3), + + YELLOW(4), + + LIME(5), + + PINK(6), + + GRAY(7), + + LIGHT_GRAY(8), + + CYAN(9), + + PURPLE(10), + + BLUE(11), + + BROWN(12), + + GREEN(13), + + RED(14), + + BLACK(15), + ; + + private final short value; + + StainedGlassColor(final int value) { + this.value = (short) value; + } + + /** + * Returns {@link StainedGlassColor} instance from the given data value. + * + *

+ * + * @param value Data value + * @return {@link StainedGlassColor} instance + */ + public static StainedGlassColor getFromShort(final short value) { + for (StainedGlassColor color : values()) { + if (color.getShortValue() == value) { + return color; + } + } + return null; + } + + /** + * Returns the data value of the glass color. + * + *

+ * + * @return Data value + */ + public short getShortValue() { + return value; + } + + /** + * Returns Glass ItemStack for the defined color. + * + *

Note: This method is only supported in legacy versions. + * + *

+ * + * @return Glass ItemStack + */ + @SuppressWarnings("deprecation") + public ItemStack getColoredGlass() { + return new ItemStack(Material.valueOf("STAINED_GLASS"), 1, getShortValue()); + } + + /** + * Returns Glass Pane ItemStack for the defined color. + * + *

Note: This method is only supported in legacy versions. + * + *

+ * + * @return Glass Pane ItemStack + */ + @SuppressWarnings("deprecation") + public ItemStack getColoredPaneGlass() { + return new ItemStack(Material.valueOf("STAINED_GLASS_PANE"), 1, getShortValue()); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/stainedglass/StainedGlassItemStack.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/stainedglass/StainedGlassItemStack.java new file mode 100644 index 0000000..d11d3eb --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/stainedglass/StainedGlassItemStack.java @@ -0,0 +1,181 @@ +package com.pepedevs.corelib.utils.itemstack.stainedglass; + +import com.cryptomorin.xseries.XMaterial; +import com.pepedevs.corelib.utils.itemstack.ItemStackUtils; +import com.pepedevs.corelib.utils.itemstack.custom.CustomItemStack; +import org.apache.commons.lang.Validate; +import org.bukkit.Material; +import org.bukkit.Utility; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.material.MaterialData; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Represents the item stacks whose type is stained glass, and allows to change its color easily. + */ +@SuppressWarnings("deprecation") +public final class StainedGlassItemStack extends CustomItemStack { + + public static final StainedGlassColor DEFAULT_COLOR = StainedGlassColor.WHITE; + public static final Material ITEM_STACK_TYPE = Material.valueOf("STAINED_GLASS"); + public static final Material PANE_ITEM_STACK_TYPE = Material.valueOf("STAINED_GLASS"); + public static final boolean COLOR_SET_BY_SHORT = + (Material.matchMaterial("PINK_STAINED_GLASS") == null); + + private boolean pane; + + @Utility + public StainedGlassItemStack() { + this(DEFAULT_COLOR); + } + + /** + * A glass item stack with the specific color. + * + *

+ * + * @param color Color that this glass item stack will have + */ + public StainedGlassItemStack(StainedGlassColor color) { + this(color, 1); + } + + /** + * A glass item stack with the specific color. + * + *

+ * + * @param color Color that this glass item stack will have + * @param amount Stack size + */ + public StainedGlassItemStack(StainedGlassColor color, int amount) { + this(color, amount, false); + } + + /** + * A glass item stack with the specific color. + * + *

+ * + * @param color Color that this glass item stack will have. + * @param pane If will be a stained glass pane. + */ + public StainedGlassItemStack(StainedGlassColor color, boolean pane) { + this(color, 1, pane); + } + + /** + * A glass item stack with the specific color. + * + *

+ * + * @param color Color that this glass item stack will have + * @param amount Stack size + * @param pane If will be a stained glass pane + */ + public StainedGlassItemStack(StainedGlassColor color, int amount, boolean pane) { + Validate.notNull(color, "Color cannot be null"); + + /* initialize */ + this.pane = pane; + super.setType(pane ? ITEM_STACK_TYPE : PANE_ITEM_STACK_TYPE); + this.setAmount(amount); + this.setColor(color); + } + + /** + * Creates a new glass item stack derived from the specified stack. + * + *

+ * + * @param stack Stack to copy + * @throws IllegalArgumentException if the specified stack is null or returns an item meta not + * created by the item factory + */ + public StainedGlassItemStack(final StainedGlassItemStack stack) + throws IllegalArgumentException { + Validate.notNull(stack, "Cannot copy null stack"); + this.setAmount(stack.getAmount()); + if (ItemStackUtils.AVAILABLE_DURABILITY_FIELD) { + super.setDurability(stack.getDurability()); + } + + super.setData(stack.getData()); + if (stack.hasItemMeta()) { + setItemMeta0(stack.getItemMeta(), pane ? ITEM_STACK_TYPE : PANE_ITEM_STACK_TYPE); + } + } + + @Override + public void setType(Material material) { + /* ignore */ + } + + @Override + public void setData(MaterialData data) { + /* ignore */ + } + + @Override + public void setDurability(short durability) { + /* ignore */ + } + + /** + * Sets the color of this glass item stack. + * + *

+ * + * @param color New color + */ + public void setColor(StainedGlassColor color) { + Validate.notNull(color, "Color cannot be null"); + if (COLOR_SET_BY_SHORT) { + /* setting color by changing the durability */ + rawSetDurability(color.getShortValue()); + } else { + /* setting color by changing the item stack type */ + super.setType( + XMaterial.valueOf((color.name() + "_STAINED_GLASS" + (pane ? "_PANE" : ""))) + .parseMaterial()); + } + } + + /** The durability of the glass item stack cannot be changed manually. */ + private void rawSetDurability(short durability) { + if (!ItemStackUtils.AVAILABLE_DURABILITY_FIELD && durability == 0) { + return; + } + super.setDurability(durability); + } + + /** Cannot be overridden, so it's safe for constructor call */ + private void setItemMeta0(ItemMeta itemMeta, Material material) { + try { + Method method = + this.getClass() + .getDeclaredMethod("setItemMeta0", ItemMeta.class, Material.class); + method.setAccessible(true); + method.invoke(this, itemMeta, material); + } catch (NoSuchMethodException + | SecurityException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + e.printStackTrace(); + } + } + + // private void createData(final byte data) { + // try { + // Method method = this.getClass().getDeclaredMethod("createData", byte.class); + // method.setAccessible(true); method.invoke(this, data); + // } catch (NoSuchMethodException | SecurityException | IllegalAccessException + // | IllegalArgumentException | InvocationTargetException e) { + // e.printStackTrace(); + // } + // } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/wool/WoolColor.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/wool/WoolColor.java new file mode 100644 index 0000000..70c728a --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/wool/WoolColor.java @@ -0,0 +1,98 @@ +package com.pepedevs.corelib.utils.itemstack.wool; + +/** Enumeration of wool colors. */ +public enum WoolColor { + WHITE(0), + + ORANGE(1), + + MAGENTA(2), + + LIGHT_BLUE(3), + + YELLOW(4), + + LIME(5), + + PINK(6), + + GRAY(7), + + LIGHT_GRAY(8), + + CYAN(9), + + PURPLE(10), + + BLUE(11), + + BROWN(12), + + GREEN(13), + + RED(14), + + BLACK(15), + ; + + private final short value; + + WoolColor(final int value) { + this.value = (short) value; + } + + /** + * Returns {@link WoolColor} instance from the given data value. + * + *

+ * + * @param value Data value + * @return {@link WoolColor} instance + */ + public static WoolColor getFromShort(final short value) { + for (WoolColor color : values()) { + if (color.getShortValue() == value) { + return color; + } + } + return null; + } + + /** + * Returns the data value of the wool color. + * + *

+ * + * @return Data value + */ + public short getShortValue() { + return value; + } + + /** + * Returns {@link WoolItemStack} for the defined color. + * + *

Note: This method is only supported in legacy versions. + * + *

+ * + * @return {@link WoolItemStack} + */ + public WoolItemStack toItemStack() { + return toItemStack(1); + } + + /** + * Returns {@link WoolItemStack} for the defined color. + * + *

Note: This method is only supported in legacy versions. + * + *

+ * + * @param amount Amount of wool for ItemStack + * @return {@link WoolItemStack} + */ + public WoolItemStack toItemStack(int amount) { + return new WoolItemStack(this, amount); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/wool/WoolItemStack.java b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/wool/WoolItemStack.java new file mode 100644 index 0000000..e2c56a1 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/itemstack/wool/WoolItemStack.java @@ -0,0 +1,147 @@ +package com.pepedevs.corelib.utils.itemstack.wool; + +import com.pepedevs.corelib.utils.itemstack.ItemStackUtils; +import com.pepedevs.corelib.utils.itemstack.custom.CustomItemStack; +import com.pepedevs.corelib.utils.reflection.general.EnumReflection; +import org.apache.commons.lang.Validate; +import org.bukkit.Material; +import org.bukkit.Utility; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.material.MaterialData; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** Represents the item stacks whose type is wool, and allows to change its color easily. */ +@SuppressWarnings("deprecation") +public class WoolItemStack extends CustomItemStack { + + public static final WoolColor DEFAULT_COLOR = WoolColor.WHITE; + public static final Material WOOL_ITEM_STACK_TYPE = Material.valueOf("WOOL"); + + @Utility + public WoolItemStack() { + this(DEFAULT_COLOR); + } + + /** + * A wool item stack with the specific color. + * + *

+ * + * @param color Color that this wool item stack will have + */ + public WoolItemStack(WoolColor color) { + this(color, 1); + } + + /** + * A wool item stack with the specific color. + * + *

+ * + * @param color Color that this wool item stack will have + * @param amount Stack size + */ + public WoolItemStack(WoolColor color, int amount) { + Validate.notNull(color, "Color cannot be null"); + + /* initialize */ + super.setType(WOOL_ITEM_STACK_TYPE); + this.setAmount(amount); + this.setColor(color); + } + + /** + * Creates a new wool item stack derived from the specified stack. + * + *

+ * + * @param stack Stack to copy + * @throws IllegalArgumentException if the specified stack is null or returns an item meta not + * created by the item factory + */ + public WoolItemStack(final WoolItemStack stack) throws IllegalArgumentException { + Validate.notNull(stack, "Cannot copy null stack"); + this.setAmount(stack.getAmount()); + if (ItemStackUtils.AVAILABLE_DURABILITY_FIELD) { + super.setDurability(stack.getDurability()); + } + + super.setData(stack.getData()); + if (stack.hasItemMeta()) { + setItemMeta0(stack.getItemMeta(), WOOL_ITEM_STACK_TYPE); + } + } + + @Override + public void setType(Material material) { + /* ignore */ + } + + @Override + public void setData(MaterialData data) { + /* ignore */ + } + + @Override + public void setDurability(short durability) { + /* ignore */ + } + + /** + * Sets the color of this wool item stack. + * + *

+ * + * @param color New color + */ + public void setColor(WoolColor color) { + Validate.notNull(color, "Color cannot be null"); + + Material wool_material = + EnumReflection.getEnumConstant(Material.class, color.name() + "_WOOL"); + if (wool_material == null) { + // we are setting color by changing the durability- + rawSetDurability(color.getShortValue()); + } else { + // we are setting the color by changing the type. + super.setType(wool_material); + } + } + + /** The durability of the wool item stack cannot be changed manually. */ + private void rawSetDurability(short durability) { + if (ItemStackUtils.AVAILABLE_DURABILITY_FIELD) { + super.setDurability(durability); + } + } + + /** Cannot be overridden, so it's safe for constructor call */ + private void setItemMeta0(ItemMeta itemMeta, Material material) { + try { + Method method = + this.getClass() + .getDeclaredMethod("setItemMeta0", ItemMeta.class, Material.class); + method.setAccessible(true); + method.invoke(this, itemMeta, material); + } catch (NoSuchMethodException + | SecurityException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + e.printStackTrace(); + } + } + + // private void createData(final byte data) { + // try { + // Method method = this.getClass().getDeclaredMethod("createData", byte.class); + // method.setAccessible(true); method.invoke(this, data); + // } catch (NoSuchMethodException | SecurityException | IllegalAccessException + // | IllegalArgumentException | InvocationTargetException e) { + // e.printStackTrace(); + // } + // } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/json/Json.java b/utils/src/main/java/com/pepedevs/corelib/utils/json/Json.java new file mode 100644 index 0000000..20ba983 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/json/Json.java @@ -0,0 +1,410 @@ +package com.pepedevs.corelib.utils.json; + +import com.google.gson.*; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.MalformedJsonException; +import com.pepedevs.corelib.utils.StringUtils; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringEscapeUtils; + +import java.io.*; +import java.util.Map; +import java.util.Set; + +/** Represents a utility class for handling Json. */ +public final class Json { + + private final JsonObject handle; + private final Json root; + private final JsonOptions options; + + /** + * Constructs a new instance of {@link Json}. + * + *

+ * + * @param handle {@link JsonObject} to handle + * @param root Root Json Object + */ + private Json(JsonObject handle, Json root) { + this.handle = handle; + this.root = root; + this.options = root.getOptions(); + } + + /** + * Constructs a new instance of {@link Json}. + * + *

+ * + * @param handle {@link JsonObject} to handle + */ + private Json(JsonObject handle) { + this.handle = handle; + this.root = this; + this.options = new JsonOptions(); + } + + /** + * Loads a File to Json Object. + * + *

+ * + * @param json_file File to parse to Json + * @param check_encrypted Whether to check encryption + * @return This class instance + * @throws MalformedJsonException if the json file is corrupted or cannot be parsed + */ + public static Json load(File json_file, boolean check_encrypted) throws MalformedJsonException { + try { + StringBuilder contents = new StringBuilder(); + BufferedReader reader = new BufferedReader(new FileReader(json_file)); + reader.lines() + .forEach( + line -> { + contents.append(line + StringUtils.LINE_SEPARATOR); + }); + + reader.close(); + return loadFromString(contents.toString(), check_encrypted); + } catch (IOException e) { + throw new MalformedJsonException(e); + } + } + + /** + * Loads a File to Json Object. + * + *

+ * + * @param json_file File to parse to Json + * @return This class instance + * @throws MalformedJsonException if the json file is corrupted or cannot be parsed + */ + public static Json load(File json_file) throws MalformedJsonException { + return load(json_file, true); + } + + /** + * Loads a String to Json Object. + * + *

+ * + * @param contents String to parse to Json + * @param check_encrypted Whether to check encryption + * @return This class instance + */ + public static Json loadFromString(String contents, boolean check_encrypted) { + if (check_encrypted) { + if (StringEscapeUtils.escapeJava(StringUtils.deleteWhitespace(contents)) + .equals(StringUtils.deleteWhitespace(contents))) { // if encrypted + contents = new String(Base64.decodeBase64(contents)); // decode! + } + } + + JsonParser parser = new JsonParser(); + JsonElement element = parser.parse(contents); + if (!element.isJsonObject()) { + throw new JsonSyntaxException("Illegal syntax!"); + } + return new Json(element.getAsJsonObject()); + } + + /** + * Loads a String to Json Object. + * + *

+ * + * @param contents String to parse to Json + * @return This class instance + */ + public static Json loadFromString(String contents) { + return loadFromString(contents, true); + } + + /** + * Creates a new instance with a empty Json Object. + * + *

+ * + * @return This class instance + */ + public static Json getNew() { + return new Json(new JsonObject()); + } + + /** + * Returns the {@link JsonObject} handle. + * + *

+ * + * @return The handle {@link JsonObject} + */ + public JsonObject getHandle() { + return handle; + } + + /** + * Returns the {@link Json} root. + * + *

+ * + * @return The root {@link Json} + */ + public Json getRoot() { + return root; + } + + /** + * Returns the {@link JsonOptions} for this Json Object. + * + *

+ * + * @return {@link JsonOptions} + */ + public JsonOptions getOptions() { + return options; + } + + /** + * Creates a new Json Object with this instance as the root. + * + *

+ * + * @param name Key of the Json Object + * @return The new Json Object + */ + public Json createObject(String name) { + JsonElement present = handle.get(name); + if (present != null) { + if (!present.isJsonObject()) { + return null; + } + } else { + handle.add(name, (present = new JsonObject())); + } + return new Json(present.getAsJsonObject(), this); + } + + /** + * Adds a new Json Element to the Json Object. + * + *

+ * + * @param property Key of the element + * @param value Value of the element + */ + public void add(String property, JsonElement value) { + getHandle().add(property, value); + } + + /** + * Remove the given Json Element from the Json Object. + * + *

+ * + * @param property Key of the element + * @return Json Element removed + */ + public JsonElement remove(String property) { + return getHandle().remove(property); + } + + /** + * Adds a property to the Json Object. + * + *

+ * + * @param property Property key + * @param value String Value of the property + */ + public void addProperty(String property, String value) { + getHandle().addProperty(property, value); + } + + /** + * Adds a property to the Json Object. + * + *

+ * + * @param property Property key + * @param value Number Value of the property + */ + public void addProperty(String property, Number value) { + getHandle().addProperty(property, value); + } + + /** + * Adds a property to the Json Object. + * + *

+ * + * @param property Property key + * @param value Boolean Value of the property + */ + public void addProperty(String property, Boolean value) { + getHandle().addProperty(property, value); + } + + /** + * Adds a property to the Json Object. + * + *

+ * + * @param property Property key + * @param value Character Value of the property + */ + public void addProperty(String property, Character value) { + getHandle().addProperty(property, value); + } + + /** + * Returns the entry set of the Json Object. + * + *

+ * + * @return Entry set of the Json Object + */ + public Set> entrySet() { + return getHandle().entrySet(); + } + + /** + * Checks whether the Json Object has the element member. + * + *

+ * + * @param memberName Key of the element + * @return true if present, false otherwise + */ + public boolean has(String memberName) { + return getHandle().has(memberName); + } + + /** + * Returns the element member of Json Object. + * + *

+ * + * @param memberName Key of the element + * @return {@link JsonElement} for the given key + */ + public JsonElement get(String memberName) { + return getHandle().get(memberName); + } + + /** + * Returns the element member of Json Object as Json Primitive. + * + *

+ * + * @param memberName Key of the element + * @return {@link JsonPrimitive} for the given key + */ + public JsonPrimitive getAsJsonPrimitive(String memberName) { + return getHandle().getAsJsonPrimitive(memberName); + } + + /** + * Returns the element member of Json Object as Json Array. + * + *

+ * + * @param memberName Key of the element + * @return {@link JsonArray} for the given key + */ + public JsonArray getAsJsonArray(String memberName) { + return getHandle().getAsJsonArray(memberName); + } + + /** + * Returns the element member of Json Object as Json Object. + * + *

+ * + * @param memberName Key of the element + * @return {@link JsonObject} for the given key + */ + public JsonObject getAsJsonObject(String memberName) { + return getHandle().getAsJsonObject(memberName); + } + + /** + * Returns the element member of Json Object as Json. + * + *

+ * + * @param memberName Key of the element + * @return {@link Json} for the given key + */ + public Json getAsJson(String memberName) { + return new Json(getAsJsonObject(memberName), this); + } + + /** + * Saves the Json to a file. + * + *

+ * + * @param json_file File to save to + * @param encrypt Whether to encrypt data + * @throws IOException thrown while performing I/O + */ + public void save(File json_file, boolean encrypt) throws IOException { + String contents = toString(); + if (encrypt) { // encrypting! + contents = Base64.encodeBase64String(contents.getBytes()); + } + + FileWriter writer = new FileWriter(json_file); + writer.append(contents); + writer.close(); + } + + /** + * Saves the Json to a file. + * + *

+ * + * @param json_file File to save to + * @throws IOException thrown while performing I/O + */ + public void save(File json_file) throws IOException { + save(json_file, false); + } + + /** + * Returns the Json as a String. + * + *

+ * + * @return Json to string + */ + @Override + public String toString() { + try { + StringWriter string_writer = new StringWriter(); + JsonWriter json_writer = new JsonWriter(string_writer); + json_writer.setLenient(true); + json_writer.setHtmlSafe(getOptions().htmlSafe()); + json_writer.setIndent(getOptions().indent()); + json_writer.setSerializeNulls(getOptions().serializeNulls()); + + Streams.write(getHandle(), json_writer); + return string_writer.toString(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public boolean equals(Object obj) { + return getHandle().equals(obj); + } + + @Override + public int hashCode() { + return getHandle().hashCode(); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/json/JsonOptions.java b/utils/src/main/java/com/pepedevs/corelib/utils/json/JsonOptions.java new file mode 100644 index 0000000..81cd397 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/json/JsonOptions.java @@ -0,0 +1,88 @@ +package com.pepedevs.corelib.utils.json; + +/** Represents options for Json. */ +public class JsonOptions { + + private boolean html_safe; + private String indent; + private boolean serialize_nulls; + + /** Constructs the {@link JsonOptions}. */ + public JsonOptions() { + this.html_safe = false; + this.indent = null; + this.serialize_nulls = true; + } + + /** + * Returns the html safe option. + * + *

+ * + * @return Html safe option + */ + public boolean htmlSafe() { + return html_safe; + } + + /** + * Sets the html safe option. + * + *

+ * + * @param html_safe Option value + * @return This Object, for chaining + */ + public JsonOptions htmlSafe(boolean html_safe) { + this.html_safe = html_safe; + return this; + } + + /** + * Returns the indent of the Json. + * + *

+ * + * @return Indent of Json + */ + public String indent() { + return (indent == null ? new String() : indent); + } + + /** + * Sets the indent of the Json. + * + *

+ * + * @param indent Indent to set + * @return This Object, for chaining + */ + public JsonOptions indent(String indent) { + this.indent = ((indent != null && indent.length() == 0) ? null : indent); + return this; + } + + /** + * Returns the Serialize Nulls option. + * + *

+ * + * @return Serialize Nulls + */ + public boolean serializeNulls() { + return serialize_nulls; + } + + /** + * Sets the Serialize Nulls option. + * + *

+ * + * @param serialize_nulls Option value + * @return This Object, for chaining + */ + public JsonOptions serializeNulls(boolean serialize_nulls) { + this.serialize_nulls = serialize_nulls; + return this; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/material/MaterialUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/material/MaterialUtils.java new file mode 100644 index 0000000..abd5963 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/material/MaterialUtils.java @@ -0,0 +1,111 @@ +package com.pepedevs.corelib.utils.material; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.MaterialData; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** Represents a utility class for Material. */ +public class MaterialUtils { + + public static Class CRAFT_MAGIC_NUMBERS_CLASS; + public static Method FROM_LEGACY_DATA_PRIORITY; + + /* initialize util fields */ + static { + try { + CRAFT_MAGIC_NUMBERS_CLASS = Class.forName("org.bukkit.craftbukkit.util.CraftMagicNumbers"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + try { + FROM_LEGACY_DATA_PRIORITY = + CRAFT_MAGIC_NUMBERS_CLASS.getMethod( + "fromLegacy", MaterialData.class, boolean.class); + } catch (NoSuchMethodException | SecurityException e) { + /* ignore */ + } + } + + /** + * Returns the Material value of the given ItemStack according to server version. + * + *

+ * + * @param stack ItemStack to get Material from + * @return Material according to server version + */ + public static Material getRightMaterial(ItemStack stack) { + try { + if (isLegacy(stack.getType())) { + return (Material) + FROM_LEGACY_DATA_PRIORITY.invoke( + CRAFT_MAGIC_NUMBERS_CLASS, stack.getData(), true); + } + } catch (Throwable t) { + /* ignore */ + } + return stack.getType(); + } + + /** + * Returns the Material value according to server version. + * + *

+ * + * @param material Material to get according to server version + * @return Material according to server version + */ + public static Material getRightMaterial(Material material) { + return getRightMaterial(new ItemStack(material)); + } + + public static boolean isLegacy(Material material) { + try { + return (boolean) material.getClass().getMethod("isLegacy").invoke(material.getClass()); + } catch (IllegalAccessException + | IllegalArgumentException + | InvocationTargetException + | NoSuchMethodException + | SecurityException e) { + return false; + } + } + + /** + * Checks whether both material are equal. + * + *

+ * + * @param a Material + * @param b Material + * @return true if equal, else false + */ + public static boolean equals(Material a, Material b) { + return (a != null && b != null && getRightMaterial(a) == getRightMaterial(b)); + } + + /** + * Creates a new BlockData instance for the specified Material, with all properties initialized + * to unspecified defaults. + * + *

+ * + * @param material Material + * @return New data instance, or null if invoked from a server version older than 1_13_R1 + */ + public static Object createBlockData(Material material) { + try { + Method create = material.getClass().getMethod("createBlockData"); + if (create != null) { // null if is a server version older than 1_13_R1 + return create.invoke(material); + } + } catch (Throwable t) { + return null; + } + return null; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/DirectionUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/DirectionUtils.java new file mode 100644 index 0000000..8b6dc69 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/DirectionUtils.java @@ -0,0 +1,320 @@ +package com.pepedevs.corelib.utils.math; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.util.NumberConversions; +import org.bukkit.util.Vector; + +/** Class for dealing with direction {@link Vector}s and EULER angles. */ +public class DirectionUtils { + + /** BlockFace each 90°. */ + public static final BlockFace[] FACES_90 = { + BlockFace.SOUTH, BlockFace.WEST, BlockFace.NORTH, BlockFace.EAST + }; + + /** + * Normalizes the provided angle to a value between 0° and 360°. + * + *

+ * + * @param angle the angle to normalize. + * @return the normalized angle. + */ + public static float normalize(float angle) { + return (angle + 360F) % 360F; + } + + /** + * Normalizes the provided angle to a value between +180 and -180°. + * + *

+ * + * @param angle the angle to normalize. + * @return the normalized angle. + */ + public static float normalize2(float angle) { + angle = normalize(angle); + return angle >= 180F ? (angle - 360F) : (angle < -180F ? (angle + 360F) : angle); + } + + /** + * Gets the equivalent {@link BlockFace} of the provided yaw angle. + * + *

+ * + * @param yaw the yaw angle. + * @return the {@link BlockFace} equivalent of the provided yaw angle. + */ + public static BlockFace getBlockFace(float yaw) { + return FACES_90[(int) Math.floor(normalize(yaw) / 90F) % 4]; + } + + /** + * Gets the equivalent yaw angle of the provided {@link BlockFace}. + * + *

Also {@code NaN} will be returned if the provided block face is not a 90° + * face. It means that only the following block faces are supported: + * + *

    + *
  • {@link BlockFace#SOUTH} + *
  • {@link BlockFace#WEST} + *
  • {@link BlockFace#NORTH} + *
  • {@link BlockFace#EAST} + *
+ * + *

+ * + * @param blockface Desired block face + * @return Equivalent yaw angle of the provided {@link BlockFace} + */ + public static float getYaw(BlockFace blockface) { + switch (blockface) { + case SOUTH: + return 0F; + case WEST: + return 90F; + case NORTH: + return 180F; + case EAST: + return 270; + default: + return Float.NaN; + } + } + + /** + * Gets the euler angles from the direction vector result of the subtraction of {@code to} and + * {@code from}. + * + *

+ * + *

    + *
  • lookAt (from, to)[0] = yaw angle. + *
  • lookAt (from, to)[1] = pitch angle. + *
+ * + *

+ * + * @param from Position from + * @param to Position to + * @return Euler angles of the result direction vector + */ + public static float[] lookAt(Vector from, Vector to) { + final double dx = (to.getX() - from.getX()); + final double dy = (to.getY() - from.getY()); + final double dz = (to.getZ() - from.getZ()); + + final double dst_xz = + Math.sqrt(NumberConversions.square(dx) + NumberConversions.square(dz)); + final double dst_y = + Math.sqrt(NumberConversions.square(dst_xz) + NumberConversions.square(dy)); + + double yaw = Math.toDegrees(Math.acos(dx / dst_xz)); + double pitch = Math.toDegrees(Math.acos(dy / dst_y)) - 90D; + + if (dz < 0D) { + yaw += Math.abs(180D - yaw) * 2D; + } + + yaw -= 90D; + pitch -= 90D; + return new float[] {(float) yaw, (float) pitch}; + } + + /** + * Gets the euler angles from the direction vector result of the subtraction of {@code to} and + * {@code from}. + * + *

+ * + *

    + *
  • lookAt (from, to)[0] = yaw angle. + *
  • lookAt (from, to)[1] = pitch angle. + *
+ * + *

+ * + * @param from Position from + * @param to Position to + * @return Euler angles of the result direction vector + * @see DirectionUtils#lookAt(Vector, Vector) + */ + public static float[] lookAt(Location from, Location to) { + return lookAt(from.toVector(), to.toVector()); + } + + /** + * Gets the euler angles from the direction vector result of the subtraction of {@code to} and + * {@code from}. + * + *

+ * + *

    + *
  • lookAt (from, to)[0] = yaw angle. + *
  • lookAt (from, to)[1] = pitch angle. + *
+ * + *

+ * + * @param from Position from + * @param to Position to + * @return Euler angles of the result direction vector + */ + public static float[] lookAt2(Vector from, Vector to) { + return getEulerAngles(to.clone().subtract(from).normalize()); + } + + /** + * Gets the euler angles from the direction vector result of the subtraction of {@code to} and + * {@code from}. + * + *

+ * + *

    + *
  • lookAt (from, to)[0] = yaw angle. + *
  • lookAt (from, to)[1] = pitch angle. + *
+ * + *

+ * + * @param from Position from + * @param to Position to + * @return Euler angles of the result direction vector + * @see DirectionUtils#lookAt2(Vector, Vector) + */ + public static float[] lookAt2(Location from, Location to) { + return lookAt2(from.toVector(), to.toVector()); + } + + /** + * Gets a unit-vector pointing in the direction represented by the specified euler angles + * ({@code yaw} and {@code pitch}). + * + *

+ * + * @param yaw Yaw angle (rotation around axis X). + * @param pitch Pitch angle (rotation around axis Y). + * @return Vector pointing the direction represented by the specified euler angles. + */ + public static Vector getDirection(float yaw, float pitch) { + Vector vector = new Vector(); + + double rotX = yaw; + double rotY = pitch; + + vector.setY(-Math.sin(Math.toRadians(rotY))); + + double xz = Math.cos(Math.toRadians(rotY)); + + vector.setX(-xz * Math.sin(Math.toRadians(rotX))); + vector.setZ(xz * Math.cos(Math.toRadians(rotX))); + return vector; + } + + /** + * Gets the euler angles from the provided direction {@link Vector}. + * + *

+ * + *

    + *
  • getEulerAngles (direction)[0] = yaw angle. + *
  • getEulerAngles (direction)[1] = pitch angle. + *
+ * + *

+ * + * @param direction Direction vector + * @return Yaw and pitch angles within an array + */ + public static float[] getEulerAngles(Vector direction) { + float yaw = 0F; + float pitch = 0F; + + final double _2PI = 2 * Math.PI; + final double x = direction.getX(); + final double z = direction.getZ(); + + if (x == 0 && z == 0) { + pitch = direction.getY() > 0 ? -90 : 90; + return new float[] {yaw, pitch}; + } + + double theta = Math.atan2(-x, z); + yaw = (float) Math.toDegrees((theta + _2PI) % _2PI); + + double x2 = NumberConversions.square(x); + double z2 = NumberConversions.square(z); + double xz = Math.sqrt(x2 + z2); + + pitch = (float) Math.toDegrees(Math.atan(-direction.getY() / xz)); + return new float[] {yaw, pitch}; + } + + /** + * Get left {@link Block}. + * + *

+ * + * @param block Block + * @param direction Base BlockFace + * @param numBlocks Number of blocks + * @return Left Block + */ + public static Block getLeft(Block block, BlockFace direction, int numBlocks) { + BlockFace bf = getLeftFace(direction); + return block.getRelative( + bf.getModX() * numBlocks, bf.getModY() * numBlocks, bf.getModZ() * numBlocks); + } + + /** + * Get right {@link Block}. + * + *

+ * + * @param block Block + * @param direction Base BlockFace + * @param numBlocks Number of blocks + * @return Right Block + */ + public static Block getRight(Block block, BlockFace direction, int numBlocks) { + BlockFace bf = getRightFace(direction); + return block.getRelative( + bf.getModX() * numBlocks, bf.getModY() * numBlocks, bf.getModZ() * numBlocks); + } + + /** + * Get the {@link BlockFace} at the right of other BlockFace. + * + *

+ * + * @param direction Base BlockFace + * @return Right BlockFace + */ + public static BlockFace getRightFace(BlockFace direction) { + return getLeftFace(direction).getOppositeFace(); + } + + /** + * Get the {@link BlockFace} at the left of other BlockFace. + * + * @param direction Base BlockFace + * @return Left BlockFace + */ + public static BlockFace getLeftFace(BlockFace direction) { + switch (direction) { + case SOUTH: + return BlockFace.EAST; + case EAST: + return BlockFace.NORTH; + case NORTH: + return BlockFace.WEST; + case WEST: + return BlockFace.SOUTH; + default: + break; + } + return BlockFace.NORTH; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/IntersectionUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/IntersectionUtils.java new file mode 100644 index 0000000..0a4e81b --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/IntersectionUtils.java @@ -0,0 +1,245 @@ +package com.pepedevs.corelib.utils.math; + +import com.pepedevs.corelib.utils.math.collision.BoundingBox; +import com.pepedevs.corelib.utils.math.collision.Ray; +import org.bukkit.util.Vector; + +/** + * Class offering various static methods for intersection testing between different geometric + * objects. + */ +public class IntersectionUtils { + + // private final static Vector v0 = new Vector(); + // private final static Vector v1 = new Vector(); + private static final Vector v2 = new Vector(); + + /** + * Intersects a {@link Ray} and a {@link BoundingBox}, returning the intersection point in + * intersection. This intersection is defined as the point on the ray closest to the origin + * which is within the specified bounds. + * + *

The returned intersection (if any) is guaranteed to be within the bounds of the bounding + * box, but it can occasionally diverge slightly from ray, due to small floating-point errors. + * + *

If the origin of the ray is inside the box, this method returns true and the intersection + * point is set to the origin of the ray, accordingly to the definition above. + * + * @param ray The ray + * @param box The box + * @param intersection The intersection point (optional) + * @return Whether an intersection is present + */ + public static boolean intersectRayBounds(Ray ray, BoundingBox box, Vector intersection) { + if (box.contains(ray.getOrigin())) { + if (intersection != null) { + set(intersection, ray.getOrigin()); + } + return true; + } + + double lowest = 0, t; + boolean hit = false; + + // min x + if (ray.getOrigin().getX() <= box.getMinimum().getX() && ray.getDirection().getX() > 0) { + t = (box.getMinimum().getX() - ray.getOrigin().getX()) / ray.getDirection().getX(); + if (t >= 0) { + set(v2, ray.getDirection()).multiply(t).add(ray.getOrigin()); + if (v2.getY() >= box.getMinimum().getY() + && v2.getY() <= box.getMaximum().getY() + && v2.getZ() >= box.getMinimum().getZ() + && v2.getZ() <= box.getMaximum().getZ() + && (!hit || t < lowest)) { + hit = true; + lowest = t; + } + } + } + // max x + if (ray.getOrigin().getX() >= box.getMaximum().getX() && ray.getDirection().getX() < 0) { + t = (box.getMaximum().getX() - ray.getOrigin().getX()) / ray.getDirection().getX(); + if (t >= 0) { + set(v2, ray.getDirection()).multiply(t).add(ray.getOrigin()); + if (v2.getY() >= box.getMinimum().getY() + && v2.getY() <= box.getMaximum().getY() + && v2.getZ() >= box.getMinimum().getZ() + && v2.getZ() <= box.getMaximum().getZ() + && (!hit || t < lowest)) { + hit = true; + lowest = t; + } + } + } + // min y + if (ray.getOrigin().getY() <= box.getMinimum().getY() && ray.getDirection().getY() > 0) { + t = (box.getMinimum().getY() - ray.getOrigin().getY()) / ray.getDirection().getY(); + if (t >= 0) { + set(v2, ray.getDirection()).multiply(t).add(ray.getOrigin()); + if (v2.getX() >= box.getMinimum().getX() + && v2.getX() <= box.getMaximum().getX() + && v2.getZ() >= box.getMinimum().getZ() + && v2.getZ() <= box.getMaximum().getZ() + && (!hit || t < lowest)) { + hit = true; + lowest = t; + } + } + } + // max y + if (ray.getOrigin().getY() >= box.getMaximum().getY() && ray.getDirection().getY() < 0) { + t = (box.getMaximum().getY() - ray.getOrigin().getY()) / ray.getDirection().getY(); + if (t >= 0) { + set(v2, ray.getDirection()).multiply(t).add(ray.getOrigin()); + if (v2.getX() >= box.getMinimum().getX() + && v2.getX() <= box.getMaximum().getX() + && v2.getZ() >= box.getMinimum().getZ() + && v2.getZ() <= box.getMaximum().getZ() + && (!hit || t < lowest)) { + hit = true; + lowest = t; + } + } + } + // min z + if (ray.getOrigin().getZ() <= box.getMinimum().getZ() && ray.getDirection().getZ() > 0) { + t = (box.getMinimum().getZ() - ray.getOrigin().getZ()) / ray.getDirection().getZ(); + if (t >= 0) { + set(v2, ray.getDirection()).multiply(t).add(ray.getOrigin()); + if (v2.getX() >= box.getMinimum().getX() + && v2.getX() <= box.getMaximum().getX() + && v2.getY() >= box.getMinimum().getY() + && v2.getY() <= box.getMaximum().getY() + && (!hit || t < lowest)) { + hit = true; + lowest = t; + } + } + } + // max y + if (ray.getOrigin().getZ() >= box.getMaximum().getZ() && ray.getDirection().getZ() < 0) { + t = (box.getMaximum().getZ() - ray.getOrigin().getZ()) / ray.getDirection().getZ(); + if (t >= 0) { + set(v2, ray.getDirection()).multiply(t).add(ray.getOrigin()); + if (v2.getX() >= box.getMinimum().getX() + && v2.getX() <= box.getMaximum().getX() + && v2.getY() >= box.getMinimum().getY() + && v2.getY() <= box.getMaximum().getY() + && (!hit || t < lowest)) { + hit = true; + lowest = t; + } + } + } + if (hit && intersection != null) { + Vector temp = ray.getDirection().clone().multiply(lowest).add(ray.getOrigin()); + intersection.setX(temp.getX()).setY(temp.getY()).setZ(temp.getZ()); + if (intersection.getX() < box.getMinimum().getX()) { + intersection.setX(box.getMinimum().getX()); + } else if (intersection.getX() > box.getMaximum().getX()) { + intersection.setX(box.getMaximum().getX()); + } + if (intersection.getY() < box.getMinimum().getY()) { + intersection.setY(box.getMinimum().getY()); + } else if (intersection.getY() > box.getMaximum().getY()) { + intersection.setY(box.getMaximum().getY()); + } + if (intersection.getZ() < box.getMinimum().getZ()) { + intersection.setZ(box.getMinimum().getZ()); + } else if (intersection.getZ() > box.getMaximum().getZ()) { + intersection.setZ(box.getMaximum().getZ()); + } + } + return hit; + } + + /** + * Quick check whether the given {@link Ray} and {@link BoundingBox} intersect. + * + * @param ray The ray + * @param box The bounding box + * @return Whether the ray and the bounding box intersect + */ + public static boolean intersectRayBoundsFast(Ray ray, BoundingBox box) { + return intersectRayBoundsFast(ray, box.getCenter(), box.getDimensions()); + } + + /** + * Quick check whether the given {@link Ray} and {@link BoundingBox} intersect. + * + * @param ray The ray + * @param center The center of the bounding box + * @param dimensions The dimensions (width, height and depth) of the bounding box + * @return Whether the ray and the bounding box intersect + */ + public static boolean intersectRayBoundsFast(Ray ray, Vector center, Vector dimensions) { + final double divX = 1f / ray.getDirection().getX(); + final double divY = 1f / ray.getDirection().getY(); + final double divZ = 1f / ray.getDirection().getZ(); + + double minx = ((center.getX() - dimensions.getX() * .5f) - ray.getOrigin().getX()) * divX; + double maxx = ((center.getX() + dimensions.getX() * .5f) - ray.getOrigin().getX()) * divX; + if (minx > maxx) { + final double t = minx; + minx = maxx; + maxx = t; + } + + double miny = ((center.getY() - dimensions.getY() * .5f) - ray.getOrigin().getY()) * divY; + double maxy = ((center.getY() + dimensions.getY() * .5f) - ray.getOrigin().getY()) * divY; + if (miny > maxy) { + final double t = miny; + miny = maxy; + maxy = t; + } + + double minz = ((center.getZ() - dimensions.getZ() * .5f) - ray.getOrigin().getZ()) * divZ; + double maxz = ((center.getZ() + dimensions.getZ() * .5f) - ray.getOrigin().getZ()) * divZ; + if (minz > maxz) { + final double t = minz; + minz = maxz; + maxz = t; + } + + double min = Math.max(Math.max(minx, miny), minz); + double max = Math.min(Math.min(maxx, maxy), maxz); + + return max >= 0 && max >= min; + } + + /** + * Quick check whether the provided {@link Ray} and {@link BoundingBox} intersects. + * + *

+ * + * @param ray Ray + * @param bounds Bounding box. + * @return Whether the ray and the bounding box intersects. + */ + public static boolean intersectRayBoundsFast2(Ray ray, BoundingBox bounds) { + final Vector origin = ray.getOrigin(); + if (bounds.contains(origin)) { + return true; + } + + final double d0 = origin.distance(bounds.getCenter()); + final double d1 = origin.distance(bounds.getMinimum()); + final double d2 = origin.distance(bounds.getMaximum()); + + final Vector v0 = ray.getDirection(); + final Vector v1 = v0.clone().multiply(d0); + final Vector v2 = v0.clone().multiply(d1); + final Vector v3 = v0.clone().multiply(d2); + + final boolean c0 = bounds.contains(origin.clone().add(v1)); + final boolean c1 = bounds.contains(origin.clone().add(v2)); + final boolean c2 = bounds.contains(origin.clone().add(v3)); + + return c0 || c1 || c2; + } + + private static Vector set(Vector v0, Vector v1) { + v0.setX(v1.getX()).setY(v1.getY()).setZ(v1.getZ()); + return v0; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/LocationUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/LocationUtils.java new file mode 100644 index 0000000..e44f256 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/LocationUtils.java @@ -0,0 +1,229 @@ +package com.pepedevs.corelib.utils.math; + +import com.pepedevs.corelib.utils.reflection.general.ClassReflection; +import org.apache.commons.lang.reflect.FieldUtils; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.WorldBorder; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Player; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** Class for dealing with locations. */ +public class LocationUtils { + + /** + * Formats the desired {@link Location}. + * + *

+ * + * @param location Location to format + * @param append_yaw Append yaw? + * @param append_pitch Append pitch? + * @param append_world Append world name? + * @return Formatted location. + */ + public static String format( + Location location, boolean append_yaw, boolean append_pitch, boolean append_world) { + return (location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ()) + + (append_yaw ? ", " + location.getYaw() : "") + + (append_pitch ? ", " + location.getPitch() : "") + + (append_world ? ", (at '" + location.getWorld().getName() + "')" : ""); + } + + /** + * Rounds up the yaw angle of the provided location. + * + *

+ * + * @param location Location to round + * @return Location with its yaw angle rounded up + */ + public static Location roundUpYaw(Location location) { + location.setYaw(DirectionUtils.getYaw(DirectionUtils.getBlockFace(location.getYaw()))); + return location; + } + + /** + * Rounds up the pitch angle of the provided location. + * + *

+ * + * @param location Location to round + * @return Location with its pitch angle rounded up + */ + public static Location roundUpPitch(Location location) { + location.setPitch(location.getPitch() != 0F ? (location.getPitch() < 0F ? -90F : 90F) : 0F); + return location; + } + + /** + * Gets all the blocks between the provided location corners. + * + *

+ * + * @param corner_a First corner + * @param corner_b Second corner + * @return Set containing the blocks between the provided locations + * @throws IllegalArgumentException for differing worlds + */ + public static Set getBlocksBetween(Location corner_a, Location corner_b) { + if (corner_a.getWorld() != corner_b.getWorld()) { + throw new IllegalArgumentException( + "cannot get blocks between " + + corner_a.getWorld().getName() + + " and " + + corner_b.getWorld().getName()); + } + + final Set blocks = new HashSet<>(); + + int min_x = Math.min(corner_a.getBlockX(), corner_b.getBlockX()); + int min_y = Math.min(corner_a.getBlockY(), corner_b.getBlockY()); + int min_z = Math.min(corner_a.getBlockZ(), corner_b.getBlockZ()); + int max_x = Math.max(corner_a.getBlockX(), corner_b.getBlockX()); + int max_y = Math.max(corner_a.getBlockY(), corner_b.getBlockY()); + int max_z = Math.max(corner_a.getBlockZ(), corner_b.getBlockZ()); + + for (int x = min_x; x <= max_x; x++) { + for (int y = min_y; y <= max_y; y++) { + for (int z = min_z; z <= max_z; z++) { + blocks.add(corner_a.getWorld().getBlockAt(x, y, z)); + } + } + } + return blocks; + } + + /** + * Get circle around of location. + * + * @param center Center location + * @param radius Circle radius + * @param amount Circle definition + * @return List with circle locations + */ + public static ArrayList getCircle(Location center, double radius, int amount) { + World world = center.getWorld(); + double increment = (2 * Math.PI) / amount; + ArrayList locations = new ArrayList(); + for (int i = 0; i < amount; i++) { + double angle = i * increment; + double x = center.getX() + (radius * Math.cos(angle)); + double z = center.getZ() + (radius * Math.sin(angle)); + locations.add(new Location(world, x, center.getY(), z)); + } + return locations; + } + + /** + * Get cuboid around of location. + * + *

+ * + * @param center Center location + * @param radius Cuboid radius + * @param amount Cuboid definition + * @return List with cuboid locations + */ + public static ArrayList getCuboid(Location center, double radius, int amount) { + // make list. + ArrayList locations = new ArrayList(); + + // get main eyes. + final double mainX = center.getX(); + final double mainY = center.getY(); + final double mainZ = center.getZ(); + + // get cuboid. + World world = center.getWorld(); + for (int z = 0; z < 2; z++) { + double newZ = z == 0 ? (mainZ - radius) : (mainZ + radius); + Location corner = new Location(world, mainX - radius, mainY, newZ); + Location corner2 = new Location(world, mainX + radius, mainY, newZ); + double increment = (corner.distance(corner2) / amount); + for (int i = 0; i < (amount + 1); i++) { + locations.add(corner.clone().add(increment * i, 0.0D, 0.0D)); + } + } + + for (int x = 0; x < 2; x++) { + double newX = x == 0 ? (mainX - radius) : (mainX + radius); + Location corner = new Location(world, newX, mainY, mainZ - radius); + Location corner2 = new Location(world, newX, mainY, mainZ + radius); + double increment = (corner.distance(corner2) / amount); + for (int i = 0; i < (amount + 1); i++) { + locations.add(corner.clone().add(0.0D, 0.0D, increment * i)); + } + } + return locations; + } + + public static boolean isInsideWorldBorder(final Player p, final WorldBorder border) { + return isInsideWorldBorder(p.getLocation(), border); + } + + public static boolean isInsideWorldBorder(final Location location, final WorldBorder border) { + try { + /* get world of the world border */ + final World world = border.getCenter().getWorld(); + + /* load reflection */ + final Class craft_world_border_class = border.getClass(); + final Class block_position_class = + ClassReflection.getNmsClass("BlockPosition", "core"); + final Constructor block_position_constructor = + block_position_class.getConstructor(double.class, double.class, double.class); + + /* get instances */ + final Object craft = craft_world_border_class.cast(border); + final Object handle = FieldUtils.readField(craft, "handle", true); + final Object block_position = + block_position_constructor.newInstance( + location.getX(), location.getY(), location.getZ()); + + // check is inside conparing world and invoking method "a". + final Method a = handle.getClass().getMethod("a", block_position_class); + return (location.getWorld().equals(world)) + && (boolean) (a.invoke(handle, block_position)); + } catch (Throwable t) { + t.printStackTrace(); + return false; + } + } + + /** + * Add a {@link BlockFace} mod axis to {@link Location}. + * + *

+ * + * @param location The {@link Location} + * @param face The {@link BlockFace} + * @return Modified location + */ + public static Location add(final Location location, final BlockFace face) { + add(location, face, 1); + return location; + } + + /** + * Add a {@link BlockFace} mod axis to {@link Location}. + * + *

+ * + * @param location The {@link Location} + * @param face The {@link BlockFace} + * @param num The blocks amount + * @return Modified location + */ + public static Location add(final Location location, final BlockFace face, double num) { + location.add(face.getModX() * num, face.getModY() * num, face.getModZ() * num); + return location; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector.java new file mode 100644 index 0000000..8c616ef --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector.java @@ -0,0 +1,116 @@ +package com.pepedevs.corelib.utils.math; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.serialization.ConfigurationSerializable; + +/** + * Encapsulates a immutable Vector. Methods like {@link #normalize()} will not modify this Vector, + * and a new Vector containing the result will be returned instead. + * + *

Also values like {@link #length()} are cached for a better performance. + */ +public interface Vector extends ConfigurationSerializable { + + /** A "close to zero" float epsilon value for use */ + public static final float FLOAT_EPSILON = Float.intBitsToFloat(0x34000000); + + /** + * Gets the magnitude of the Vector, defined as sqrt ( x^2 + y^2 + z^2 ). The value of this + * method is cached, so repeatedly calling this method to get the vector's magnitude will not + * re-calculate it. NaN will be returned if the inner result of the sqrt() function overflows, + * which will be caused if the length is too long. + * + *

This is the equivalent of using: + * + *

Math.sqrt ( {@link #lengthSquared()} )
+ * + *

+ * + * @return Magnitude + */ + public float length(); + + /** + * Gets the magnitude of the vector squared. + * + *

+ * + * @return Magnitude + */ + public float lengthSquared(); + + /** + * Gets the unit equivalent of this vector. ( a Vector with length of 1 ). + * + *

+ * + * @return Created unit vector from this + */ + public Vector normalize(); + + /** + * Gets the 2D equivalent of this vector. + * + *

+ * + * @return Equivalent, or the same vector if called from an instance of the same class. + */ + public Vector2D toVector2D(); + + /** + * Gets the 3D equivalent of this vector. + * + *

Note that calling this method from {@link Vector2D} will result in an {@link + * UnsupportedOperationException}. The method {@link Vector2D#toVector3D(float)} must be used + * instead. + * + *

+ * + * @return Equivalent, or the same vector if called from an instance of the same class. + */ + public Vector3D toVector3D(); + + /** + * Gets the Bukkit equivalent of this vector. + * + *

Note that calling this method from {@link Vector2D} will result in an {@link + * UnsupportedOperationException}. The method {@link Vector2D#toBukkit(double)} must be used + * instead. + * + *

+ * + * @return The equivalent + */ + public org.bukkit.util.Vector toBukkit(); + + /** + * Gets a Location version of this Vector. + * + *

Note that calling this method from {@link Vector2D} will result in an {@link + * UnsupportedOperationException}. The method {@link Vector2D#toLocation(World, double, float, + * float)} must be used instead. + * + *

+ * + * @param world World to link the location to + * @param yaw Desired yaw + * @param pitch Desired pitch + * @return Location + */ + public Location toLocation(final World world, final float yaw, final float pitch); + + /** + * Gets a Location version of this Vector with yaw and pitch being 0. + * + *

Note that calling this method from {@link Vector2D} will result in an {@link + * UnsupportedOperationException}. The method {@link Vector2D#toLocation(World, double)} must be + * used instead. + * + *

+ * + * @param world World to link the location to + * @return Location + */ + public Location toLocation(final World world); +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector2D.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector2D.java new file mode 100644 index 0000000..b40e9e8 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector2D.java @@ -0,0 +1,302 @@ +package com.pepedevs.corelib.utils.math; + +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.HashMap; +import java.util.Map; + +/** + * Encapsulates an immutable cached Vector of two dimensions ( x and y ). + * + *

The values of {@link #length()}, {@link #lengthSquared()}, {@link #normalize()} and {@link + * #hashCode()} are cached for a better performance. + */ +public class Vector2D implements Vector { + + public static final Vector2D ZERO = new Vector2D(0F, 0F); + public static final Vector2D ONE = new Vector2D(1F, 1F); + public static final Vector2D X = new Vector2D(1F, 0F); + public static final Vector2D Y = new Vector2D(0F, 1F); + + protected final float x; + protected final float y; + + /* cached values */ + private float length_squared = Float.NaN; + private float length = Float.NaN; + + private Vector2D normalized = null; + + private boolean hashed = false; + private int hashcode = 0; + + private int x_bits = 0; + private int y_bits = 0; + private boolean bitset = false; + + public Vector2D(final float x, final float y) { + this.x = x; + this.y = y; + } + + public Vector2D(final int x, final int y) { + this((float) x, (float) y); + } + + public Vector2D(final double x, final double y) { + this((float) x, (float) y); + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public Vector2D add(final Vector2D other) { + return new Vector2D((this.x + other.x), (this.y + other.y)); + } + + public Vector2D add(final float x, final float y) { + return new Vector2D((this.x + x), (this.y + y)); + } + + public Vector2D subtract(final Vector2D other) { + return new Vector2D((this.x - other.x), (this.y - other.y)); + } + + public Vector2D subtract(final float x, final float y) { + return new Vector2D((this.x - x), (this.y - y)); + } + + public Vector2D multiply(final Vector2D other) { + return new Vector2D((this.x * other.x), (this.y * other.y)); + } + + public Vector2D multiply(final float x, final float y) { + return new Vector2D((this.x * x), (this.y * y)); + } + + public Vector2D divide(final Vector2D other) { + return new Vector2D((this.x / other.x), (this.y / other.y)); + } + + public Vector2D divide(final float x, final float y) { + return new Vector2D((this.x / x), (this.y / y)); + } + + @Override + public float length() { + if (Float.isNaN(length)) { + this.length = (float) Math.sqrt(lengthSquared()); + } + return length; + } + + @Override + public float lengthSquared() { + if (Float.isNaN(length_squared)) { + this.length_squared = (x * x) + (y * y); + } + return length_squared; + } + + public float distance(final Vector2D other) { + return (float) Math.sqrt(distanceSquared(other)); + } + + public float distanceSquared(final Vector2D other) { + final float x_d = (this.x - other.x); + final float y_d = (this.y - other.y); + + return ((x_d * x_d) + (y_d * y_d)); + } + + /** + * Gets the angle between this vector and another in degrees. + * + *

+ * + * @param other Other vector + * @return Angle in degrees + */ + public float angle(final Vector2D other) { + return (float) Math.toDegrees(Math.atan2(crossProduct(other), dotProduct(other))); + } + + public Vector2D midpoint(final Vector2D other) { + final float x = (this.x + other.x) / 2; + final float y = (this.y + other.y) / 2; + + return new Vector2D(x, y); + } + + public float dotProduct(final Vector2D other) { + return (this.x * other.x) + (this.y * other.y); + } + + public float crossProduct(final Vector2D other) { + return (this.x * other.y) - (this.y * other.x); + } + + @Override + public Vector2D normalize() { + if (this.normalized == null) { + if (Math.abs(length()) < FLOAT_EPSILON) { + this.normalized = Vector2D.ZERO; + } else { + this.normalized = new Vector2D((this.x / length()), (this.y / length())); + } + } + return normalized; + } + + @Override + public Vector2D toVector2D() { + return this; + } + + @Override + public Vector3D toVector3D() { + throw new UnsupportedOperationException("not supported ( missing z component )"); + } + + public Vector3D toVector3D(final float z) { + return new Vector3D(this.x, this.y, z); + } + + /** + * Creates and returns a new {@link Vector3D} by specifying the components to provide, following + * the rules explain below: + * + *

-1 represents the value of {@link #getX()} in this
+     * vector.
+ * + *
0 represents the value of {@link #getY()} in this
+     * vector.
+ * + *
1 represents 0.
+ * + *

+ * + * @param x Providing -1, will construct the Vector3D with its x component + * having the same value as the x component of this vector. Providing 0, + * will construct the Vector3D with its x component having the same value as the y component + * of this vector. Providing 1, will construct the Vector3D with its x + * being 0. + * @param y Providing -1, will construct the Vector3D with its y component + * having the same value as the x component of this vector. Providing 0, + * will construct the Vector3D with its y component having the same value as the y component + * of this vector. Providing 1, will construct the Vector3D with its y + * being 0. + * @param z Providing -1, will construct the Vector3D with its z component + * having the same value as the x component of this vector. Providing 0, + * will construct the Vector3D with its z component having the same value as the y component + * of this vector. Providing 1, will construct the Vector3D with its z + * being 0. + * @return the 3D version. + */ + public Vector3D toVector3D(final int x, final int y, final int z) { + // -1 = x + // 0 = y + // 1 = 0 + + final float vector_x = x < 0 ? this.x : (x == 0 ? this.y : 0F); + final float vector_y = y < 0 ? this.x : (x == 0 ? this.y : 0F); + final float vector_z = z < 0 ? this.x : (x == 0 ? this.y : 0F); + + return new Vector3D(vector_x, vector_y, vector_z); + } + + @Override + public org.bukkit.util.Vector toBukkit() { + throw new UnsupportedOperationException("not supported ( missing z component )"); + } + + public org.bukkit.util.Vector toBukkit(final double z) { + return new org.bukkit.util.Vector(this.x, this.y, z); + } + + @Override + public Location toLocation(final World world, final float yaw, final float pitch) { + throw new UnsupportedOperationException("not supported ( missing z component )"); + } + + public Location toLocation( + final World world, final double z, final float yaw, final float pitch) { + return new Location(world, this.x, this.y, z, yaw, pitch); + } + + @Override + public Location toLocation(final World world) { + throw new UnsupportedOperationException("not supported ( missing z component )"); + } + + public Location toLocation(final World world, final double z) { + return toLocation(world, z, 0F, 0F); + } + + @Override + public String toString() { + return "(" + x + ", " + y + ")"; + } + + @Override + public Map serialize() { + final Map serialized = new HashMap<>(); + + serialized.put("x", this.x); + serialized.put("y", this.y); + + return serialized; + } + + @Override + public int hashCode() { + if (!hashed) { + this.hashcode = 7; + this.hashcode = + 79 * hashcode + + (int) + (Float.floatToIntBits(this.x) + ^ (Float.floatToIntBits(this.x) >>> 32)); + this.hashcode = + 79 * hashcode + + (int) + (Float.floatToIntBits(this.y) + ^ (Float.floatToIntBits(this.y) >>> 32)); + + this.hashed = true; + } + return hashcode; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof Vector2D)) { + return false; + } + + Vector2D other = (Vector2D) obj; + this.bitset(); + other.bitset(); + + return (this.x_bits == other.x_bits) && (this.y_bits == other.y_bits); + } + + protected void bitset() { + if (!bitset) { + this.x_bits = Float.floatToIntBits(this.x); + this.y_bits = Float.floatToIntBits(this.y); + + this.bitset = true; + } + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector3D.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector3D.java new file mode 100644 index 0000000..276d01e --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/Vector3D.java @@ -0,0 +1,311 @@ +package com.pepedevs.corelib.utils.math; + +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.HashMap; +import java.util.Map; + +/** + * Encapsulates an immutable cached Vector of three dimensions ( x, y and z ). + * + *

The values of {@link #length()}, {@link #lengthSquared()}, {@link #normalize()} and {@link + * #hashCode()} are cached for a better performance. + */ +public class Vector3D implements Vector { + + public static final Vector3D ZERO = new Vector3D(0F, 0F, 0F); + public static final Vector3D ONE = new Vector3D(1F, 1F, 1F); + public static final Vector3D X = new Vector3D(1F, 0F, 0F); + public static final Vector3D Y = new Vector3D(0F, 1F, 0F); + public static final Vector3D Z = new Vector3D(0F, 0F, 1F); + + protected final float x; + protected final float y; + protected final float z; + + /* cached values */ + protected float length_squared = Float.NaN; + protected float length = Float.NaN; + + protected Vector3D normalized = null; + + protected boolean hashed = false; + protected int hashcode = 0; + + protected int x_bits = 0; + protected int y_bits = 0; + protected int z_bits = 0; + protected boolean bitset = false; + + public Vector3D(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3D(final int x, final int y, final int z) { + this((float) x, (float) y, (float) z); + } + + public Vector3D(final double x, final double y, final double z) { + this((float) x, (float) y, (float) z); + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public float getZ() { + return z; + } + + public Vector3D add(final Vector3D other) { + return new Vector3D((this.x + other.x), (this.y + other.y), (this.z + other.z)); + } + + public Vector3D add(final float x, final float y, final float z) { + return new Vector3D((this.x + x), (this.y + y), (this.z + z)); + } + + public Vector3D subtract(final Vector3D other) { + return new Vector3D((this.x - other.x), (this.y - other.y), (this.z - other.z)); + } + + public Vector3D subtract(final float x, final float y, final float z) { + return new Vector3D((this.x - x), (this.y - y), (this.z - z)); + } + + public Vector3D multiply(final Vector3D other) { + return new Vector3D((this.x * other.x), (this.y * other.y), (this.z * other.z)); + } + + public Vector3D multiply(final float x, final float y, final float z) { + return new Vector3D((this.x * x), (this.y * y), (this.z * z)); + } + + public Vector3D multiply(final float factor) { + return new Vector3D((this.x * factor), (this.y * factor), (this.z * factor)); + } + + public Vector3D divide(final Vector3D other) { + return new Vector3D((this.x / other.x), (this.y / other.y), (this.z / other.z)); + } + + public Vector3D divide(final float x, final float y, final float z) { + return new Vector3D((this.x / x), (this.y / y), (this.z / z)); + } + + public Vector3D divide(final float factor) { + return new Vector3D((this.x / factor), (this.y / factor), (this.z / factor)); + } + + @Override + public float length() { + if (Float.isNaN(length)) { + this.length = (float) Math.sqrt(lengthSquared()); + } + return length; + } + + @Override + public float lengthSquared() { + if (Float.isNaN(length_squared)) { + this.length_squared = (x * x) + (y * y) + (z * z); + } + return length_squared; + } + + public float distance(final Vector3D other) { + return (float) Math.sqrt(distanceSquared(other)); + } + + public float distanceSquared(final Vector3D other) { + final float x_d = (this.x - other.x); + final float y_d = (this.y - other.y); + final float z_d = (this.z - other.z); + + return (x_d * x_d) + (y_d * y_d) + (z_d * z_d); + } + + /** + * Gets the angle between this vector and another in degrees. + * + *

+ * + * @param other Other vector + * @return Angle in degrees + */ + public float angle(final Vector3D other) { + return (float) Math.toDegrees(Math.acos(dotProduct(other) / (length() * other.length()))); + } + + public Vector3D midpoint(final Vector3D other) { + final float x = (this.x + other.x) / 2; + final float y = (this.y + other.y) / 2; + final float z = (this.z + other.z) / 2; + + return new Vector3D(x, y, z); + } + + public float dotProduct(final Vector3D other) { + return (this.x * other.x) + (this.y * other.y) + (this.z * other.z); + } + + public Vector3D crossProduct(final Vector3D other) { + final float x = (this.y * other.z) - (other.y * this.z); + final float y = (this.z * other.x) - (other.z * this.x); + final float z = (this.x * other.y) - (other.x * this.y); + + return new Vector3D(x, y, z); + } + + @Override + public Vector3D normalize() { + if (this.normalized == null) { + if (Math.abs(length()) < FLOAT_EPSILON) { + this.normalized = Vector3D.ZERO; + } else { + this.normalized = + new Vector3D((this.x / length()), (this.y / length()), (this.z / length())); + } + } + return normalized; + } + + @Override + public Vector2D toVector2D() { + return toVector2D(-1, 0); + } + + /** + * Creates and returns a new {@link Vector2D} by specifying the components to provide, following + * the rules explain below: + * + *

-1 represents the value of {@link #getX()} in this
+     * vector.
+ * + *
0 represents the value of {@link #getY()} in this
+     * vector.
+ * + *
1 represents the value of {@link #getZ()} in this
+     * vector.
+ * + *

+ * + * @param x Providing -1, will construct the Vector2D with its x component + * having the same value as the x component of this vector. Providing 0, + * will construct the Vector2D with its x component having the same value as the y component + * of this vector. Providing 1, will construct the Vector2D with its x + * component having the same value as the z component of this vector. + * @param y Providing -1, will construct the Vector2D with its y component + * having the same value as the x component of this vector. Providing 0, + * will construct the Vector2D with its y component having the same value as the y component + * of this vector. Providing 1, will construct the Vector2D with its y + * component having the same value as the z component of this vector. + * @return 2D version + */ + public Vector2D toVector2D(final int x, final int y) { + // -1 = x + // 0 = y + // 1 = z + + final float vector_x = x < 0 ? this.x : (x == 0 ? this.y : this.z); + final float vector_y = y < 0 ? this.x : (x == 0 ? this.y : this.z); + + return new Vector2D(vector_x, vector_y); + } + + @Override + public Vector3D toVector3D() { + return this; + } + + @Override + public org.bukkit.util.Vector toBukkit() { + return new org.bukkit.util.Vector(this.x, this.y, this.z); + } + + @Override + public Location toLocation(final World world, final float yaw, final float pitch) { + return new Location(world, this.x, this.y, this.z, yaw, pitch); + } + + public Location toLocation(final World world) { + return toLocation(world, 0F, 0F); + } + + @Override + public String toString() { + return "(" + x + ", " + y + ", " + z + ")"; + } + + @Override + public Map serialize() { + final Map serialized = new HashMap<>(); + + serialized.put("x", this.x); + serialized.put("y", this.y); + serialized.put("z", this.z); + + return serialized; + } + + @Override + public int hashCode() { + if (!hashed) { + this.hashcode = 7; + this.hashcode = + 79 * hashcode + + (int) + (Float.floatToIntBits(this.x) + ^ (Float.floatToIntBits(this.x) >>> 32)); + this.hashcode = + 79 * hashcode + + (int) + (Float.floatToIntBits(this.y) + ^ (Float.floatToIntBits(this.y) >>> 32)); + this.hashcode = + 79 * hashcode + + (int) + (Float.floatToIntBits(this.z) + ^ (Float.floatToIntBits(this.z) >>> 32)); + + this.hashed = true; + } + return hashcode; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof Vector3D)) { + return false; + } + + Vector3D other = (Vector3D) obj; + this.bitset(); + other.bitset(); + + return (this.x_bits == other.x_bits) + && (this.y_bits == other.y_bits) + && (this.z_bits == other.z_bits); + } + + protected void bitset() { + if (!bitset) { + this.x_bits = Float.floatToIntBits(this.x); + this.y_bits = Float.floatToIntBits(this.y); + this.z_bits = Float.floatToIntBits(this.z); + + this.bitset = true; + } + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/VectorUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/VectorUtils.java new file mode 100644 index 0000000..1f3a7b6 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/VectorUtils.java @@ -0,0 +1,151 @@ +package com.pepedevs.corelib.utils.math; + +import org.bukkit.util.Vector; + +/** Class for dealing with {@link Vector}s. */ +public class VectorUtils { + + /** + * Generates a random unit vector. + * + *

+ * + * @return New random unit vector. + */ + public static Vector getRandomVector() { + double x = Math.random() * 2 - 1; + double y = Math.random() * 2 - 1; + double z = Math.random() * 2 - 1; + + return new Vector(x, y, z).normalize(); + } + + /** + * Generates a vector pointing to a random direction over a circle. + * + *

+ * + * @return Random vector. + */ + public static Vector getRandomCircleVector() { + double rnd = Math.random() * 2 * Math.PI; + + double x = Math.cos(rnd); + double z = Math.sin(rnd); + + return new Vector(x, 0, z); + } + + /** + * Rotates a vector around the X axis at an angle + * + *

+ * + * @param v Starting vector + * @param angle How much to rotate + * @return The starting vector rotated + */ + public static final Vector rotateAroundAxisX(Vector v, double angle) { + double y, z, cos, sin; + cos = Math.cos(angle); + sin = Math.sin(angle); + y = v.getY() * cos - v.getZ() * sin; + z = v.getY() * sin + v.getZ() * cos; + return v.setY(y).setZ(z); + } + + /** + * Rotates a vector around the Y axis at an angle + * + *

+ * + * @param v Starting vector + * @param angle How much to rotate + * @return The starting vector rotated + */ + public static final Vector rotateAroundAxisY(Vector v, double angle) { + double x, z, cos, sin; + cos = Math.cos(angle); + sin = Math.sin(angle); + x = v.getX() * cos + v.getZ() * sin; + z = v.getX() * -sin + v.getZ() * cos; + return v.setX(x).setZ(z); + } + + /** + * Rotates a vector around the Z axis at an angle + * + *

+ * + * @param v Starting vector + * @param angle How much to rotate + * @return The starting vector rotated + */ + public static final Vector rotateAroundAxisZ(Vector v, double angle) { + double x, y, cos, sin; + cos = Math.cos(angle); + sin = Math.sin(angle); + x = v.getX() * cos - v.getY() * sin; + y = v.getX() * sin + v.getY() * cos; + return v.setX(x).setY(y); + } + + /** + * Rotates a vector around the X, Y, and Z axes + * + *

+ * + * @param v The starting vector + * @param angleX The change angle on X + * @param angleY The change angle on Y + * @param angleZ The change angle on Z + * @return The starting vector rotated + */ + public static final Vector rotateVector(Vector v, double angleX, double angleY, double angleZ) { + rotateAroundAxisX(v, angleX); + rotateAroundAxisY(v, angleY); + rotateAroundAxisZ(v, angleZ); + return v; + } + + /** + * This handles non-unit vectors, with yaw and pitch instead of X,Y,Z angles. + * + *

+ * + * @param vector The starting vector + * @param yawDegrees The yaw offset in degrees + * @param pitchDegrees The pitch offset in degrees + * @return The starting vector rotated + */ + public static final Vector rotateVector(Vector vector, float yawDegrees, float pitchDegrees) { + // get radians. + double yaw = Math.toRadians(-yawDegrees); + double pitch = Math.toRadians(-pitchDegrees); + + // get yaw/pitch sine and cosine. + double cosYaw = Math.cos(yaw); + double cosPitch = Math.cos(pitch); + double sinYaw = Math.sin(yaw); + double sinPitch = Math.sin(pitch); + + // axis. + double initialX, initialY, initialZ; + double x, y, z; + + // Z_axis rotation (Pitch) + initialX = vector.getX(); + initialY = vector.getY(); + x = initialX * cosPitch - initialY * sinPitch; + y = initialX * sinPitch + initialY * cosPitch; + + // Y_axis rotation (Yaw) + initialZ = vector.getZ(); + initialX = x; + z = initialZ * cosYaw - initialX * sinYaw; + x = initialZ * sinYaw + initialX * cosYaw; + + // return a new vector with calculated axis. + return new Vector(x, y, z); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/collision/BoundingBox.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/collision/BoundingBox.java new file mode 100644 index 0000000..2236be7 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/collision/BoundingBox.java @@ -0,0 +1,523 @@ +package com.pepedevs.corelib.utils.math.collision; + +import org.bukkit.util.Vector; + +import java.util.Collection; + +/** + * Encapsulates an axis aligned bounding box represented by a minimum and a maximum Vector. + * Additionally you can query for the bounding box's center, dimensions and corner points. + */ +public class BoundingBox { + + public static final BoundingBox ZERO = + new BoundingBox(new Vector(0D, 0D, 0D), new Vector(0D, 0D, 0D)); + public static final BoundingBox BLOCK = + new BoundingBox(new Vector(0D, 0D, 0D), new Vector(1D, 1D, 1D)); + public static final BoundingBox INFINITY = + new BoundingBox( + new Vector( + Double.POSITIVE_INFINITY, + Double.POSITIVE_INFINITY, + Double.POSITIVE_INFINITY), + new Vector( + Double.NEGATIVE_INFINITY, + Double.NEGATIVE_INFINITY, + Double.NEGATIVE_INFINITY)); + + protected final Vector minimum; + protected final Vector maximum; + + protected final Vector center; + protected final Vector dimensions; + + /** + * Constructs the new bounding box using the provided minimum and maximum vector. + * + * @param minimum the minimum vector. + * @param maximum the maximum vector. + */ + public BoundingBox(final Vector minimum, final Vector maximum) { + if (Double.isInfinite(minimum.getX()) + && Double.isInfinite(minimum.getY()) + && Double.isInfinite(minimum.getZ()) + && Double.isInfinite(maximum.getX()) + && Double.isInfinite(maximum.getY()) + && Double.isInfinite(maximum.getZ())) { + this.minimum = minimum; + this.maximum = maximum; + + this.center = new Vector(0D, 0D, 0D); + this.dimensions = new Vector(0D, 0D, 0D); + } else { + this.minimum = + new Vector( + Math.min(minimum.getX(), maximum.getX()), + Math.min(minimum.getY(), maximum.getY()), + Math.min(minimum.getZ(), maximum.getZ())); + + this.maximum = + new Vector( + Math.max(minimum.getX(), maximum.getX()), + Math.max(minimum.getY(), maximum.getY()), + Math.max(minimum.getZ(), maximum.getZ())); + + this.center = this.minimum.clone().add(this.maximum).multiply(0.5F); + this.dimensions = this.maximum.clone().subtract(this.minimum); + } + } + + /** + * Constructs the new bounding box using the provided minimum and maximum coordinates. + * + *

+ * + * @param minimum_x the minimum x. + * @param minimum_y the minimum y. + * @param minimum_z the minimum z. + * @param maximum_x the maximum x. + * @param maximum_y the maximum y. + * @param maximum_z the maximum z. + */ + public BoundingBox( + Double minimum_x, + Double minimum_y, + Double minimum_z, + Double maximum_x, + Double maximum_y, + Double maximum_z) { + this( + new Vector(minimum_x, minimum_y, minimum_z), + new Vector(maximum_x, maximum_y, maximum_z)); + } + + /** + * Constructs the new bounding box incorporating the provided points to calculate the minimum + * and maximum. + * + *

+ * + * @param points the points to incorporate. + */ + public BoundingBox(Vector... points) { + BoundingBox temp = INFINITY; + for (Vector point : points) { + temp = temp.extend(point); + } + + this.minimum = temp.minimum; + this.maximum = temp.maximum; + + this.center = temp.center; + this.dimensions = temp.dimensions; + } + + /** + * Constructs the new bounding box incorporating the provided points to calculate the minimum + * and maximum. + * + *

+ * + * @param points the points to incorporate. + */ + public BoundingBox(Collection points) { + this(points.toArray(new Vector[points.size()])); + } + + /** + * Gets the minimum vector of this bounding box. + * + *

+ * + * @return a new copy of the minimum vector. + */ + public Vector getMinimum() { + return minimum.clone(); + } + + /** + * Gets the maximum vector of this bounding box. + * + *

+ * + * @return a new copy of the maximum Vector. + */ + public Vector getMaximum() { + return maximum.clone(); + } + + /** + * Gets the center of this bounding box. + * + *

+ * + * @return a new copy of the center Vector of this bounding box. + */ + public Vector getCenter() { + return center.clone(); + } + + /** + * Gets the dimensions of this bounding box. + * + *

+ * + * @return a new copy of the dimensions Vector of this bounding box. + */ + public Vector getDimensions() { + return dimensions.clone(); + } + + /** + * Gets the width of this bounding box. + * + *

This is the equivalent of using: {@code BoundingBox.getDimensions().getX()}. + * + *

+ * + * @return the width of the bounding box. + */ + public Double getWidth() { + return dimensions.getX(); + } + + /** + * Gets the height of this bounding box. + * + *

This is the equivalent of using: {@code BoundingBox.getDimensions().getY()}. + * + *

+ * + * @return the height of the bounding box. + */ + public Double getHeight() { + return dimensions.getY(); + } + + /** + * Gets the depth of this bounding box. + * + *

This is the equivalent of using: {@code BoundingBox.getDimensions().getZ()}. + * + *

+ * + * @return the depth of the bounding box. + */ + public Double getDepth() { + return dimensions.getZ(); + } + + /* BoundingBox Corners */ + + public Vector getCorner000() { + return new Vector(minimum.getX(), minimum.getY(), minimum.getZ()); + } + + public Vector getCorner001() { + return new Vector(minimum.getX(), minimum.getY(), maximum.getZ()); + } + + public Vector getCorner010() { + return new Vector(minimum.getX(), maximum.getY(), minimum.getZ()); + } + + public Vector getCorner011() { + return new Vector(minimum.getX(), maximum.getY(), maximum.getZ()); + } + + public Vector getCorner100() { + return new Vector(maximum.getX(), minimum.getY(), minimum.getZ()); + } + + public Vector getCorner101() { + return new Vector(maximum.getX(), minimum.getY(), maximum.getZ()); + } + + public Vector getCorner110() { + return new Vector(maximum.getX(), maximum.getY(), minimum.getZ()); + } + + public Vector getCorner111() { + return new Vector(maximum.getX(), maximum.getY(), maximum.getZ()); + } + + public Vector[] getCorners() { + return new Vector[] { + getCorner000(), + getCorner001(), + getCorner010(), + getCorner011(), + getCorner100(), + getCorner101(), + getCorner110(), + getCorner111() + }; + } + + /** + * Adds the provided location {@link Vector}. + * + *

This is commonly used for locating unit bounding boxes. + * + *

+ * + * @param location the location to add. + * @return a new BoundingBox containing the addition result. + */ + public BoundingBox add(Vector location) { + return new BoundingBox(minimum.add(location), maximum.add(location)); + } + + /** + * Adds the provided location coordinates. + * + *

This is commonly used for locating unit bounding boxes. + * + *

+ * + * @param x the x coordinate. + * @param y the y coordinate. + * @param z the z coordinate. + * @return a new BoundingBox containing the addition result. + */ + public BoundingBox add(Double x, Double y, Double z) { + return add(new Vector(x, y, z)); + } + + /** + * Subtract by the provided location {@link Vector}. + * + *

+ * + * @param location the location to subtract. + * @return a new BoundingBox containing the subtraction result. + */ + public BoundingBox subtract(Vector location) { + return new BoundingBox(minimum.subtract(location), maximum.subtract(location)); + } + + /** + * Subtract by the provided location coordinates. + * + *

+ * + * @param x the x coordinate. + * @param y the y coordinate. + * @param z the z coordinate. + * @return a new BoundingBox containing the subtraction result. + */ + public BoundingBox subtract(Double x, Double y, Double z) { + return subtract(new Vector(x, y, z)); + } + + /** + * Multiply by the provided location {@link Vector}. + * + *

+ * + * @param location the location to multiply. + * @return a new BoundingBox containing the multiplication result. + */ + public BoundingBox multiply(Vector location) { + return new BoundingBox(minimum.multiply(location), maximum.multiply(location)); + } + + /** + * Multiply by the provided location coordinates. + * + *

+ * + * @param x the x coordinate. + * @param y the y coordinate. + * @param z the z coordinate. + * @return a new BoundingBox containing the multiplication result. + */ + public BoundingBox multiply(Double x, Double y, Double z) { + return multiply(new Vector(x, y, z)); + } + + /** + * Divide by the provided location {@link Vector}. + * + *

+ * + * @param location the location to divide. + * @return a new BoundingBox containing the division result. + */ + public BoundingBox divide(Vector location) { + return new BoundingBox(minimum.divide(location), maximum.divide(location)); + } + + /** + * Divide by the provided location coordinates. + * + *

+ * + * @param x the x coordinate. + * @param y the y coordinate. + * @param z the z coordinate. + * @return a new BoundingBox containing the division result. + */ + public BoundingBox divide(Double x, Double y, Double z) { + return divide(new Vector(x, y, z)); + } + + /** + * Extends this bounding box by the provided bounding box. + * + *

+ * + * @param a_bounds the other bounding box. + * @return a new BoundingBox containing the extension result. + */ + public BoundingBox extend(BoundingBox a_bounds) { + return new BoundingBox( + new Vector( + Math.min(minimum.getX(), a_bounds.minimum.getX()), + Math.min(minimum.getY(), a_bounds.minimum.getY()), + Math.min(minimum.getZ(), a_bounds.minimum.getZ())), + new Vector( + Math.max(maximum.getX(), a_bounds.maximum.getX()), + Math.max(maximum.getY(), a_bounds.maximum.getY()), + Math.max(maximum.getZ(), a_bounds.maximum.getZ()))); + } + + /** + * Extends this bounding box to incorporate the provided {@link Vector}. + * + *

+ * + * @param point the point to incorporate. + * @return a new BoundingBox containing the extension result. + */ + public BoundingBox extend(Vector point) { + return extend(point.getX(), point.getY(), point.getZ()); + } + + /** + * Extends this bounding box to incorporate the provided coordinates. + * + *

+ * + * @param x the x-coordinate. + * @param y the y-coordinate. + * @param z the z-coordinate. + * @return a new BoundingBox containing the extension result. + */ + public BoundingBox extend(Double x, Double y, Double z) { + return new BoundingBox( + new Vector( + Math.min(minimum.getX(), x), + Math.min(minimum.getY(), y), + Math.min(minimum.getZ(), z)), + new Vector( + Math.max(maximum.getX(), x), + Math.max(maximum.getY(), y), + Math.max(maximum.getZ(), z))); + } + + /** + * Extends this bounding box by the given sphere. + * + *

+ * + * @param center the sphere center. + * @param radius the sphere radius. + * @return a new BoundingBox containing the extension result. + */ + public BoundingBox extend(Vector center, Double radius) { + return new BoundingBox( + new Vector( + Math.min(minimum.getX(), (center.getX() - radius)), + Math.min(minimum.getY(), (center.getY() - radius)), + Math.min(minimum.getZ(), (center.getZ() - radius))), + new Vector( + Math.max(maximum.getX(), (center.getX() + radius)), + Math.max(maximum.getY(), (center.getY() + radius)), + Math.max(maximum.getZ(), (center.getZ() + radius)))); + } + + /** + * Gets whether the provided {@link BoundingBox} is intersecting this bounding box ( at least + * one point in ). + * + *

+ * + * @param other the bounding box to check. + * @return true if intersecting. + */ + public boolean intersects(final BoundingBox other) { + /* test using SAT (separating axis theorem) */ + double lx = Math.abs(this.center.getX() - other.center.getX()); + double sumx = (this.dimensions.getX() / 2.0F) + (other.dimensions.getX() / 2.0F); + + double ly = Math.abs(this.center.getY() - other.center.getY()); + double sumy = (this.dimensions.getY() / 2.0F) + (other.dimensions.getY() / 2.0F); + + double lz = Math.abs(this.center.getZ() - other.center.getZ()); + double sumz = (this.dimensions.getZ() / 2.0F) + (other.dimensions.getZ() / 2.0F); + + return (lx <= sumx && ly <= sumy && lz <= sumz); + } + + public boolean contains(final Vector vector) { + return (minimum.getX() <= vector.getX() + && maximum.getX() >= vector.getX() + && minimum.getY() <= vector.getY() + && maximum.getY() >= vector.getY() + && minimum.getZ() <= vector.getZ() + && maximum.getZ() >= vector.getZ()); + } + + public boolean contains(final BoundingBox other) { + return (minimum.getX() <= other.minimum.getX() + && minimum.getY() <= other.minimum.getY() + && minimum.getZ() <= other.minimum.getZ() + && maximum.getX() >= other.maximum.getX() + && maximum.getY() >= other.maximum.getY() + && maximum.getZ() >= other.maximum.getZ()); + } + + public boolean isValid() { + return (minimum.getX() <= maximum.getX() + && minimum.getY() <= maximum.getY() + && minimum.getZ() <= maximum.getZ()); + } + + @Override + public String toString() { + return "[ " + minimum.toString() + " | " + maximum.toString() + " ]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((center == null) ? 0 : center.hashCode()); + result = prime * result + ((dimensions == null) ? 0 : dimensions.hashCode()); + result = prime * result + ((maximum == null) ? 0 : maximum.hashCode()); + result = prime * result + ((minimum == null) ? 0 : minimum.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof BoundingBox)) { + return false; + } + + BoundingBox other = (BoundingBox) obj; + if (!maximum.equals(other.maximum) || !minimum.equals(other.minimum)) { + return false; + } + + if (!center.equals(other.center) || !dimensions.equals(other.dimensions)) { + return false; + } + return true; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/math/collision/Ray.java b/utils/src/main/java/com/pepedevs/corelib/utils/math/collision/Ray.java new file mode 100644 index 0000000..66f0651 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/math/collision/Ray.java @@ -0,0 +1,108 @@ +package com.pepedevs.corelib.utils.math.collision; + +import org.bukkit.Location; +import org.bukkit.util.Vector; + +import java.util.Objects; + +/** Encapsulates an immutable ray having a starting position and a unit length direction. */ +public class Ray { + + protected final Vector origin; + protected final Vector direction; + + /** + * Constructs the ray. + * + *

+ * + * @param origin Starting position + * @param direction Direction + */ + public Ray(final Vector origin, final Vector direction) { + this.origin = origin.clone(); + this.direction = direction.clone().normalize(); + } + + /** + * Constructs the ray. + * + *

+ * + * @param origin Starting position + * @param direction Direction + */ + public Ray(final Location origin, final Vector direction) { + this(origin.toVector(), direction); + } + + /** + * Constructs the ray from the provided {@link Location}, setting the {@link #origin} from its + * position coordinates (x, y and z), and the {@link #direction} from its direction. + * + *

+ * + * @param data Location + */ + public Ray(final Location data) { + this(data, data.getDirection()); + } + + /** + * Gets the ray's origin location vector ( a vector representing a location ). + * + *

+ * + * @return New copy of the ray's origin. + */ + public Vector getOrigin() { + return origin.clone(); + } + + /** + * Gets the ray's direction vector. + * + *

+ * + * @return New copy of the ray's origin + */ + public Vector getDirection() { + return direction.clone(); + } + + /** + * Returns the endpoint given the distance. This is calculated as startpoint + distance * + * direction. + * + *

+ * + * @param distance Distance from the end point to the start point + * @return Endpoint result + */ + public Vector getEndPoint(final double distance) { + return direction.clone().multiply(distance).add(origin); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof Ray) { + return Objects.equals(((Ray) obj).origin, origin) + && Objects.equals(((Ray) obj).direction, direction); + } else { + return false; + } + } + + @Override + public int hashCode() { + final int prime = 73; + int result = 1; + result = prime * result + this.direction.hashCode(); + result = prime * result + this.origin.hashCode(); + return result; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/reflection/PacketConstant.java b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/PacketConstant.java new file mode 100644 index 0000000..e9e3a6d --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/PacketConstant.java @@ -0,0 +1,94 @@ +package com.pepedevs.corelib.utils.reflection; + +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.NMSClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; + +public class PacketConstant { + + public static final ClassWrapper PACKET_CLASS; + public static final ClassWrapper PACKET_PLAY_OUT_ANIMATION; + public static final ClassWrapper PACKET_PLAY_OUT_ATTACH_ENTITY; + public static final ClassWrapper PACKET_PLAY_OUT_BLOCK_ACTION; + public static final ClassWrapper PACKET_PLAY_OUT_BLOCK_BREAK; + public static final ClassWrapper PACKET_PLAY_OUT_BLOCK_BREAK_ANIMATION; + public static final ClassWrapper PACKET_PLAY_OUT_BLOCK_CHANGE; + public static final ClassWrapper PACKET_PLAY_OUT_BOSS; + public static final ClassWrapper PACKET_PLAY_OUT_CHAT; + public static final ClassWrapper PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_DESTROY; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_EFFECT; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_EQUIPMENT; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_HEAD_ROTATION; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_METADATA; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_SOUND; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_STATUS; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_TELEPORT; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_VELOCITY; + public static final ClassWrapper PACKET_PLAY_OUT_ENTITY_EXPERIENCE; + public static final ClassWrapper PACKET_PLAY_OUT_EXPLOSION; + public static final ClassWrapper PACKET_PLAY_OUT_HELD_ITEM_SLOT; + public static final ClassWrapper PACKET_PLAY_OUT_MULTI_BLOCK_CHANGE; + public static final ClassWrapper PACKET_PLAY_OUT_NAMED_ENTITY_SPAWN; + public static final ClassWrapper PACKET_PLAY_OUT_PLAYER_INFO; + public static final ClassWrapper PACKET_PLAY_OUT_PLAYER_LIST_HEADER_FOOTER; + public static final ClassWrapper PACKET_PLAY_OUT_POSITION; + public static final ClassWrapper PACKET_PLAY_OUT_REMOVE_ENTITY_EFFECT; + public static final ClassWrapper PACKET_PLAY_OUT_RESPAWN; + public static final ClassWrapper PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJECTIVE; + public static final ClassWrapper PACKET_PLAY_OUT_SCOREBOARD_OBJECTIVE; + public static final ClassWrapper PACKET_PLAY_OUT_SCOREBOARD_SCORE; + public static final ClassWrapper PACKET_PLAY_OUT_SCOREBOARD_TEAM; + public static final ClassWrapper PACKET_PLAY_OUT_SPAWN_ENTITY; + public static final ClassWrapper PACKET_PLAY_OUT_SPAWN_ENTITY_EXPERIENCE_ORB; + public static final ClassWrapper PACKET_PLAY_OUT_SPAWN_ENTITY_LIVING; + public static final ClassWrapper PACKET_PLAY_OUT_STATISTIC; + public static final ClassWrapper PACKET_PLAY_OUT_TAB_COMPLETE; + public static final ClassWrapper PACKET_PLAY_OUT_UPDATE_HEALTH; + public static final ClassWrapper PACKET_PLAY_OUT_UPDATE_TIME; + + static { + NMSClassResolver nmsClassResolver = new NMSClassResolver(); + String packetPackageV17 = "net.minecraft.network.protocol.game."; + PACKET_CLASS = nmsClassResolver.resolveWrapper("Packet", "net.minecraft.network.protocol.Packet"); + PACKET_PLAY_OUT_ANIMATION = nmsClassResolver.resolveWrapper("PacketPlayOutAnimation", packetPackageV17 + "PacketPlayOutAnimation"); + PACKET_PLAY_OUT_ATTACH_ENTITY = nmsClassResolver.resolveWrapper("PacketPlayOutAttachEntity", packetPackageV17 + "PacketPlayOutAttachEntity"); + PACKET_PLAY_OUT_BLOCK_ACTION = nmsClassResolver.resolveWrapper("PacketPlayOutBlockAction", packetPackageV17 + "PacketPlayOutBlockAction"); + PACKET_PLAY_OUT_BLOCK_BREAK = nmsClassResolver.resolveWrapper("PacketPlayOutBlockBreak", packetPackageV17 + "PacketPlayOutBlockBreak"); + PACKET_PLAY_OUT_BLOCK_BREAK_ANIMATION = nmsClassResolver.resolveWrapper("PacketPlayOutBlockBreakAnimation", packetPackageV17 + "PacketPlayOutBlockBreakAnimation"); + PACKET_PLAY_OUT_BLOCK_CHANGE = nmsClassResolver.resolveWrapper("PacketPlayOutBlockChange", packetPackageV17 + "PacketPlayOutBlockChange"); + PACKET_PLAY_OUT_BOSS = nmsClassResolver.resolveWrapper("PacketPlayOutBoss", packetPackageV17 + "PacketPlayOutBoss"); + PACKET_PLAY_OUT_CHAT = nmsClassResolver.resolveWrapper("PacketPlayOutChat", packetPackageV17 + "PacketPlayOutChat"); + PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT = nmsClassResolver.resolveWrapper("PacketPlayOutCustomSoundEffect", packetPackageV17 + "PacketPlayOutCustomSoundEffect"); + PACKET_PLAY_OUT_ENTITY_DESTROY = nmsClassResolver.resolveWrapper("PacketPlayOutEntityDestroy", packetPackageV17 + "PacketPlayOutEntityDestroy"); + PACKET_PLAY_OUT_ENTITY_EFFECT = nmsClassResolver.resolveWrapper("PacketPlayOutEntitySound", packetPackageV17 + "PacketPlayOutEntitySound"); + PACKET_PLAY_OUT_ENTITY_EQUIPMENT = nmsClassResolver.resolveWrapper("PacketPlayOutEntityEquipment", packetPackageV17 + "PacketPlayOutEntityEquipment"); + PACKET_PLAY_OUT_ENTITY_HEAD_ROTATION = nmsClassResolver.resolveWrapper("PacketPlayOutEntityHeadRotation", packetPackageV17 + "PacketPlayOutEntityHeadRotation"); + PACKET_PLAY_OUT_ENTITY_METADATA = nmsClassResolver.resolveWrapper("PacketPlayOutEntityMetadata", packetPackageV17 + "PacketPlayOutEntityMetadata"); + PACKET_PLAY_OUT_ENTITY_SOUND = nmsClassResolver.resolveWrapper("PacketPlayOutEntitySound", packetPackageV17 + "PacketPlayOutEntitySound"); + PACKET_PLAY_OUT_ENTITY_STATUS = nmsClassResolver.resolveWrapper("PacketPlayOutEntityStatus", packetPackageV17 + "PacketPlayOutEntityStatus"); + PACKET_PLAY_OUT_ENTITY_TELEPORT = nmsClassResolver.resolveWrapper("PacketPlayOutEntityTeleport", packetPackageV17 + "PacketPlayOutEntityTeleport"); + PACKET_PLAY_OUT_ENTITY_VELOCITY = nmsClassResolver.resolveWrapper("PacketPlayOutEntityVelocity", packetPackageV17 + "PacketPlayOutEntityVelocity"); + PACKET_PLAY_OUT_ENTITY_EXPERIENCE = nmsClassResolver.resolveWrapper("PacketPlayOutExperience", packetPackageV17 + "PacketPlayOutExperience"); + PACKET_PLAY_OUT_EXPLOSION = nmsClassResolver.resolveWrapper("PacketPlayOutExplosion", packetPackageV17 + "PacketPlayOutExplosion"); + PACKET_PLAY_OUT_HELD_ITEM_SLOT = nmsClassResolver.resolveWrapper("PacketPlayOutHeldItemSlot", packetPackageV17 + "PacketPlayOutHeldItemSlot"); + PACKET_PLAY_OUT_MULTI_BLOCK_CHANGE = nmsClassResolver.resolveWrapper("PacketPlayOutMultiBlockChange", packetPackageV17 + "PacketPlayOutMultiBlockChange"); + PACKET_PLAY_OUT_NAMED_ENTITY_SPAWN = nmsClassResolver.resolveWrapper("PacketPlayOutNamedEntitySpawn", packetPackageV17 + "PacketPlayOutNamedEntitySpawn"); + PACKET_PLAY_OUT_PLAYER_INFO = nmsClassResolver.resolveWrapper("PacketPlayOutMultiBlockChange", packetPackageV17 + "PacketPlayOutMultiBlockChange"); + PACKET_PLAY_OUT_PLAYER_LIST_HEADER_FOOTER = nmsClassResolver.resolveWrapper("PacketPlayOutPlayerListHeaderFooter", packetPackageV17 + "PacketPlayOutPlayerListHeaderFooter"); + PACKET_PLAY_OUT_POSITION = nmsClassResolver.resolveWrapper("PacketPlayOutPosition", packetPackageV17 + "PacketPlayOutPosition"); + PACKET_PLAY_OUT_REMOVE_ENTITY_EFFECT = nmsClassResolver.resolveWrapper("PacketPlayOutRemoveEntityEffect", packetPackageV17 + "PacketPlayOutRemoveEntityEffect"); + PACKET_PLAY_OUT_RESPAWN = nmsClassResolver.resolveWrapper("PacketPlayOutRespawn", packetPackageV17 + "PacketPlayOutRespawn"); + PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJECTIVE = nmsClassResolver.resolveWrapper("PacketPlayOutScoreboardDisplayObjective", packetPackageV17 + "PacketPlayOutScoreboardDisplayObjective"); + PACKET_PLAY_OUT_SCOREBOARD_OBJECTIVE = nmsClassResolver.resolveWrapper("PacketPlayOutScoreboardObjective", packetPackageV17 + "PacketPlayOutScoreboardObjective"); + PACKET_PLAY_OUT_SCOREBOARD_SCORE = nmsClassResolver.resolveWrapper("PacketPlayOutScoreboardScore", packetPackageV17 + "PacketPlayOutScoreboardScore"); + PACKET_PLAY_OUT_SCOREBOARD_TEAM = nmsClassResolver.resolveWrapper("PacketPlayOutScoreboardTeam", packetPackageV17 + "PacketPlayOutScoreboardTeam"); + PACKET_PLAY_OUT_SPAWN_ENTITY = nmsClassResolver.resolveWrapper("PacketPlayOutSpawnEntity", packetPackageV17 + "PacketPlayOutSpawnEntity"); + PACKET_PLAY_OUT_SPAWN_ENTITY_EXPERIENCE_ORB = nmsClassResolver.resolveWrapper("PacketPlayOutSpawnEntityExperienceOrb", packetPackageV17 + "PacketPlayOutSpawnEntityExperienceOrb"); + PACKET_PLAY_OUT_SPAWN_ENTITY_LIVING = nmsClassResolver.resolveWrapper("PacketPlayOutSpawnEntityLiving", packetPackageV17 + "PacketPlayOutSpawnEntityLiving"); + PACKET_PLAY_OUT_STATISTIC = nmsClassResolver.resolveWrapper("PacketPlayOutStatistic", packetPackageV17 + "PacketPlayOutStatistic"); + PACKET_PLAY_OUT_TAB_COMPLETE = nmsClassResolver.resolveWrapper("PacketPlayOutTabComplete", packetPackageV17 + "PacketPlayOutTabComplete"); + PACKET_PLAY_OUT_UPDATE_HEALTH = nmsClassResolver.resolveWrapper("PacketPlayOutUpdateHealth", packetPackageV17 + "PacketPlayOutUpdateHealth"); + PACKET_PLAY_OUT_UPDATE_TIME = nmsClassResolver.resolveWrapper("PacketPlayOutUpdateTime", packetPackageV17 + "PacketPlayOutUpdateTime"); + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/BukkitReflection.java b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/BukkitReflection.java new file mode 100644 index 0000000..92a3308 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/BukkitReflection.java @@ -0,0 +1,150 @@ +package com.pepedevs.corelib.utils.reflection.bukkit; + +import com.pepedevs.corelib.utils.reflection.PacketConstant; +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.general.MethodReflection; +import com.pepedevs.corelib.utils.reflection.resolver.FieldResolver; +import com.pepedevs.corelib.utils.reflection.resolver.MethodResolver; +import com.pepedevs.corelib.utils.reflection.resolver.ResolverQuery; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.CraftClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.NMSClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import com.pepedevs.corelib.utils.version.Version; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.lang.reflect.InvocationTargetException; + +/** Class for reflecting servers based on Bukkit. */ +public class BukkitReflection { + + public static final ClassWrapper MINECRAFT_SERVER; + public static final ClassWrapper PLAYER_LIST; + public static final ClassWrapper DIMENSION_MANAGER; + public static final ClassWrapper CRAFT_WORLD; + public static final ClassWrapper NMS_WORLD; + public static final ClassWrapper RESOURCE_KEY; + public static final MethodWrapper SEND_PACKET; + public static final MethodWrapper MINECRAFT_SERVER_GET_SERVER; + public static final MethodWrapper MINECRAFT_SERVER_SET_MOTD; + public static final MethodWrapper MINECRAFT_SERVER_GET_PLAYER_LIST; + public static final MethodWrapper SEND_PACKET_NEARBY; + public static final FieldAccessor WORLD_BORDER_FIELD; + public static final FieldAccessor WORLD_WORLD_SERVER; + + static { + NMSClassResolver nmsClassResolver = new NMSClassResolver(); + MINECRAFT_SERVER = nmsClassResolver.resolveWrapper("MinecraftServer", "net.minecraft.server.MinecraftServer"); + PLAYER_LIST = nmsClassResolver.resolveWrapper("PlayerList", "net.minecraft.server.players.PlayerList"); + DIMENSION_MANAGER = nmsClassResolver.resolveWrapper("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager"); + CRAFT_WORLD = new CraftClassResolver().resolveWrapper("CraftWorld"); + NMS_WORLD = nmsClassResolver.resolveWrapper("World", "net.minecraft.world.level.World"); + RESOURCE_KEY = nmsClassResolver.resolveWrapper("ResourceKey", "net.minecraft.resources.ResourceKey"); + SEND_PACKET = new MethodResolver(PlayerReflection.PLAYER_CONNECTION_CLASS.getClazz()).resolveWrapper( + ResolverQuery.builder().with("sendPacket", PacketConstant.PACKET_CLASS.getClazz()).build()); + MINECRAFT_SERVER_GET_SERVER = new MethodResolver(MINECRAFT_SERVER.getClazz()).resolveWrapper("getServer"); + MINECRAFT_SERVER_SET_MOTD = new MethodResolver(MINECRAFT_SERVER.getClazz()).resolveWrapper( + ResolverQuery.builder().with("setMotd", String.class).build()); + MINECRAFT_SERVER_GET_PLAYER_LIST = new MethodResolver(MINECRAFT_SERVER.getClazz()).resolveWrapper("getPlayerList"); + SEND_PACKET_NEARBY = new MethodResolver(PLAYER_LIST.getClazz()).resolveWrapper( + ResolverQuery.builder() + .with("sendPacketNearby", EntityReflection.ENTITY_HUMAN.getClazz(), + double.class, double.class, double.class, double.class, int.class, PacketConstant.PACKET_CLASS.getClazz()) + .with("sendPacketNearby", EntityReflection.ENTITY_HUMAN.getClazz(), + double.class, double.class, double.class, double.class, DIMENSION_MANAGER.getClazz(), PacketConstant.PACKET_CLASS.getClazz()) + .with("sendPacketNearby", EntityReflection.ENTITY_HUMAN.getClazz(), + double.class, double.class, double.class, double.class, RESOURCE_KEY.getClazz(), PacketConstant.PACKET_CLASS.getClazz()) + .build()); + WORLD_BORDER_FIELD = new FieldResolver(CRAFT_WORLD.getClazz()).resolveAccessor("worldBorder"); + WORLD_WORLD_SERVER = new FieldResolver(BukkitReflection.CRAFT_WORLD.getClazz()).resolveAccessor("world"); + } + + /** + * Gets the handle (the represented nms object by a craftbukkit object) of the provided + * craftbukkit object. + * + *

+ * + * @param object Object to get + * @return Handle of the provided craftbukkit object + */ + public static Object getHandle(Object object){ + try { + return object.getClass().getMethod("getHandle").invoke(object); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + throw new UnsupportedOperationException( + "cannot get the handle of the provided object!"); + } + } + + /** + * Sends the provided packet to the desired player. + * + *

+ * + * @param player Player that will receive the packet + * @param packet Packet instance to send + */ + public static void sendPacket(Player player, Object packet) { + Object connection = PlayerReflection.getPlayerConnection(player); + SEND_PACKET.invokeSilent(connection, packet); + } + + public static void sendPacketNearby(Player player, double x, double y, double z, double range, World world, Object packet) { + Object handle = null; + if (player != null) { + handle = PlayerReflection.getHandle(player); + } + Object minecraft_server = MINECRAFT_SERVER_GET_SERVER.invoke(null); + Object player_list = MINECRAFT_SERVER_GET_PLAYER_LIST.invoke(minecraft_server); + + Object world_server = WORLD_WORLD_SERVER.get(world); + + Object dimension; + try { + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_16_R1)) { + dimension = + MethodReflection.get(NMS_WORLD.getClazz(), "getDimensionKey") + .invoke(world_server); + } else if (Version.SERVER_VERSION.isNewerEquals(Version.v1_13_R2)) { + Object world_provider = + MethodReflection.get(NMS_WORLD.getClazz(), "getWorldProvider") + .invoke(world_server); + dimension = + MethodReflection.get(world_provider.getClass(), "getDimensionManager") + .invoke(world_provider); + } else { + dimension = world_server.getClass().getField("dimension").getInt(world_server); + } + + SEND_PACKET_NEARBY.invoke(player_list, handle, x, y, z, range, dimension, packet); + } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + /** + * Sets the MOTD of the current running server. + * + *

+ * + * @param motd New MOTD for this server + */ + public static void setMotd(String motd) { + Object server = MINECRAFT_SERVER_GET_SERVER.invokeSilent(null); + MINECRAFT_SERVER_SET_MOTD.invokeSilent(server, motd); + } + + /** + * Clears the world border of the desired {@link World}. + * + *

+ * + * @param world World with the desired border to clear + */ + public static void clearBorder(World world) { + world.getWorldBorder().reset(); + WORLD_BORDER_FIELD.set(world, null); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/EntityReflection.java b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/EntityReflection.java new file mode 100644 index 0000000..c33a9f0 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/EntityReflection.java @@ -0,0 +1,502 @@ +package com.pepedevs.corelib.utils.reflection.bukkit; + +import com.pepedevs.corelib.utils.math.collision.BoundingBox; +import com.pepedevs.corelib.utils.reflection.PacketConstant; +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.general.MethodReflection; +import com.pepedevs.corelib.utils.reflection.resolver.ConstructorResolver; +import com.pepedevs.corelib.utils.reflection.resolver.FieldResolver; +import com.pepedevs.corelib.utils.reflection.resolver.MethodResolver; +import com.pepedevs.corelib.utils.reflection.resolver.ResolverQuery; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.NMSClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ConstructorWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import com.pepedevs.corelib.utils.version.Version; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.lang.reflect.InvocationTargetException; + +/** Class for reflecting Bukkit entities */ +public class EntityReflection { + + public static final ClassWrapper NMS_ENTITY_CLASS; + public static final ClassWrapper ENTITY_HUMAN; + public static final ClassWrapper ENTITY_LIVING; + public static final ClassWrapper ENTITY_ARMOR_STAND; + public static final ClassWrapper NBT_TAG_COMPOUND; + public static final ClassWrapper DAMAGE_SOURCE; + public static final ClassWrapper SOUND_CATEGORY; + public static final ConstructorWrapper PACKET_PLAY_OUT_ENTITY_DESTROY_CONSTRUCTOR; + public static final ConstructorWrapper PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT_CONSTRUCTOR; + public static final ConstructorWrapper NBT_TAG_COMPOUND_CONSTRUCTOR; + public static final MethodWrapper NMS_ENTITY_GET_BOUNDING_BOX; + public static final MethodWrapper NMS_ENTITY_GET_HEAD_HEIGHT; + public static final MethodWrapper NMS_ENTITY_LOC_X; + public static final MethodWrapper NMS_ENTITY_LOC_Y; + public static final MethodWrapper NMS_ENTITY_LOC_Z; + public static final MethodWrapper NMS_ENTITY_SET_LOCATION; + public static final MethodWrapper NMS_ENTITY_SET_YAW_PITCH; + public static final MethodWrapper NMS_ENTITY_IS_INVISIBLE; + public static final MethodWrapper NMS_ENTITY_SET_INVISIBLE; + public static final MethodWrapper NMS_ENTITY_IS_SILENT; + public static final MethodWrapper NMS_ENTITY_SET_SILENT; + public static final MethodWrapper NMS_ENTITY_IS_INVULNERABLE; + public static final MethodWrapper NBT_TAG_COMPOUND_SET_INT; + public static final MethodWrapper ENTITY_LIVING_C; + public static final MethodWrapper ENTITY_LIVING_F; + public static final MethodWrapper SOUND_CATEGORY_VALUE_OF; + public static final FieldAccessor NMS_ENTITY_YAW; + public static final FieldAccessor NMS_ENTITY_PITCH; + public static final FieldAccessor NMS_ENTITY_INVULNERABLE; + public static final FieldAccessor ENTITY_ARMOR_STAND_INVULNERABLE; + public static final FieldAccessor DAMAGE_SOURCE_GENERIC; + + static { + NMSClassResolver nmsClassResolver = new NMSClassResolver(); + NMS_ENTITY_CLASS = nmsClassResolver.resolveWrapper("Entity", "net.minecraft.world.entity.Entity"); + ENTITY_HUMAN = nmsClassResolver.resolveWrapper("EntityHuman", "net.minecraft.world.entity.player.EntityHuman"); + ENTITY_LIVING = nmsClassResolver.resolveWrapper("EntityLiving", "net.minecraft.world.entity.EntityLiving"); + ENTITY_ARMOR_STAND = nmsClassResolver.resolveWrapper("EntityArmorStand", "net.minecraft.world.entity.decoration.EntityArmorStand"); + NBT_TAG_COMPOUND = nmsClassResolver.resolveWrapper("NBTTagCompound", "net.minecraft.nbt.NBTTagCompound"); + DAMAGE_SOURCE = nmsClassResolver.resolveWrapper("DamageSource", "net.minecraft.world.damagesource.DamageSource"); + SOUND_CATEGORY = nmsClassResolver.resolveWrapper("SoundCategory", "net.minecraft.sounds.SoundCategory"); + PACKET_PLAY_OUT_ENTITY_DESTROY_CONSTRUCTOR = new ConstructorResolver(PacketConstant.PACKET_PLAY_OUT_ENTITY_DESTROY.getClazz()).resolveWrapper(new Class[]{int[].class}); + PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT_CONSTRUCTOR = new ConstructorResolver(PacketConstant.PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT.getClazz()) + .resolveWrapper(new Class[]{String.class, SOUND_CATEGORY.getClazz(), double.class, double.class, double.class, float.class, float.class}); + NBT_TAG_COMPOUND_CONSTRUCTOR = new ConstructorResolver(NBT_TAG_COMPOUND.getClazz()).resolveWrapper(); + NMS_ENTITY_GET_BOUNDING_BOX = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("getBoundingBox"); + NMS_ENTITY_GET_HEAD_HEIGHT = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("getHeadHeight"); + NMS_ENTITY_LOC_X = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("locX"); + NMS_ENTITY_LOC_Y = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("locY"); + NMS_ENTITY_LOC_Z = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("locZ"); + NMS_ENTITY_SET_LOCATION = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper( + ResolverQuery.builder().with("setLocation", double.class, double.class, double.class, float.class, float.class).build()); + NMS_ENTITY_SET_YAW_PITCH = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper( + ResolverQuery.builder().with("setYawPitch", float.class, float.class).build()); + NMS_ENTITY_IS_INVISIBLE = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("isInvisible"); + NMS_ENTITY_SET_INVISIBLE = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper( + ResolverQuery.builder().with("setInvisible", boolean.class).build()); + NMS_ENTITY_IS_SILENT = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper("isSilent", "R", "ad"); + NMS_ENTITY_SET_SILENT = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper( + ResolverQuery.builder().with("b", boolean.class).with("setSilent", boolean.class).build()); + NMS_ENTITY_IS_INVULNERABLE = new MethodResolver(NMS_ENTITY_CLASS.getClazz()).resolveWrapper( + ResolverQuery.builder().with("isInvulnerable", DAMAGE_SOURCE.getClazz()).build()); + ENTITY_LIVING_C = new MethodResolver(ENTITY_LIVING.getClazz()).resolveWrapper( + ResolverQuery.builder().with("c", NBT_TAG_COMPOUND.getClazz()).build()); + ENTITY_LIVING_F = new MethodResolver(ENTITY_LIVING.getClazz()).resolveWrapper( + ResolverQuery.builder().with("f", NBT_TAG_COMPOUND.getClazz()).build()); + NBT_TAG_COMPOUND_SET_INT = new MethodResolver(NBT_TAG_COMPOUND.getClazz()).resolveWrapper( + ResolverQuery.builder().with("setInt", String.class, int.class).build()); + SOUND_CATEGORY_VALUE_OF = new MethodResolver(SOUND_CATEGORY.getClazz()).resolveWrapper( + ResolverQuery.builder().with("valueOf", String.class).build()); + NMS_ENTITY_YAW = new FieldResolver(NMS_ENTITY_CLASS.getClazz()).resolveAccessor( + ResolverQuery.builder().with("yaw", float.class).with("ay", float.class).build()); + NMS_ENTITY_PITCH = new FieldResolver(NMS_ENTITY_CLASS.getClazz()).resolveAccessor( + ResolverQuery.builder().with("pitch", float.class).with("az", float.class).build()); + NMS_ENTITY_INVULNERABLE = new FieldResolver(NMS_ENTITY_CLASS.getClazz()).resolveAccessor( + ResolverQuery.builder().with("invulnerable", boolean.class).build()); + ENTITY_ARMOR_STAND_INVULNERABLE = new FieldResolver(ENTITY_ARMOR_STAND.getClazz()).resolveAccessor( + ResolverQuery.builder().with("by", boolean.class).with("bz", boolean.class) + .with("bA", boolean.class).with("bG", boolean.class).with("bD", boolean.class) + .with("armorStandInvisible", boolean.class).with("ce", boolean.class) + .with("h", boolean.class).build()); + DAMAGE_SOURCE_GENERIC = new FieldResolver(DAMAGE_SOURCE.getClazz()).resolveAccessor("GENERIC", "n"); + } + + /** + * Gets the {@link BoundingBox} for the provided {@link Entity}. + * + *

+ * + * @param entity Entity to get its BoundingBox + * @param height Entity's height + * @return BoundingBox for the entity, or null if couldn't get + */ + public static BoundingBox getBoundingBox(Entity entity, float height) { + final Object handle = BukkitReflection.getHandle(entity); + final Object nms_bb = NMS_ENTITY_GET_BOUNDING_BOX.invoke(handle); + + int i = 0; + + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_17_R1)) i = 1; + + FieldResolver resolver = new FieldResolver(nms_bb.getClass()); + final double min_x = resolver.resolveIndexAccessor(i++).get(nms_bb); + final double min_y = (double) resolver.resolveIndexAccessor(i++).get(nms_bb) - height; + final double min_z = resolver.resolveIndexAccessor(i++).get(nms_bb); + + final double max_x = resolver.resolveIndexAccessor(i++).get(nms_bb); + final double max_y = (double) resolver.resolveIndexAccessor(i++).get(nms_bb) - height; + final double max_z = resolver.resolveIndexAccessor(i++).get(nms_bb); + + return new BoundingBox(new Vector(min_x, min_y, min_z), new Vector(max_x, max_y, max_z)); + } + + /** + * Gets the {@link BoundingBox} for the provided {@link Entity}. The accuracy is not guaranteed + * when calculating the entity's height. + * + *

+ * + * @param entity Entity to get its BoundingBox + * @return BoundingBox for the entity, or null if couldn't get + */ + public static BoundingBox getBoundingBox(Entity entity) { + final Object handle = BukkitReflection.getHandle(entity); + final float head_height = NMS_ENTITY_GET_HEAD_HEIGHT.invoke(handle); + + return EntityReflection.getBoundingBox(entity, head_height); + } + + /** + * This method makes the provided {@code entity} invisible to the desired target players. + * + *

Note that after calling this, the entity cannot be made visible. Instead the entity will + * be invisible if the player left the server and joins it again. + * + *

+ * + * @param entity Entity to make invisible to the {@code targets} players + * @param targets Players that will not can see the entity + */ + public static void setInvisibleTo(Entity entity, Player... targets) { + Object packet = PACKET_PLAY_OUT_ENTITY_DESTROY_CONSTRUCTOR.newInstance(entity.getEntityId()); + for (Player target : targets) { + if (target.isOnline()) { + BukkitReflection.sendPacket(target, packet); + } + } + } + + /** + * Sets whether the provided {@code entity} will have AI. + * + *

+ * + * @param entity Target entity + * @param ai Whether the entity will have AI or not + */ + public static void setAI(LivingEntity entity, boolean ai) { + if (Version.SERVER_VERSION.isOlder(Version.v1_9_R2)) { + + Object handle = BukkitReflection.getHandle(entity); + Object nbt = NBT_TAG_COMPOUND_CONSTRUCTOR.newInstance(); + + ENTITY_LIVING_C.invoke(handle, nbt); + NBT_TAG_COMPOUND_SET_INT.invoke(nbt, "NoAI", (ai ? 0 : 1)); + ENTITY_LIVING_F.invoke(handle, nbt); + } else { + entity.setAI(ai); + } + } + + /** + * Sets whether the provided {@code entity} is will have collisions. + * + *

+ * + * @param entity Target entity + * @param collidable Whether to enable collisions for the entity + */ + public static void setCollidable(LivingEntity entity, boolean collidable) { + if (Version.SERVER_VERSION.isNewerEquals(Version.v1_9_R2)) { + entity.setCollidable(collidable); + } + } + + /** + * Gets the entity's current position. + * + *

+ * + * @param entity Entity to get + * @return Copy of Location containing the position of the desired entity, or null if couldn't + * get + */ + public static Location getLocation(Entity entity) { + Object handle = BukkitReflection.getHandle(entity); + + final double x = NMS_ENTITY_LOC_X.invoke(handle); + final double y = NMS_ENTITY_LOC_Y.invoke(handle); + final double z = NMS_ENTITY_LOC_Z.invoke(handle); + + final float yaw = NMS_ENTITY_YAW.get(handle); + final float pitch = NMS_ENTITY_PITCH.get(handle); + + return new Location(entity.getWorld(), x, y, z, yaw, pitch); + } + + /** + * Sets the location of the provided nms entity. Note that this method doesn't teleport the + * entity. + * + *

+ * + * @param entity Nms entity to set + * @param x Location's x + * @param y Location's y + * @param z Location's z + * @param yaw Rotation around axis y + * @param pitch Rotation around axis x + */ + public static void setLocation( + Object entity, double x, double y, double z, float yaw, float pitch) { + try { + MethodReflection.invoke( + MethodReflection.get( + entity.getClass(), + "setLocation", + double.class, + double.class, + double.class, + float.class, + float.class), + entity, + x, + y, + z, + yaw, + pitch); + } catch (IllegalAccessException + | IllegalArgumentException + | InvocationTargetException + | NoSuchMethodException + | SecurityException e) { + e.printStackTrace(); + } + } + + /** + * Sets the location of the provided entity. Note that this method doesn't teleport the entity. + * + *

+ * + * @param entity Entity to set + * @param x Location's x + * @param y Location's y + * @param z Location's z + * @param yaw Rotation around axis y + * @param pitch Rotation around axis x + */ + public static void setLocation( + Entity entity, double x, double y, double z, float yaw, float pitch) { + Object handle = BukkitReflection.getHandle(entity); + NMS_ENTITY_SET_LOCATION.invoke(handle, x, y, z, yaw, pitch); + } + + /** + * Sets the location of the provided entity. Note that this method doesn't teleport the entity. + * + *

+ * + * @param entity Entity to set + * @param location Location for the entity + */ + public static void setLocation(Entity entity, Location location) { + EntityReflection.setLocation( + entity, + location.getX(), + location.getY(), + location.getZ(), + location.getYaw(), + location.getPitch()); + } + + /** + * Sets the location of the provided nms entity. Note that this method doesn't teleport the + * entity. + * + *

+ * + * @param entity Nms entity to set + * @param location Location for the entity + */ + public static void setLocation(Object entity, Location location) { + EntityReflection.setLocation( + entity, + location.getX(), + location.getY(), + location.getZ(), + location.getYaw(), + location.getPitch()); + } + + /** + * Sets the yaw and pitch of the provided entity. Note that this method doesn't teleport the + * entity. + * + *

Also {@link #setLocation(Entity, double, double, double, float, float)} is recommended to + * be used instead. + * + *

+ * + * @param entity Entity the entity to set + * @param yaw New yaw + * @param pitch New pitch + */ + public static void setYawPitch(Entity entity, float yaw, float pitch) { + Object handle = BukkitReflection.getHandle(entity); + NMS_ENTITY_SET_YAW_PITCH.invoke(handle, yaw, pitch); + } + + /** + * Gets whether the provided entity is visible or not. + * + *

+ * + * @param entity Entity to check + * @return true if visible + */ + public static boolean isVisible(Entity entity) { + Object handle = BukkitReflection.getHandle(entity); + return !(boolean) NMS_ENTITY_IS_INVISIBLE.invoke(handle); + } + + /** + * Sets whether the provided entity is visible or not. + * + *

+ * + * @param entity Entity to set + * @param visible {@code true} = visible, {@code false} = invisible + */ + public static void setVisible(Entity entity, boolean visible) { + Object handle = BukkitReflection.getHandle(entity); + NMS_ENTITY_SET_INVISIBLE.invoke(handle, !visible); + } + + /** + * Gets whether the provided entity is silent or not. + * + *

+ * + * @param entity Entity to check + * @return true if silent + */ + public static boolean isSilent(Entity entity) { + Object handle = BukkitReflection.getHandle(entity); + return NMS_ENTITY_IS_SILENT.invoke(handle); + } + + /** + * Sets whether the provided entity is silent or not. + * + *

+ * + * @param entity Entity to set + * @param silent true = silent, false = not silent + */ + public static void setSilent(Entity entity, boolean silent) { + Object handle = BukkitReflection.getHandle(entity); + NMS_ENTITY_SET_SILENT.invoke(handle, silent); + } + + /** + * Gets whether or not the provided entity is invulnerable. + * + *

+ * + * @param entity Entity to check + * @return true if invulnerable + */ + public static boolean isInvulnerable(Entity entity) { + if (Version.SERVER_VERSION.isOlderEquals(Version.v1_9_R2)) { + Object handle = BukkitReflection.getHandle(entity); + Object generic_damage = DAMAGE_SOURCE_GENERIC.get(null); + + return NMS_ENTITY_IS_INVULNERABLE.invoke(handle, generic_damage); + } else { + return entity.isInvulnerable(); + } + } + + /** + * Sets whether the provided entity is invulnerable or not. + * + *

When an entity is invulnerable it can only be damaged by players in creative mode. + * + *

+ * + * @param entity Entity to set + * @param invulnerable true = invulnerable, false = vulnerable + */ + public static void setInvulnerable(Entity entity, boolean invulnerable) { + if (Version.SERVER_VERSION.isOlderEquals(Version.v1_9_R2)) { + Object handle = BukkitReflection.getHandle(entity); + NMS_ENTITY_INVULNERABLE.set(handle, invulnerable); + } else { + entity.setInvulnerable(invulnerable); + } + } + + /** + * Sets the provided {@link ArmorStand} as invulnerable. This method is to be used instead of + * {@link #setInvulnerable(Entity, boolean)}. + * + *

+ * + * @param stand Armor stand to set + */ + public static void setInvulnerable(ArmorStand stand, boolean invulnerable) { + ENTITY_ARMOR_STAND_INVULNERABLE.set(BukkitReflection.getHandle(stand), !invulnerable); + } + + /** + * Plays a sound for the provided player. + * + *

This method will fail silently if Location or Sound are null. No sound will be heard by + * the player if their client does not have the respective sound for the value passed. + * + *

+ * + * @param player Location to play the sound + * @param sound Internal sound name to play + * @param volume Volume of the sound + * @param pitch Pitch of the sound + */ + public static void playNamedSound(Player player, String sound, float volume, float pitch) { + Object master = SOUND_CATEGORY_VALUE_OF.invoke(null, "MASTER"); + + Location location = player.getEyeLocation(); + double x = location.getX(); + double y = location.getY(); + double z = location.getZ(); + + Object packet = PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT_CONSTRUCTOR.newInstance(sound, master, x, y, z, volume, pitch); + BukkitReflection.sendPacket(player, packet); + } + + /** + * Plays a Sound at the provided Location in the World. + * + *

This method will fail silently if Location or Sound are null. No sound will be heard by + * the players if their clients do not have the respective sound for the value passed. + * + *

+ * + * @param location Location to play the sound + * @param sound Internal sound name to play + * @param volume Volume of the sound + * @param pitch Pitch of the sound + */ + public static void playNameSoundAt(Location location, String sound, float volume, float pitch) { + World w = location.getWorld(); + double x = location.getX(); + double y = location.getY(); + double z = location.getZ(); + + boolean newest = Version.SERVER_VERSION.isNewerEquals(Version.v1_17_R1); + + Object master = SOUND_CATEGORY_VALUE_OF.invoke(null, newest ? "a" : "MASTER"); + Object packet = PACKET_PLAY_OUT_CUSTOM_SOUND_EFFECT_CONSTRUCTOR.newInstance(sound, master, x, y, z, volume, pitch); + + BukkitReflection.sendPacketNearby(null, x, y, z, (volume > 1.0F ? 16.0F * volume : 16.0D), w, packet); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/PlayerReflection.java b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/PlayerReflection.java new file mode 100644 index 0000000..0411083 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/reflection/bukkit/PlayerReflection.java @@ -0,0 +1,94 @@ +package com.pepedevs.corelib.utils.reflection.bukkit; + +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.resolver.FieldResolver; +import com.pepedevs.corelib.utils.reflection.resolver.MethodResolver; +import com.pepedevs.corelib.utils.reflection.resolver.ResolverQuery; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.CraftClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.minecraft.NMSClassResolver; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.ClassWrapper; +import com.pepedevs.corelib.utils.reflection.resolver.wrapper.MethodWrapper; +import io.netty.channel.Channel; +import org.bukkit.entity.Player; + +/** Class for reflecting {@link Player} */ +public class PlayerReflection { + + public static final ClassWrapper CRAFT_PLAYER_CLASS; + public static final ClassWrapper ENTITY_PLAYER_CLASS; + public static final ClassWrapper PLAYER_CONNECTION_CLASS; + public static final ClassWrapper NETWORK_MANAGER_CLASS; + public static final FieldAccessor PLAYER_CONNECTION_FIELD; + public static final FieldAccessor NETWORK_MANAGER_FIELD; + public static final FieldAccessor CHANNEL_FIELD; + public static final MethodWrapper CRAFT_PLAYER_GET_HANDLE; + + static { + NMSClassResolver nmsClassResolver = new NMSClassResolver(); + CraftClassResolver craftClassResolver = new CraftClassResolver(); + CRAFT_PLAYER_CLASS = craftClassResolver.resolveWrapper("CraftPlayer"); + ENTITY_PLAYER_CLASS = nmsClassResolver.resolveWrapper("net.minecraft.server.level.EntityPlayer", "EntityPlayer"); + PLAYER_CONNECTION_CLASS = nmsClassResolver.resolveWrapper("net.minecraft.server.network.PlayerConnection", "PlayerConnection"); + NETWORK_MANAGER_CLASS = nmsClassResolver.resolveWrapper("net.minecraft.network.NetworkManager", "NetworkManager"); + PLAYER_CONNECTION_FIELD = new FieldResolver(ENTITY_PLAYER_CLASS.getClazz()).resolveAccessor( + ResolverQuery.builder().with("b", PLAYER_CONNECTION_CLASS.getClazz()) + .with("playerConnection", PLAYER_CONNECTION_CLASS.getClazz()).build()); + NETWORK_MANAGER_FIELD = new FieldResolver(ENTITY_PLAYER_CLASS.getClazz()).resolveAccessor( + ResolverQuery.builder().with("a", NETWORK_MANAGER_CLASS.getClazz()) + .with("networkManager", NETWORK_MANAGER_CLASS.getClazz()).build()); + CHANNEL_FIELD = new FieldResolver(NETWORK_MANAGER_CLASS.getClazz()).resolveAccessor( + ResolverQuery.builder().with("i", Channel.class) + .with("k", Channel.class).with("channel", Channel.class).build()); + CRAFT_PLAYER_GET_HANDLE = new MethodResolver(CRAFT_PLAYER_CLASS.getClazz()).resolveWrapper("getHandle"); + } + + /** + * Gets the handle (the represented nms player by the craftbukkit player) of the provided + * {@code player}. + * + *

+ * + * @param player Player to get + * @return Handle of the provided craftbukkit player + */ + public static Object getHandle(Player player) { + return CRAFT_PLAYER_GET_HANDLE.invokeSilent(CRAFT_PLAYER_CLASS.getClazz().cast(player)); + } + + /** + * Gets player's connection. + * + *

+ * + * @param player Player to get + * @return Player's connection + */ + public static Object getPlayerConnection(Player player) { + return PLAYER_CONNECTION_FIELD.get(PlayerReflection.getHandle(player)); + } + + /** + * Gets player's network manager. + * + *

+ * + * @param player Player to get + * @return Player's network manager + */ + public static Object getNetworkManager(Player player) { + return NETWORK_MANAGER_FIELD.get(PlayerReflection.getPlayerConnection(player)); + } + + /** + * Gets player's channel. + * + *

+ * + * @param player Player to get + * @return Player's channel + */ + public static Channel getChannel(Player player) { + return CHANNEL_FIELD.get(PlayerReflection.getNetworkManager(player)); + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/scheduler/SchedulerUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/scheduler/SchedulerUtils.java new file mode 100644 index 0000000..b01668f --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/scheduler/SchedulerUtils.java @@ -0,0 +1,292 @@ +package com.pepedevs.corelib.utils.scheduler; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.scheduler.BukkitWorker; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Useful class for dealing with schedulers. This is just a direct-access to the scheduler of + * Bukkit. + * + *

+ * + * @see Bukkit#getScheduler() + */ +public class SchedulerUtils { + + /** + * Schedules a once off task to occur after a delay. + * + *

This task will be executed by the main server thread. + * + *

+ * + * @param task Task to be executed + * @param delay Delay in server ticks before executing task + * @param plugin Plugin that owns the task + * @return Task id number (-1 if scheduling failed) + */ + public static int scheduleSyncDelayedTask(Runnable task, long delay, Plugin plugin) { + return getScheduler().scheduleSyncDelayedTask(plugin, task, delay); + } + + /** + * Schedules a once off task to occur as soon as possible. + * + *

This task will be executed by the main server thread. + * + *

+ * + * @param task Task to be executed + * @param plugin Plugin that owns the task + * @return Task id number (-1 if scheduling failed) + */ + public static int scheduleSyncDelayedTask(Runnable task, Plugin plugin) { + return getScheduler().scheduleSyncDelayedTask(plugin, task); + } + + /** + * Schedules a repeating task. + * + *

This task will be executed by the main server thread. + * + *

+ * + * @param task Task to be executed + * @param delay Delay in server ticks before executing first repeat + * @param period Period in server ticks of the task + * @param plugin Plugin that owns the task + * @return Task id number (-1 if scheduling failed) + */ + public static int scheduleSyncRepeatingTask( + Runnable task, long delay, long period, Plugin plugin) { + return getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); + } + + /** + * Calls a method on the main thread and returns a Future object. This task will be executed by + * the main server thread. + * + *

    + *
  • Note: The Future.get() methods must NOT be called from the main thread. + *
  • Note2: There is at least an average of 10ms latency until the isDone() method returns + * true. + *
+ * + * @param The callable's return type + * @param task Task to be executed + * @param plugin Plugin that owns the task + * @return Future Future object related to the task + */ + public static Future callSyncMethod(Callable task, Plugin plugin) { + return getScheduler().callSyncMethod(plugin, task); + } + + /** + * Removes task from scheduler. + * + *

+ * + * @param id Id number of task to be removed + */ + public static void cancelTask(int id) { + getScheduler().cancelTask(id); + } + + /** + * Removes all tasks associated with a particular plugin from the scheduler. + * + *

+ * + * @param plugin Owner of tasks to be removed + */ + public static void cancelTasks(Plugin plugin) { + getScheduler().cancelTasks(plugin); + } + + /** + * Check if the task is currently running. + * + *

A repeating task might not be running currently, but will be running in the future. A task + * that has finished, and does not repeat, will not be running ever again. + * + *

Explicitly, a task is running if there exists a thread for it, and that thread is alive. + * + *

+ * + * @param id The task to check. + * @return If the task is currently running. + */ + public static boolean isCurrentlyRunning(int id) { + return getScheduler().isCurrentlyRunning(id); + } + + /** + * Check if the task queued to be run later. + * + *

If a repeating task is currently running, it might not be queued now but could be in the + * future. A task that is not queued, and not running, will not be queued again. + * + *

+ * + * @param id The task to check. + * @return If the task is queued to be run. + */ + public static boolean isQueued(int id) { + return getScheduler().isQueued(id); + } + + /** + * Returns a list of all active workers. + * + *

This list contains async tasks that are being executed by separate threads. + * + *

+ * + * @return Active workers + */ + public static List getActiveWorkers() { + return getScheduler().getActiveWorkers(); + } + + /** + * Returns a list of all pending tasks. The ordering of the tasks is not related to their order + * of execution. + * + *

+ * + * @return Active workers + */ + public static List getPendingTasks() { + return getScheduler().getPendingTasks(); + } + + /** + * Returns a task that will run on the next server tick. + * + *

+ * + * @param task Task to be run + * @param plugin Reference to the plugin scheduling task + * @return BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public static BukkitTask runTask(Runnable task, Plugin plugin) throws IllegalArgumentException { + return getScheduler().runTask(plugin, task); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care should be taken to + * assure the thread-safety of asynchronous tasks. + * + *

Returns a task that will run asynchronously. + * + *

+ * + * @param task Task to be run + * @param plugin Reference to the plugin scheduling task + * @return BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public static BukkitTask runTaskAsynchronously(Runnable task, Plugin plugin) + throws IllegalArgumentException { + return getScheduler().runTaskAsynchronously(plugin, task); + } + + /** + * Returns a task that will run after the specified number of server ticks. + * + *

+ * + * @param task Task to be run + * @param delay Ticks to wait before running the task + * @param plugin Reference to the plugin scheduling task + * @return BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public static BukkitTask runTaskLater(Runnable task, long delay, Plugin plugin) + throws IllegalArgumentException { + return getScheduler().runTaskLater(plugin, task, delay); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care should be taken to + * assure the thread-safety of asynchronous tasks. + * + *

Returns a task that will run asynchronously after the specified number of server ticks. + * + *

+ * + * @param task Task to be run + * @param delay Ticks to wait before running the task + * @param plugin Reference to the plugin scheduling task + * @return BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public static BukkitTask runTaskLaterAsynchronously(Runnable task, long delay, Plugin plugin) + throws IllegalArgumentException { + return getScheduler().runTaskLaterAsynchronously(plugin, task, delay); + } + + /** + * Returns a task that will repeatedly run until cancelled, starting after the specified number + * of server ticks. + * + *

+ * + * @param task Task to be run + * @param delay Ticks to wait before running the task + * @param period Ticks to wait between runs + * @param plugin Reference to the plugin scheduling task + * @return BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public static BukkitTask runTaskTimer(Runnable task, long delay, long period, Plugin plugin) + throws IllegalArgumentException { + return getScheduler().runTaskTimer(plugin, task, delay, period); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care should be taken to + * assure the thread-safety of asynchronous tasks. + * + *

Returns a task that will repeatedly run asynchronously until cancelled, starting after the + * specified number of server ticks. + * + *

+ * + * @param task Task to be run + * @param delay Ticks to wait before running the task for the first time + * @param period Ticks to wait between runs + * @param plugin Reference to the plugin scheduling task + * @return BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public static BukkitTask runTaskTimerAsynchronously( + Runnable task, long delay, long period, Plugin plugin) throws IllegalArgumentException { + return getScheduler().runTaskTimerAsynchronously(plugin, task, delay, period); + } + + /** + * Gets the scheduler for managing scheduled events. + * + *

+ * + * @return Scheduling service for this server + */ + public static BukkitScheduler getScheduler() { + return Bukkit.getScheduler(); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/server/ServerLifePhase.java b/utils/src/main/java/com/pepedevs/corelib/utils/server/ServerLifePhase.java new file mode 100644 index 0000000..01dbb58 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/server/ServerLifePhase.java @@ -0,0 +1,46 @@ +package com.pepedevs.corelib.utils.server; + +import org.bukkit.Bukkit; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public enum ServerLifePhase { + + STARTUP, + RUNNING, + SHUTDOWN, + UNKNOWN, + ; + + /** + * Returns the server's current life phase - when you call this in onEnable or onDisable, and it + * returns RUNNING, it means the server is reloading. + * + * @return Server's life phase + */ + public static ServerLifePhase getLifePhase() { + try { + final Field currentTickField = + Bukkit.getScheduler().getClass().getDeclaredField("currentTick"); + currentTickField.setAccessible(true); + final int currentTick = currentTickField.getInt(Bukkit.getScheduler()); + if (currentTick == -1) { + return ServerLifePhase.STARTUP; + } + final Method getServerMethod = Bukkit.getServer().getClass().getMethod("getServer"); + final Object nmsServer = getServerMethod.invoke(Bukkit.getServer()); + final Method isRunningMethod = nmsServer.getClass().getMethod("isRunning"); + return ((boolean) isRunningMethod.invoke(nmsServer)) + ? ServerLifePhase.RUNNING + : ServerLifePhase.SHUTDOWN; + } catch (final NoSuchFieldException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException exception) { + return ServerLifePhase.UNKNOWN; + } + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/server/TpsUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/server/TpsUtils.java new file mode 100644 index 0000000..9934d33 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/server/TpsUtils.java @@ -0,0 +1,27 @@ +package com.pepedevs.corelib.utils.server; + +import com.pepedevs.corelib.utils.reflection.accessor.FieldAccessor; +import com.pepedevs.corelib.utils.reflection.bukkit.BukkitReflection; +import com.pepedevs.corelib.utils.reflection.resolver.FieldResolver; + +/** Class for getting the current tips per second of the running server. */ +public class TpsUtils { + + public static final FieldAccessor RECENT_TPS = new FieldResolver(BukkitReflection.MINECRAFT_SERVER.getClazz()) + .resolveAccessor("recentTps"); + + /** + * Gets current server ticks per second. + * + *

+ * + * @return server current ticks per second. + */ + public static double getTicksPerSecond() { + final Object server = BukkitReflection.MINECRAFT_SERVER_GET_SERVER.invoke(null); + final double[] tps = (double[]) RECENT_TPS.get(server); + + return tps[0]; + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRule.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRule.java new file mode 100644 index 0000000..e0df2ad --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRule.java @@ -0,0 +1,94 @@ +package com.pepedevs.corelib.utils.world; + +import com.pepedevs.corelib.utils.Validable; +import org.apache.commons.lang.Validate; +import org.bukkit.World; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class GameRule implements Validable { + + protected final GameRuleType type; + protected final Object value; + protected final Set parents; + + /** + * Construct the {@link GameRule}. + * + *

+ * + * @param type Game rule type + * @param value Game rule value + * @param parents Required {@link GameRule GameRules} to work + */ + public GameRule(GameRuleType type, Object value, GameRule... parents) { + Validate.notNull(type, "type cannot be null!"); + Validate.notNull(value, "value cannot be null!"); + Validate.isTrue( + type.isSameDataType(value), "the specified type and value are incompatible!"); + + this.type = type; + this.value = value; + this.parents = new HashSet<>(); + this.parents.addAll( + Arrays.stream(parents) + .filter(GameRule::isValid) + .collect(Collectors.toSet())); + } + + /** + * Gets the game rule type. + * + *

+ * + * @return Game rule type + */ + public GameRuleType getType() { + return type; + } + + /** + * Gets the game rule value. + * + *

+ * + * @return Game rule value + */ + public Object getValue() { + return value; + } + + /** + * Gets the {@link GameRule GameRules} this requires to work. + * + *

+ * + * @return Required game rules + */ + public Set getParents() { + return parents; + } + + /** + * Applies this rule the given world. + * + *

+ * + * @param world World to apply + * @return Same world, useful for chaining + */ + public World apply(World world) { + this.getType().apply(world, value); + this.parents.forEach(parent -> parent.getType().apply(world, parent.getValue())); + return world; + } + + @Override + public boolean isValid() { + return getType() != null && getValue() != null && getType().isSameDataType(getValue()); + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleDisableDaylightCycle.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleDisableDaylightCycle.java new file mode 100644 index 0000000..71c0e30 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleDisableDaylightCycle.java @@ -0,0 +1,40 @@ +package com.pepedevs.corelib.utils.world; + +import org.bukkit.World; + +/** A game rule that disables the daylight cycle and sets a permanent time. */ +public class GameRuleDisableDaylightCycle extends GameRule { + + /** The default final time worlds with this rule will have. */ + public static final long DEFAULT_FINAL_TIME = 500L; + + /** The permanent time for the world. */ + private final long permanent_time; + + /** + * Construct a new {@link GameRule} that disables the daylight cycle of the worlds. With the + * default final time {@link GameRuleDisableDaylightCycle#DEFAULT_FINAL_TIME} + */ + public GameRuleDisableDaylightCycle() { + this(GameRuleDisableDaylightCycle.DEFAULT_FINAL_TIME); + } + + /** + * Construct a new {@link GameRule} that disables the daylight cycle of the worlds. + * + *

+ * + * @param permanent_time Permanent time for the world + */ + public GameRuleDisableDaylightCycle(long permanent_time) { + super(GameRuleType.DAYLIGHT_CYCLE, false); + this.permanent_time = permanent_time; + } + + @Override + public World apply(World world) { + super.apply(world); + world.setTime(permanent_time); + return world; + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleHandler.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleHandler.java new file mode 100644 index 0000000..c92f8c7 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleHandler.java @@ -0,0 +1,80 @@ +package com.pepedevs.corelib.utils.world; + +import com.pepedevs.corelib.utils.PluginHandler; +import org.bukkit.GameMode; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.plugin.Plugin; + +/** Handling custom Game rules */ +public class GameRuleHandler extends PluginHandler { + + protected static Plugin plugin; + + public GameRuleHandler(Plugin plugin) { + super(plugin); + register(); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockPlace(BlockPlaceEvent event) { + if (isPositivePresent(GameRuleType.DISABLE_BLOCK_PLACING, event.getBlock().getWorld()) + && event.getPlayer().getGameMode() != GameMode.CREATIVE) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockBreak(BlockBreakEvent event) { + if (isPositivePresent(GameRuleType.DISABLE_BLOCK_BREAKING, event.getBlock().getWorld()) + && event.getPlayer().getGameMode() != GameMode.CREATIVE) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onEntityDamage(EntityDamageEvent event) { + if (isPositivePresent(GameRuleType.INVULNERABLE_ENTITIES, event.getEntity().getWorld())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPvP(EntityDamageByEntityEvent event) { + if (event.getEntity() instanceof Player && event.getDamager() instanceof Player) { + if (isPositivePresent(GameRuleType.DISALLOW_PVP, event.getEntity().getWorld())) { + event.setCancelled(true); + } + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onHunger(FoodLevelChangeEvent event) { + if (isPositivePresent(GameRuleType.DISABLE_HUNGER, event.getEntity().getWorld())) { + event.setCancelled(true); + } + } + + protected boolean isPositivePresent(GameRuleType type, World world) { + return type.getGameRuleMetadata(world) != null + && type.getGameRuleMetadata(world).asBoolean(); + } + + @Override + protected boolean isAllowMultipleInstances() { + return false; + } + + @Override + protected boolean isSingleInstanceForAllPlugin() { + return true; + } + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleMetadata.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleMetadata.java new file mode 100644 index 0000000..76c53aa --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleMetadata.java @@ -0,0 +1,132 @@ +package com.pepedevs.corelib.utils.world; + +import org.apache.commons.lang.Validate; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.NumberConversions; + +/** Class for changing GameRule Metadata values */ +public class GameRuleMetadata extends FixedMetadataValue { + + protected final GameRuleType type; + + /** + * Constructs the {@link GameRuleMetadata}. + * + *

+ * + * @param owning_plugin Plugin instance + * @param type {@link GameRuleType} type + * @param value Value of GameRule + */ + public GameRuleMetadata(Plugin owning_plugin, GameRuleType type, Object value) { + super(owning_plugin, value); + Validate.isTrue(type.isSameDataType(value)); + this.type = type; + } + + /** + * Returns the type of GameRule. + * + *

+ * + * @return {@link GameRuleType} + */ + public GameRuleType getType() { + return type; + } + + /** + * Gets Int meta data. + * + *

+ * + * @return Int meta data + */ + public int asInt() { + validateNumericValue(); + return NumberConversions.toInt(value()); + } + + /** + * Gets Float meta data. + * + *

+ * + * @return Float meta data + */ + public float asFloat() { + validateNumericValue(); + return NumberConversions.toFloat(value()); + } + + /** + * Gets Double meta data. + * + *

+ * + * @return Double meta data + */ + public double asDouble() { + validateNumericValue(); + return NumberConversions.toDouble(value()); + } + + /** + * Gets Long meta data. + * + *

+ * + * @return Long meta data + */ + public long asLong() { + validateNumericValue(); + return NumberConversions.toLong(value()); + } + + /** + * Gets Short meta data. + * + *

+ * + * @return Short meta data + */ + public short asShort() { + validateNumericValue(); + return NumberConversions.toShort(value()); + } + + /** + * Gets Byte meta data. + * + *

+ * + * @return Byte meta data + */ + public byte asByte() { + validateNumericValue(); + return NumberConversions.toByte(value()); + } + + /** + * Gets Boolean meta data. + * + *

+ * + * @return Boolean meta data + */ + public boolean asBoolean() { + validateBooleanValue(); + return (Boolean) value(); + } + + protected void validateNumericValue() { + Validate.isTrue( + (getType().isNumericalValue() && value() instanceof Number), "wrong value type!"); + } + + protected void validateBooleanValue() { + Validate.isTrue( + (getType().isBooleanValue() && value() instanceof Boolean), "wrong value type!"); + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRulePresentationMode.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRulePresentationMode.java new file mode 100644 index 0000000..6970611 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRulePresentationMode.java @@ -0,0 +1,12 @@ +package com.pepedevs.corelib.utils.world; + +/** An enumeration for possible game rule presentation modes. */ +public enum GameRulePresentationMode { + + /** Ordinary Bukkit presentation. */ + ORDINARY, + + /** Custom presentation that allows us to create custom rules. */ + METADATA; + +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleType.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleType.java new file mode 100644 index 0000000..d587621 --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/GameRuleType.java @@ -0,0 +1,555 @@ +package com.pepedevs.corelib.utils.world; + +import com.pepedevs.corelib.utils.reflection.DataType; +import org.apache.commons.lang.Validate; +import org.bukkit.World; +import org.bukkit.metadata.MetadataValue; + +import java.util.stream.Collectors; + +/** Enumeration helper class for all {@link org.bukkit.GameRule} */ +public enum GameRuleType { + FIRE_TICK { + public String getName() { + return "doFireTick"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + MOB_GRIEFING { + public String getName() { + return "mobGriefing"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + KEEP_INVENTORY { + public String getName() { + return "keepInventory"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + MOB_SPAWNING { + public String getName() { + return "doMobSpawning"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + MOB_LOOT { + public String getName() { + return "doMobLoot"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + TILE_DROPS { + public String getName() { + return "doTileDrops"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + ENTITY_DROPS { + public String getName() { + return "doEntityDrops"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + COMMAND_BLOCK_OUTPUT { + public String getName() { + return "commandBlockOutput"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + NATURAL_REGENERATION { + public String getName() { + return "naturalRegeneration"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + DAYLIGHT_CYCLE { + public String getName() { + return "doDaylightCycle"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + LOG_ADMIN_COMMANDS { + public String getName() { + return "logAdminCommands"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + SHOW_DEATH_MESSAGES { + public String getName() { + return "showDeathMessages"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + RANDOM_TICK_SPEED { + public String getName() { + return "randomTickSpeed"; + } + + public boolean isBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return true; + } + + public int getDefaultNumericalValue() { + return 3; + } + }, + + SEND_COMMAND_FEEDBACK { + public String getName() { + return "sendCommandFeedback"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + REDUCED_DEBUG_INFO { + public String getName() { + return "reducedDebugInfo"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + SPECTATORS_GENERATE_CHUNKS { + public String getName() { + return "spectatorsGenerateChunks"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return true; + } + + public boolean isNumericalValue() { + return false; + } + }, + + SPAWN_RADIUS { + public String getName() { + return "randomTickSpeed"; + } + + public boolean isBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return true; + } + + public int getDefaultNumericalValue() { + return 10; + } + }, + + DISABLE_ELYTRA_MOVEMENT_CHECK { + public String getName() { + return "disableElytraMovementCheck"; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + DISABLE_BLOCK_PLACING { + public String getName() { + return "disableBlockPlacing"; + } + + public GameRulePresentationMode getPresentationMode() { + return GameRulePresentationMode.METADATA; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + DISABLE_BLOCK_BREAKING { + public String getName() { + return "disableBlockBreaking"; + } + + public GameRulePresentationMode getPresentationMode() { + return GameRulePresentationMode.METADATA; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + INVULNERABLE_ENTITIES { + public String getName() { + return "invulnerableEntities"; + } + + public GameRulePresentationMode getPresentationMode() { + return GameRulePresentationMode.METADATA; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + DISALLOW_PVP { + public String getName() { + return "disallowPvP"; + } + + public GameRulePresentationMode getPresentationMode() { + return GameRulePresentationMode.METADATA; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + + DISABLE_HUNGER { + public String getName() { + return "disableHunger"; + } + + public GameRulePresentationMode getPresentationMode() { + return GameRulePresentationMode.METADATA; + } + + public boolean isBooleanValue() { + return true; + } + + public boolean getDefaultBooleanValue() { + return false; + } + + public boolean isNumericalValue() { + return false; + } + }, + ; + + public String getName() { + throw new UnsupportedOperationException(); + } + + public DataType getDataType() { + return isBooleanValue() ? DataType.BOOLEAN : DataType.INTEGER; + } + + public GameRulePresentationMode getPresentationMode() { + return GameRulePresentationMode.ORDINARY; + } + + @SuppressWarnings("deprecation") + public Object getValue(World world) { + if (!isPresent(world)) { + return null; + } + + if (getPresentationMode() == GameRulePresentationMode.ORDINARY) { + try { + // this class was implemented on spigot 1.14. + Class.forName("org.bukkit.GameRule"); + return world.getGameRuleValue(org.bukkit.GameRule.getByName(this.getName())); + } catch (ClassNotFoundException ex) { + // deprecated on 1.14, but works for older versions. + return world.getGameRuleValue(this.getName()); + } + } else { + return getGameRuleMetadata(world) != null ? getGameRuleMetadata(world).value() : null; + } + } + + public GameRuleMetadata getGameRuleMetadata(World world) { + Validate.isTrue( + getPresentationMode() == GameRulePresentationMode.METADATA, + "wrong presentation mode!"); + + for (MetadataValue metadata : + world.getMetadata(getName()).stream() + .filter(other -> other instanceof GameRuleMetadata) + .collect(Collectors.toList())) { + if (((GameRuleMetadata) metadata).getType() == this) { + return (GameRuleMetadata) metadata; + } + } + return null; + } + + public boolean isPresent(World world) { + return getPresentationMode() != GameRulePresentationMode.METADATA + || getGameRuleMetadata(world) != null; + } + + public boolean isSameDataType(Object value) { + return value != null && (getDataType() == DataType.fromClass(value.getClass())); + } + + public boolean isBooleanValue() { + throw new UnsupportedOperationException(); + } + + public boolean getDefaultBooleanValue() { + throw new IllegalStateException("wrong value type!"); + } + + public boolean isNumericalValue() { + throw new UnsupportedOperationException(); + } + + public int getDefaultNumericalValue() { + throw new IllegalStateException("wrong value type!"); + } + + @SuppressWarnings({"unchecked", "deprecation"}) + public void apply(World world, Object value) { + Validate.isTrue(isSameDataType(value), "wrong value type!"); + + // ordinary presentation mode + if (getPresentationMode() == GameRulePresentationMode.ORDINARY) { + try { + // this class was implemented on spigot 1.14. + Class.forName("org.bukkit.GameRule"); + switch (getDataType()) { + case BOOLEAN: + default: + world.setGameRule( + (org.bukkit.GameRule) + org.bukkit.GameRule.getByName(getName()), + (Boolean) value); + break; + + case INTEGER: + world.setGameRule( + (org.bukkit.GameRule) + org.bukkit.GameRule.getByName(getName()), + (Integer) value); + break; + } + } catch (ClassNotFoundException ex) { + // deprecated on 1.14, but works for older versions. + world.setGameRuleValue(getName(), value.toString()); + } + } else { // metadata presentation mode + world.setMetadata( + getName(), + new GameRuleMetadata(GameRuleHandler.getSingletonHandler(GameRuleHandler.class).getPlugin(), this, value)); + } + } +} diff --git a/utils/src/main/java/com/pepedevs/corelib/utils/world/WorldUtils.java b/utils/src/main/java/com/pepedevs/corelib/utils/world/WorldUtils.java new file mode 100644 index 0000000..3e5f6db --- /dev/null +++ b/utils/src/main/java/com/pepedevs/corelib/utils/world/WorldUtils.java @@ -0,0 +1,29 @@ +package com.pepedevs.corelib.utils.world; + +import java.io.File; + +/** Class for dealing with worlds. */ +public class WorldUtils { + + public static final String LEVEL_DATA_FILE_NAME = "level.dat"; + public static final String REGION_FOLDER_NAME = "region"; + + /** + * Checks whether the given directory is a world folder. + * + *

+ * + * @param world_folder Directory to check + * @return true if it is a world folder, else false + */ + public static boolean worldFolderCheck(File world_folder) { + if (world_folder.isDirectory() && world_folder.exists()) { + File dat = new File(world_folder, LEVEL_DATA_FILE_NAME); + File region = new File(world_folder, REGION_FOLDER_NAME); + + return dat.exists() && region.exists(); + } else { + return false; + } + } +} diff --git a/vault-hook/pom.xml b/vault-hook/pom.xml new file mode 100644 index 0000000..d07d3bc --- /dev/null +++ b/vault-hook/pom.xml @@ -0,0 +1,42 @@ + + + + CoreLib + com.pepedevs + 1.0 + + 4.0.0 + + vault-hook + + + 8 + 8 + + + + + + jitpack.io + https://jitpack.io + + + + + + com.pepedevs + event-utils + ${project.parent.version} + + + + com.github.MilkBowl + VaultAPI + 1.7 + provided + + + + \ No newline at end of file diff --git a/vault-hook/src/main/java/com/pepedevs/corelib/economy/Balance.java b/vault-hook/src/main/java/com/pepedevs/corelib/economy/Balance.java new file mode 100644 index 0000000..a0223c1 --- /dev/null +++ b/vault-hook/src/main/java/com/pepedevs/corelib/economy/Balance.java @@ -0,0 +1,100 @@ +package com.pepedevs.corelib.economy; + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.OfflinePlayer; + +/** Class for managing player balance */ +public class Balance { + + private final Economy economy; + private final OfflinePlayer player; + + /** + * Constructs the Balance class. + * + *

+ * + * @param economy {@code Vault} {@link Economy} + * @param player Player + */ + public Balance(Economy economy, OfflinePlayer player) { + this.economy = economy; + this.player = player; + } + + /** + * Constructs the Balance class. + * + *

+ * + * @param player Player + */ + public Balance(OfflinePlayer player) { + this(EconomyManager.getEconomy(), player); + if (economy == null) + throw new IllegalStateException("Cannot find economy, vault not found (?)"); + } + + /** + * Gets the balance of player. + * + *

+ * + * @return Balance of player + */ + public double get() { + return economy.getBalance(player); + } + + /** + * Get the formatted value of balance. + * + *

+ * + * @return Formatted value of player balance + */ + public String getFormatted() { + return economy.format(get()); + } + + /** + * Add balance to player + * + *

+ * + * @param money Amount to add + */ + public void give(double money) { + economy.depositPlayer(player, money); + } + + /** + * Subtract balance from player + * + *

+ * + * @param money Amount to subtract + * @return Whether the transaction was successful + */ + public boolean take(double money) { + return economy.withdrawPlayer(player, money).transactionSuccess(); + } + + /** + * Checks if the player account has the amount. + * + *

+ * + *

Note:

+ * + * DO NOT USE NEGATIVE AMOUNTS + * + *

+ * + * @param money Amount to check + * @return {@code true} if the player has the given amount, false otherwise + */ + public boolean has(double money) { + return economy.has(player, money); + } +} diff --git a/vault-hook/src/main/java/com/pepedevs/corelib/economy/EconomyManager.java b/vault-hook/src/main/java/com/pepedevs/corelib/economy/EconomyManager.java new file mode 100644 index 0000000..0038b7f --- /dev/null +++ b/vault-hook/src/main/java/com/pepedevs/corelib/economy/EconomyManager.java @@ -0,0 +1,79 @@ +package com.pepedevs.corelib.economy; + +import com.pepedevs.corelib.events.PluginLoadEvent; +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredServiceProvider; + +/** Class for dealing with Vault economy */ +public class EconomyManager { + + private static boolean enabled = false; + private static Economy economy; + + /** Enables the economy manager and hooks into Vault */ + public static void enable(Plugin handlingPlugin, Runnable notFoundCallback) { + PluginLoadEvent.onPluginLoaded(handlingPlugin, + "Vault", + plugin -> { + RegisteredServiceProvider rsp = + Bukkit.getServicesManager().getRegistration(Economy.class); + if (rsp == null) { + notFoundCallback.run(); + enabled = false; + return; + } + enabled = true; + economy = rsp.getProvider(); + }, notFoundCallback); + } + + /** + * Gets the Vault Economy class if enabled + * + *

+ * + * @return {@link Economy} if economy enabled otherwise null + */ + public static Economy getEconomy() { + return enabled ? economy : null; + } + + /** + * Gets the {@link Balance} of a player + * + *

+ * + * @param player Player to get balance of + * @return {@link Balance} of the given player + */ + public static Balance get(OfflinePlayer player) { + return economy == null ? null : new Balance(player); + } + + /** + * Format amount into a human-readable String. This provides translation into economy specific + * formatting to improve consistency between plugins. + * + *

+ * + * @param v Amount to format + * @return Formatted amount + */ + public static String format(double v) { + return economy.format(v); + } + + /** + * Returns if economy system enabled successfully + * + *

+ * + * @return {@code true} if Vault hook was successful, false otherwise + */ + public static boolean isEnabled() { + return enabled; + } +}